[Django] 日本ひげ男協会のサイトを作成する。その7

注意

この記事は、id:SumiTomohiko:20070126:1169833683の続きです。

環境

この記事の内容は、Ubuntu 6.10, Python 2.4, Django 0.95, Firefox 2.0.0.1で確認しました。

編集画面

今回は編集画面を作成します。会員登録画面と似ているので楽勝だろうと思いきや、問題がひとつありました。それは、画像の扱いについてです。編集画面には、アップロードするファイルを指定するテキストボックスがあります。編集画面を開いた直後は、ここは空欄になっています(アップロードしたファイルのローカルのパスをサーバで覚えておくわけはないので、当り前です)。このままの状態でサーバに送信すると、ファイルがないものとみなされ、既存のファイルが削除されてしまいます。削除されないようにするには、また同じファイルをアップロードしなければなりませんが、これでは不便です。そこで今回は、「画像を削除する」チェックボックスを設けることにしました。ファイル名の欄が空白のときは既存の画像をそのまま使い続け、「画像を削除する」チェックボックスがチェックされたときだけ、画像を削除することにします。これら、ファイル名欄と「画像を削除する」チェックボックスによる動作をデシジョンテーブルで表すと、以下のようになります。

既存の画像の有無 ファイル名 チェックボックス 動作
なし なし チェックしない なにもしない
なし なし チェックする なにもしない
なし あり チェックしない アップロードされた画像を設定する
なし あり チェックする エラーとする
あり なし チェックしない 既存の画像のままとする
あり なし チェックする 既存の画像を削除する
あり あり チェックしない アップロードされた画像で置き換える
あり あり チェックする エラーとする

さてこのようにすると、Userモデルの項目と画面の項目が一対一で対応しなくなるので、以前 (id:SumiTomohiko:20070126:1169768661) と同じように、マニピュレータを作成する必要があります。ここでは、会員登録画面との共通性を利用して、継承して定義します。マニピュレータを記述するファイルは、user/manipulators.pyとしていますが、以下のようになります。

class UserManipulator(forms.Manipulator):
    def __init__(self):
        self.fields = [
            forms.TextField(field_name="account", length=16, maxlength=32, is_required=True, validator_list=[validators.isAlphaNumeric]), 
            forms.TextField(field_name="name", length=16, maxlength=32, is_required=True), 
            forms.PasswordField(field_name="password", length=16, maxlength=32, is_required=True, validator_list=[password_length_validator, password_validator]), 
            forms.PasswordField(field_name="password2", length=16, maxlength=32, is_required=True, validator_list=[password_length_validator]), 
            forms.PositiveSmallIntegerField(field_name="from_year", maxlength=4, is_required=True, validator_list=[from_year_validator]), 
            forms.ImageUploadField(field_name="image_file"), 
            forms.LargeTextField(field_name="introduction", rows=5, maxlength=8192), 
            forms.URLField(field_name="web", maxlength=256), 
            forms.URLField(field_name="blog", maxlength=256),
        ]

class UserAddManipulator(UserManipulator):
    pass

class UserEditManipulator(UserManipulator):
    def __init__(self):
        super(UserEditManipulator, self).__init__()
        self.fields.extend([
            forms.HiddenField(field_name="id"), 
            forms.CheckboxField(field_name="delete_image", validator_list=[delete_image_validator])])

UserEditManipulatorのdelete_imageフィールドのヴァリデータに設定されているdelete_image_validatorは、ファイルをアップロードしながら削除も指示していないか検証するためのヴァリデータです。user/validators.pyで、以下のように定義されています。

class DeleteImageValidator(object):
    def __init__(self, message="画像をアップロードするか、削除するか、どちらかにしてください。"):
        self._message = message

    def __call__(self, field_data, all_data):
        if (all_data.has_key("image_file")) and (field_data == "on"):
            raise ValidationError, self._message

ビューは、HTTPのメソッドがPOSTならデータベースを更新し、そうでなければ新規に画面を描画するという、いつものパターンで作成します。会員登録画面との共通点を関数化しつつ、以下のようにしました。

def _save_user(user, new_data, manipulator, errors):
    manipulator.do_html2python(new_data)

    user.account = new_data["account"]
    user.name = new_data["name"]
    user.password = new_data["password"]
    user.from_year = new_data["from_year"]
    user.introduction = new_data["introduction"]
    user.web = new_data["web"]
    user.blog = new_data["blog"]

    image_file = new_data["image_file"]
    if image_file:
        filename = image_file["filename"]
        user.image = filename
        user.save_image_file(filename, image_file["content"])
    elif new_data.has_key("delete_image") and new_data["delete_image"]:
        filename = user.get_image_filename()
        if os.path.exists(filename):
            os.remove(filename)
        user.image = ""

    try:
        user.save()
    except StandardError, e:
        if str(e) != "column account is not unique":
            raise
        _add_error(errors, "account", "このアカウント名はすでに使用されています。")

def edit(request, id):
    manipulator = manipulators.UserEditManipulator()
    if request.POST:
        new_data = request.POST.copy()
        new_data.update(request.FILES)
        errors = manipulator.get_validation_errors(new_data)
        if not errors:
            u = User.objects.get(id=id)
            _save_user(u, new_data, manipulator, errors)
            if not errors:
                return _redirect_to_list()
        else:
            u = User.objects.get(id=id)
    else:
        errors = {}
        u = User.objects.get(id=id)
        new_data = dict(id=id, account=u.account, name=u.name, password=u.password, password2=u.password, from_year=u.from_year, introduction=u.introduction, web=u.web, blog=u.blog)

    form = forms.FormWrapper(manipulator, new_data, errors)
    return render_to_response('user/user_edit.html', { "form": form, "location": juma.settings.LOCATION, "image_url": u.get_image_url() })

def add(request):
    manipulator = manipulators.UserAddManipulator()
    if request.POST:
        new_data = request.POST.copy()
        new_data.update(request.FILES)
        errors = manipulator.get_validation_errors(new_data)
        if not errors:
            u = User()
            _save_user(u, new_data, manipulator, errors)
            if not errors:
                request.session[juma.user.settings.USER_ID_KEY] = u.id
                return _redirect_to_list()
    else:
        errors = new_data = {}

    form = forms.FormWrapper(manipulator, new_data, errors)
    return render_to_response('user/user_add.html', { "form": form })

この後テンプレートを作成したのですが、面白くともなんともないので、省略します。

あとは、user/urls.pyにURLを追加するだけです。以下の1行を追加します。

    (r'^edit/(?P<id>[\d]+)/$', 'juma.user.views.edit'), 

これで編集画面ができました。編集画面では、現在の画像も表示できるようになっています。

なお、エラーで画面が再表示されたとき、ファイル名が空白になってしまうのは、仕様です。仮りに<input type="file" value="/foo/bar">と書いても、ファイル名には何も表示されません。この点に関しては、利用する方々に御不便をおかけします。

著作権表示

スクリーンショットに含まれるウェルシュコーギーの画像の著作権は、フォトカフェさんが保持します。