DjangoのClearableFileInputのテンプレートをカスタマイズ

Django

ImageFieldを使っているとDjangoAdmin側のフォーム部分のリンクがバグる

自分が携わっているプロジェクトでは、S3に置かれている画像のURLをImageFieldにフルパスで入れているのですが、これだとAdmin側の編集画面で、画像のリンクの飛び先がバグってしまうということがありました。

原因としては、clearable_file_input.htmlのhrefにwidget.value.urlが使われていることで、その値がおかしいということが分かりました。

そもそもwidget.value.urlはどこから来ているのか?

フォーム上で表示されている画像のリンク自体は正しいものが表示されているのに、実際の飛び先であるhrefは正しくない飛び先ということで、clearable_file_input.htmlのファイルの中身を見てみると、上記のとおり、hrefの中身はwidget.value.urlテキスト部分はwidget.valueが使われているという謎仕様でした。

そこでこのwidget.value.urlはどこから来ているのかを調べると、私の環境では、S3にファイルをアップロードする為にdjango-storagesというパッケージを使っているのですが、この中のurlが問題だということが分かり、これのサブクラスを作って、FileStorageに使ってあげれば良いという話でした。

コード的にはこんな感じで、url関数をoverrideして、name部分を弄ってやれば問題なくなりました。

from storages.backends.s3boto3 import S3Boto3Storage

class S3FilesStorage(S3Boto3Storage):
    def url(self, name, parameters=None, expire=None, http_method=None):
        # Preserve the trailing slash after normalizing the path.
        # name = self._normalize_name(self._clean_name(name))  <-- これをコメントアウト
        if expire is None:
            expire = self.querystring_expire

        if self.custom_domain:
            url = "{}//{}/{}".format(
                self.url_protocol, self.custom_domain, filepath_to_uri(name))

            if self.querystring_auth and self.cloudfront_signer:
                expiration = datetime.utcnow() + timedelta(seconds=expire)

                return self.cloudfront_signer.generate_presigned_url(url, date_less_than=expiration)

            return url

        params = parameters.copy() if parameters else {}
        params['Bucket'] = self.bucket.name
        params['Key'] = name
        url = self.bucket.meta.client.generate_presigned_url('get_object', Params=params,
                                                             ExpiresIn=expire, HttpMethod=http_method)
        if self.querystring_auth:
            return url
        return self._strip_signing_parameters(url)

画像ファイルをtarget=”_blank”で開きたい

上記の変更により、URL自体は正常なものになったのですが、画像ファイルをtarget=”_blank”で開きたいという話も出ました。

そこで、clearable_file_input.htmlをカスタマイズする必要性があり、独自のものに置き換えました。

こちらはなかなか大変で、幾つかのステップをかまして、やっと実装出来ました。実装方法は下記のとおりです。

サブクラスを作り、テンプレートパスの変更

まずは、ClearableFileInputのサブクラスを作り、使われているテンプレートのパスを変更します。

from django import forms

class CustomAdminFileWidget(forms.ClearableFileInput):
    template_name = 'admin/widgets/custom_clearable_file_input.html'

テンプレートの作成

上記でパス指定した箇所に、custom_clearable_file_input.htmlを作成し、target=”_blank”を入れます。

{% if widget.is_initial %}<p class="file-upload">{{ widget.initial_text }}: <a href="{{ widget.value.url }}" target="_blank">{{ widget.value }}</a>{% if not widget.required %}
<span class="clearable-file-input">
<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}"{% if widget.attrs.disabled %} disabled{% endif %}>
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label></span>{% endif %}<br>
{{ widget.input_text }}:{% endif %}
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>{% if widget.is_initial %}</p>{% endif %}

新しく作ったテンプレートのパスを見に行くようにする

普通の設定のままだと新しく作ったテンプレートを見に行ってくれずに、これが一番ハマりました。
設定ファイルに以下を追加する必要があります。

INSTALLED_APPS = (
    'django.forms', ...
)
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'

色々と悩みましたが、こちらのstackoverflowに上記解決法が載っていました。

Adminでwidgetを上書きする

Adminファイル内でwidgetに作ったサブクラスを設定します。

from django.contrib.admin.options import FORMFIELD_FOR_DBFIELD_DEFAULTS
from django.db import models
from utils.admin import CustomAdminFileWidget

FORMFIELD_FOR_DBFIELD_DEFAULTS[models.ImageField] = {'widget': CustomAdminFileWidget}
FORMFIELD_FOR_DBFIELD_DEFAULTS[models.FileField] = {'widget': CustomAdminFileWidget}

以上のやり方で、templateの書き換えまでを行うことが出来ました。
なかなか大変でした。

最後に

確かにDjangoAdminは便利なのですが、ところどころカスタマイズしたいところが出てくるので、あまり弄ることのない部分ですが、やってみて色々と勉強になりました。

コメントを残す