このレッスンでは写真の投稿機能と削除機能を作ります。基本的には2つ目のチュートリアルでメモを作った時と同様で、ModelFormを使います。

投稿機能を実装する

まずは投稿画面を作ります。URLの設定をします。

~/PhotoService/app/urls.py

app_name = 'app'
urlpatterns = [
    path('', views.index, name='index'),
    path('users/<int:pk>', views.users_detail, name='users_detail'),
    path('photos/new/', views.photos_new, name='photos_new'), # 追加
    path('signup/', views.signup, name='signup'),
    path('login/', auth_views.LoginView.as_view(template_name='app/login.html'), name='login'),
    path('logout/', auth_views.LogoutView.as_view(), name='logout'),
]    

投稿画面のHTMLファイルを作ります。投稿フォームのimageフィールドからは、画像ファイルをアップロードすることになります。ファイルをアップロードする場合は、formタグにenctype="multipart/form-data"をつけないと正常にアップロードできないので注意しましょう。

~/PhotoService/app/templates/app/photos_new.html

{% extends 'app/base.html' %}

{% block content %}

<div>
    <a href="{% url 'app:index' %}">ホームに戻る</a>
</div>

<form action="{% url 'app:photos_new' %}" method="POST" enctype="multipart/form-data">{% csrf_token %}
    <table>
        <tr>
            <th>タイトル</th>
            <td>{{ form.title }}</td>
        </tr>
        <tr>
            <th>コメント</th>
            <td>{{ form.comment }}</td>
        </tr>
        <tr>
            <th>画像</th>
            <td>{{ form.image }}</td>
        </tr>
        <tr>
            <th>カテゴリー</th>
            <td>{{ form.category }}</td>
        </tr>
    </table>
    <button type="submit" class="btn">保存</button>
</form>

{% endblock %}

Photoを投稿する用のModelFormを作ります。

~/PhotoService/app/forms.py

from django.forms import ModelForm
from .models import Photo

class PhotoForm(ModelForm):
    class Meta:
        model = Photo
        fields = ['title', 'comment', 'image', 'category']

base.htmlの「投稿」ボタンのリンクを設定し、ヘッダーのボタンから投稿画面に飛べるようにします。

~/PhotoService/app/templates/app/base.html

<div class="header-menu">
    <a href="{% url 'app:photos_new' %}">投稿</a> <!-- 更新 -->
    {% if request.user.is_authenticated %}
        <a href="{% url 'app:users_detail' request.user.id %}">マイページ</a>
        <a href="{% url 'app:logout' %}">ログアウト</a>
    {% else %}
        <a href="{% url 'app:login' %}">ログイン</a>
    {% endif %}
</div>

views.pyで、Photoの新規投稿機能を実装します。

~/PhotoService/app/views.py

from django.contrib.auth.decorators import login_required # 追加
from .forms import PhotoForm # 追加

@login_required  # ①
def photos_new(request):
    if request.method == "POST":
        form = PhotoForm(request.POST, request.FILES)  # ②
        if form.is_valid():
            photo = form.save(commit=False)  # ③
            photo.user = request.user # ④
            photo.save()  # ⑤
        return redirect('app:users_detail', pk=request.user.pk)
    else:   
        form = PhotoForm()
    return render(request, 'app/photos_new.html', {'form': form})

各ポイントに番号をつけましたので、それぞれ説明します。

@login_requiredというものをインポートして、関数の上につけています。@マークがついているものはPythonのデコレーターと呼ばれる機能です。簡単に説明すると、デコレーターは関数の上につけることによってその関数を加工することができます。

今回の場合は、@login_requiredをつけることによって、ユーザーがログイン状態であればphotos_new関数をそのまま実行し、ログインしていない状態であればphotos_new関数を実行せずにログイン画面(settings.pyで設定したLOGIN_URL)にリダイレクトさせるようにしています。ログインしているユーザーだけに限定したい関数には@login_requiredをつけましょう。ログインしていないユーザーが写真を投稿できてしまってはまずいので、photos_new関数にはこのデコレーターをつけます。

このあと実装する削除機能では、@require_POSTというデコレーターを使って、HTTPリクエストがPOSTメソッドの時にしかその関数を実行しないという処理をしています。

デコレーターは自作することもできますが、このようにDjangoが提供しているデコレーターを適宜インポートして使うと良いでしょう。

② 入力された情報からフォーム情報を生成します。ファイル情報を受け取るときは、request.FILESがないと正常にアップロードされないので気をつけてください。今回はimageフィールドから画像ファイルがアップロードされるので、これが必要です。

③ 入力された情報から、Photoインスタンスを生成します。しかし、form.save(commit=False)のように、saveメソッドのcommit引数をFalseにすることで、DBには保存しないようにしています。なぜなら、この段階ではPhotoインスタンスのuserフィールドに入れる値が決まっていないからです。(PhotoFormから受け取るのは['title', 'comment', 'image', 'category']だけのため、userフィールドに入れる値は取得できない。)

