This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

More than 3 years have passed since last update.

[Day 32]画像アップロード機能を付与する

Posted at

February 10, 2021
←前回:Day 31 Cookieへのデータの保存と読み出し

「Djangoを学びたい」とのことでありましたら[Day 1]Djangoの開発環境から読むことをおすすめします。

はじめに

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

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

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

では、コメント投稿時に画像が投稿できるか確かめてみましょう。コメント投稿欄の「投稿画像」部分のボタンを押すと画像選択用のウィンドウが開きます。

ここまでやったのですが、imageがうまく入らないので今回の昨日はやめます。
最近、Djangoの概要がわからなくなってきているので、次のタイムゾーンが終わったら一から復習しようかと思います。

おわりに

今日は大学の成績発表でした。
大学でいい成績をとったところで。。。って話ですが、いい成績を取れると嬉しいものです。
前年度首席でした。で、今年度は前年度よりも成績がよかったのでおそらく首席維持です。
しかし、私の大学は首席をとったところで学費免除になったり、お得な特典がないので残念ですが、一つあげるとすれば、事務の審査が通りやすいということです。
研究援助金などのお金関係のものが少し通りやすいような気もします。
無理して取る必要はありませんが、とって損はないので,取れる環境にいるなら頑張ってみる価値はあると思います。

それではまたまた

←前回:Day 31 Cookieへのデータの保存と読み出し
→次回:Day 33

0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up