2-11. 画像アップロード機能を付与する

今回のテーマは「画像アップロード機能を付与する」です。掲示板に画像のアップロード機能をつけることでファイルの扱いについて見ていきましょう。全てのファイルの種類を扱うのは難しいので画像データのアップロードという点に焦点を当てて見ていきたいと思います。

※本ページはCookieへのデータの保存と読み出しまで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります。

MEDIA_ROOTの設定

Djangoでファイルを扱うにはMEDIA_ROOT変数でファイルを取り扱うディレクトリを指定する必要があります。このディレクトリはDjangoの実行ユーザーの書き込み権限がある必要があります。今回はmysiteプロジェクト内にmediaディレクトリを作成することにします。少々ややこしいのでディレクトリ構成を図示します。


mysite
│└mysite
│─base
│─thread
│─search
│─static
└─media

(venv)$ cd mysite #プロジェクトディレクトリ
(venv)$ mkdir -p media

このmediaディレクトリを認識させるためにmysite/settings.pyに以下を追記します。

mysite/settings.py(一部抜粋)


+ MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
+ MEDIA_URL = '/media/'

MEDIA_URL変数はWEBアプリでファイルを指し示すURLのルートとなります。今回は’/media/’としましたが、他のフレーズでもOKです。
次にmysite/urls.pyの設定をします。urlpatterns変数を以下の様に変更します。

mysite/urls.py(一部抜粋)


+ from django.conf.urls.static import static
  urlpatterns = [
      path('admin/', admin.site.urls),
      path('accounts/', include('django.contrib.auth.urls')),
      path('', include('base.urls')),
      path('thread/', include('thread.urls')),
      path('api/', include('api.urls')),
      path('search/', include('search.urls')),
      path('sitemap.xml', sitemap, {'sitemaps': sitemaps}),
- ]
+ ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

モデルの作成

今回はFileFieldを継承したImageFieldを使っていきます。ImageFieldを使う場合はPillowが必要になるためインストールします。


(venv)$ pip install pillow

コメント投稿時に画像をアップロードできるようにするためthread/models.pyを修正していきます。
thread/models.py(Commentクラス関連部分のみ)


class CommentManager(models.Manager):
    # Comment操作に関する処理を追加
    def create_comment(self, user_name, message, topic_id, image=None):
        comment = self.model(
            user_name=user_name,
            message=message,
            image=image
        )
        comment.topic = Topic.objects.get(id=topic_id)
        comment.no = self.filter(topic_id=topic_id).count() + 1
        comment.save()

class Comment(models.Model):
    id = models.BigAutoField(
        primary_key=True,
    )
    no = models.IntegerField(
        default=0,
    )
    user_name = models.CharField(
        'お名前',
        max_length=30,
        null=True,
        blank=False,
    )
    topic = models.ForeignKey(
        Topic,
        on_delete=models.PROTECT,
    )
    message = models.TextField(
        verbose_name='投稿内容'
    )
    image = models.ImageField(
        verbose_name='投稿画像',
        validators=[FileExtensionValidator(['jpg', 'png'])],
        upload_to='images/%Y/%m/%d/',
        null=True,
        blank=True,
    )
    pub_flg = models.BooleanField(
        default=True,
    )
    created = models.DateTimeField(
        auto_now_add=True,
    )
    objects = CommentManager()

    def __str__(self):
        return '{}-{}'.format(self.topic.title, self.no)

併せてビューも修正しておきます。

thread/views.py(一部抜粋)


  class TopicViewAndCommentCreateView(FormView):
      template_name = 'thread/detail_topic.html'
      form_class = CommentModelForm
      
      def form_valid(self, form):
          Comment.objects.create_comment(
              user_name=form.cleaned_data['user_name'],
              message=form.cleaned_data['message'],
              topic_id=self.kwargs['pk'],
+             image=form.cleaned_data['image']
          )
          response = super().form_valid(form)
          return response
  
      def get_success_url(self):
          return reverse_lazy('thread:topic', kwargs={'pk': self.kwargs['pk']})
      
      def get_context_data(self):
          ctx = super().get_context_data()
          ctx['topic'] = Topic.objects.get(id=self.kwargs['pk'])
          ctx['comment_list'] = Comment.objects.filter(
                  topic_id=self.kwargs['pk']).annotate(vote_count=Count('vote')).order_by('no')
          return ctx