仮に、form.save()とした場合、NOT NULL constraint failed: app_photo.user_idというエラーが表示されます。PhotoインスタンスのuserフィールドはNullにできない設定にしているにも関わらず、userフィールドが空の状態で保存しようとしているからです。

④ ③で一時的に生成したPhotoインスタンスのuserフィールドに、request.userを代入します。

⑤ この段階で、全てのフィールドに値が入った状態になったので、インスタンスをDBに保存します。

ここまでできたら、実際にフォームから投稿してみましょう。投稿後、マイページにリダイレクトされて画像が表示されれば成功です!

messageを表示する

写真が正常にアップロードされた場合は、成功メッセージを表示してあげると親切です。Djangoではメッセージを簡単に表示する機能があるので、以下のように実装してみましょう。

~/PhotoService/app/views.py

from django.contrib import messages # 追加

@login_required
def photos_new(request):
    if request.method == "POST":
        form = PhotoForm(request.POST, request.FILES)
        if form.is_valid():
            photo = form.save(commit=False)
            photo.user = request.user
            photo.save()
            messages.success(request, "投稿が完了しました!") # 追加                     
        return redirect('app:users_detail', pk=request.user.pk)
    else:   
        form = PhotoForm()
    return render(request, 'app/photos_new.html', {'form': form})

~/PhotoService/app/templates/app/base.html

    <div class="container">
        {% for message in messages %}
            <p class="message-success">{{ message }}</p>
        {% endfor %}
        {% block content %}{% endblock %}
    </div>    

これで、投稿時にメッセージが表示されるようになりました。

削除機能の実装

削除機能を実装します。2つ目のチュートリアルでもやってるので、詳細な説明は省きます。まずは、各写真の詳細を表示するページを用意して、そのページに削除ボタンを設置するようにしたいと思います。

~/PhotoService/app/urls.py

app_name = 'app'
urlpatterns = [
    path('', views.index, name='index'),
    path('users/<int:pk>', views.users_detail, name='users_detail'),
    path('photos/new/', views.photos_new, name='photos_new'),
    path('photos/<int:pk>/', views.photos_detail, name='photos_detail'), # 追加
    path('photos/<int:pk>/delete/', views.photos_delete, name='photos_delete'), # 追加
    path('signup/', views.signup, name='signup'),
    path('login/', auth_views.LoginView.as_view(template_name='app/login.html'), name='login'),
    path('logout/', auth_views.LogoutView.as_view(), name='logout'),
]    

~/PhotoService/app/views.py

from django.views.decorators.http import require_POST   # 追加

def photos_detail(request, pk):
    photo = get_object_or_404(Photo, pk=pk)
    return render(request, 'app/photos_detail.html', {'photo': photo})

@require_POST
def photos_delete(request, pk):
    photo = get_object_or_404(Photo, pk=pk)
    photo.delete()
    return redirect('app:users_detail', request.user.id)

写真をクリックすると、詳細ページに飛ぶようにリンクを設定します。

~/PhotoService/app/templates/app/index.html

<h2>トップページ</h2>

{% for photo in photos %}
<a href="{% url 'app:photos_detail' photo.id %}">
    <img src="{{ photo.image.url }}" class="photo-img">
</a>
{% endfor %}

~/PhotoService/app/templates/app/users_detail.html

<h2 class="user-name">@{{ user.username }}</h2>

{% for photo in photos %}
<a href="{% url 'app:photos_detail' photo.id %}">
    <img src="{{ photo.image.url }}" class="photo-img">
</a>
{% endfor %}

最後に、photos_detail.htmlを作り削除ボタンを表示させます。

~/PhotoService/app/templates/app/photos_detail.html

{% extends 'app/base.html' %}

{% block content %}

<div class="photo-detail">

<img src="{{ photo.image.url }}" class="photo-img">
<div class="photo-info">
    <a href="{% url 'app:users_detail' photo.user.id %}">@{{ photo.user }}</a>
</div>

<h2>{{ photo.title }}</h2>
<p>{{ photo.comment }}</p>

<!-- 削除ボタン -->
<form method="post" action="{% url 'app:photos_delete' photo.id %}">{% csrf_token %}
    <button class="btn" type="submit" onclick='return confirm("本当に削除しますか?");'>削除</button>
</form>

</div>
{% endblock %}

削除ボタンを表示させることができました。ただ今のままだと、このページにアクセスしてきたユーザーの誰しもが削除ボタンを押して写真を消去することができてしまいます。

アクセスしてきたユーザー=この写真を投稿したユーザーのときだけ削除ボタンが表示されれば良いので、以下のようにif文を追加します。

~/PhotoService/app/templates/app/photos_detail.html

<!-- 削除ボタン -->
{% if request.user == photo.user %}
<form method="post" action="{% url 'app:photos_delete' photo.id %}">{% csrf_token %}
    <button class="btn" type="submit" onclick='return confirm("本当に削除しますか?");'>削除</button>
</form>
{% endif %}

これで、写真の投稿者だけに削除ボタンが表示されるようになりました。

次のレッスンでは、リファクタリングについて学びます。

< PREV NEXT >
SHARE ! Tweet