ではテンプレートを修正していきましょう。templates/thread/detail_topic.htmlは以下のように修正されます。validatorsで拡張子によるバリデーション処理を行うように指定しています。特定の拡張子しか受け付けたくないときには便利です。upload_toにはmediaディレクトリ内のアップロードファイルを指定します。%Yのような指定子を用いることで日付や時間をディレクトリ名とすることも出来ます。今回は画像なしでもコメント投稿できるようにするためにnull,blankはTrueとしています。

templates/thread/detail_topic.html



  {% extends 'base/base.html' %}
  {% block title %}{{topic.title}} - {{ block.super }}{% endblock %}
  {% block content %}
  {% load threadfilters %}
  {% load static %}
  <div class="ui grid stackable">
      <div class="eleven wide column">
          <div class="ui breadcrumb">
              <a href="{% url 'base:top' %}" class="section">TOP</a>
              <i class="right angle icon divider"></i>
              <a href="{% url 'thread:category' url_code=topic.category.url_code %}" class="section">{{topic.category.name}}</a>
              <i class="right angle icon divider"></i>
              <a class="active section">{{topic.title}}</a>
          </div>
          <div class="ui segment">
              <div class="content">
                  <div class="header"><h3>{{topic.title}}</h3></div>
                  <p>{{topic.user_name}} - {{topic.created}}</p>
                  <div class="ui segment">
                      <p><pre>{{topic.message}}</pre></p>
                  </div>
              </div>
          </div>
          <!--コメント表示-->
          <div class="ui segment">
              {% if comment_list %}
              {% for comment in comment_list %}
              <div class="ui segment secondary">
                  <p>{{comment.no}}. {{comment.user_name}}<br>{{comment.created}}</p>
                  {% if comment.pub_flg %}
                  <p>{{comment.message | comment_filter | safe}}</p>
+                     {% if comment.image %}
+                     <a href="{{comment.image.url}}" target="_blank" rel="noopener noreferrer"><img src="{{comment.image.url}}" width=200px></a>
+                     {% endif %}
                  <div class="vote_button ui right aligned vertical segment" style="cursor: pointer;"
                      data-comment-id="{{comment.id}}" data-count="{{comment.vote_count}}">
                      <i class="heart outline icon"></i>
                      <span class="vote_counter">
                          {% if comment.vote_count > 0 %}{{comment.vote_count}}{% endif %}
                      </span>
                  </div>
                  {% else %}
                  <p style="color: grey">コメントは非表示とされました</p>
                  {% endif %}
              </div>
              {% endfor %}
              {% else %}
              <div class="ui warning message"><p>まだコメントはありません</p></div>
              {% endif %}
          </div>
          <!--//コメント表示-->
          <!--コメント投稿-->
          <h4>コメント投稿</h4>
          <div class="ui segment">
-             <form class="ui form" action="" method="POST">
+             <form class="ui form" action="" method="POST" enctype="multipart/form-data">
                  {% csrf_token %}
                  {{form.as_p}}
                  <button class="ui button orange" type="submit">コメント投稿</button>
              </form>
          </div>
          <!--//コメント投稿-->
      </div>
      {% include 'base/sidebar.html' %}
  </div>
  {% endblock %}
  {% block js %}
  <script src="{% static 'js/vote.js' %}" type='text/javascript'></script>
  {% endblock %}

まず、投稿したファイルをimgタグで呼び出しています。ファイルのpathはimageのurl属性で取得することができます。MEDIA_ROOTでしたいした’/media/’を含むpathが返されます。

注意点ですが、formタグのenctypeを”multipart/form-data”としないとファイルが送信出来ませんので忘れず修正して下さい。

実は画像をDjangoで扱う場合には上記だけでは不十分なことが多く、画像表示が重たくならないようにサムネイルやサイズごとの画像を用意する等の工夫がされることが多いのですが、サードパーティの機能の解説になることもあり、別の機会にできればと考えています。

では、コメント投稿時に画像が投稿できるか確かめてみましょう。コメント投稿欄の「投稿画像」部分のボタンを押すと画像選択用のウィンドウが開きます。(ブラウザに酔って挙動は異なります)

そのまま投稿するとコメント欄に画像が表示されます。

最後に

今回は基本的なファイルの扱いについて見てきました。実際はdjango-imagekitライブラリ等を使用することが多いと思いますが、それについては別の機会にしたいと思います。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です