このページでは、前回開発した「数当てゲーム」の拡張を行っていきます。
このサイトでは Django での数当てゲームの作り方を前編・後編構成で解説しており、このページは「後編」となります。下記ページの前編では、”最低限、数当てゲームがプレイ可能なウェブアプリ” の作り方についてのみ解説しました。この後編では、前編で開発したウェブアプリの機能拡張の例を示していきます。
【Python/Django】数当てゲーム(Hit&Blow)を開発【前編】具体的には、数当てゲームに下記の機能を追加していきます。
- ログイン機能
- ランキング表示機能
- 回答履歴の表示機能
特に上側2つの機能は、ウェブアプリでゲームを開発する時に搭載する機会の多い機能になりますので、これらの実現方法・実装方法を理解しておけば、今後のウェブアプリ開発で役に立つと思います。
また、これらの機能は、前編で開発したウェブアプリに変更を加えていくことで追加していきます。そのため、下記の前編の解説ページを読まれていない方は、先に下記ページを読んでいただくことをオススメします。
【Python/Django】数当てゲーム(Hit&Blow)を開発【前編】ログイン機能
まず、ログイン機能を実現していきます。
ログイン機能の必要性
前編では、各クライアントから独立してゲームがプレイできるように、クライアントから送信されてくる「セッション ID」を利用してクライアントの識別を行うようにしてきました。
ただし、あくまでもセッションで識別可能なのは「クライアント」です。つまり、ユーザーが利用しているウェブブラウザやデバイス(PC やスマホ等)を識別することは可能なのですが、ユーザー自体は識別できません。なので、同じユーザーがウェブアプリを利用したとしても、異なるウェブブラウザから利用すると異なるユーザーと判断されることになります。
また、セッションの有効期限が切れたりクライアント側でセッションが削除されたりすると、同じクライアントからウェブアプリを利用したとしてもウェブアプリで異なるクライアントと判断されることになるため、このような場合はゲームを継続してプレイすることができません。
セッションを利用したクライアントの識別は実現が簡単ではあるのですが、この識別には上記のような問題があります。
このような問題は、ウェブアプリにログイン機能を搭載することで解決可能です。まず、ログイン機能を搭載した場合、ログイン時に入力した情報に基づいてウェブアプリを利用中のユーザーが特定されることになるため、ログイン時に同じ情報を入力すれば、異なるデバイス・異なるウェブブラウザを利用していても同じユーザーとして識別されることになります。また、セッションが削除されたとしても再度同じ情報を入力してログインを行えば同じユーザーとして識別されることになるため、今までと同じゲームを継続してプレイすることができます。
このように、セッションでのユーザーの識別の問題はログイン機能によって解決可能です。
スポンサーリンク
ログイン機能の実現
ということで、ここからはログイン機能の実現方法について解説していきます。
ログイン機能を実現するために、まず必須となるのがログインフォームとなります。このログインフォームにユーザー名やパスワード等をユーザーに入力してもらうことで、ログインが実施されることになります。
ただし、ログインフォームさえ導入すればログインが実現できるというわけではありません。ざっと挙げると、ログインを実現し、さらにログイン機能を意味のあるものとするためには下記のようなものも必要となります。
- ユーザー登録フォーム
- ログインフォーム
- ログアウトボタン(ログアウトフォーム)
- 非ログインユーザーのアクセス制限
- ユーザーに応じた処理や表示の切り替え
本来であれば、上記に加えて「カスタムユーザー」の定義も必要となります。カスタムユーザーとは、開発者がカスタマイズした「ユーザー管理モデルクラス」のことになります。ですが、今回は実装を楽にするため、カスタムユーザーの代わりに Django にあらかじめ定義されている User
というモデルクラスを利用することにしたいと思います。もし、カスタムユーザーについて詳しく知りたいという方がおられましたら、下記ページを参照していただければと思います。
ということで、ここから上記の 1. 〜 5. を導入することで、数当てゲームにログイン機能を搭載していきます。ただ、1. ~ 5. の詳細に関しては別途下記ページで解説済みですので、ここでは変更内容をサラッと解説するのみとさせていただきたいと思います。詳細を知りたい方は、お手数をおかけしますが下記ページを参照していただければと思います。
【Django入門10】ログイン機能の実現ログイン用アプリの作成・登録
では、下記の前編のページで開発したウェブアプリを変更し、ログイン機能を実装していきたいと思います。
【Python/Django】数当てゲーム(Hit&Blow)を開発【前編】現状、数当てゲームのウェブアプリは number_guess
プロジェクトに game
アプリを登録する構成で開発してきています。
今回は、この構成に対し、ログイン及びユーザー管理を専用とするアプリとして accounts
を追加し、この accounts
のファイルを編集してログイン・ユーザー管理関連の機能を実現していきます。
ということで、まずは number_guess
フォルダ(manage.py
が存在するフォルダ)で下記コマンドを実行して number_guess
プロジェクトに accounts
アプリを作成してください。
% python manage.py startapp accounts
続いて、下記のように number_guess/settings.py
の INSTALLED_APPS
に 'accounts',
を追加してください。
INSTALLED_APPS = [
'game',
'accounts',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
さらに、number_guess/urls.py
を下記のように変更し、URL が /accounts/
から始まる HTTP リクエストを受け取った時に accounts/urls.py
の設定が参照されるように設定します(accounts/urls.py
は後ほど作成します)。
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('game/', include('game.urls')),
path('accounts/', include('accounts.urls')),
]
フォームの定義
ここからは、基本的には accounts
フォルダ内のファイルの編集およびファイルの追加によって、ログイン関連の機能を実現していくことになります(一部 game
フォルダ内のファイルの編集も行います)。
まずは、必要なフォームを定義していきたいと思います。定義するフォームは「ユーザー登録フォーム」と「ログインフォーム」の2つとなります(ログアウト時はデータの入力は不要なのでフォームの定義も不要)。これらを定義するため、accounts/forms.py
を新規作成し、中身を下記のように変更してください。名前からも推測できると思いますが、SignupForm
がユーザー登録フォームで、LoginForm
がログインフォームとなります。
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
class SignupForm(UserCreationForm):
class Meta:
model = User
fields = ['username', 'email']
class LoginForm(AuthenticationForm):
pass
下記ページでも解説しているように、ユーザー登録フォームは UserCreationForm
のサブクラスとして定義する必要があります。
さらに、下記ページでも解説しているように、ログインフォームは AuthenticationForm
のサブクラスとして定義する必要があります(そのまま AuthenticationForm
を利用しても良いです)。
詳細は上記の各ページで解説していますが、これらのサブクラスとしてフォームを定義することでパスワードが暗号化されるようになり、セキュリティの高いウェブアプリを実現することができます。
また、上記における User
は Django であらかじめ定義されているユーザーを管理するモデルクラスとなります。django.contrib.auth.models
から import
してやれば、別途モデルクラスを定義することなくウェブアプリでユーザーを管理することができるようになります。
スポンサーリンク
ビューの定義
続いて、ユーザー登録・ログイン・ログアウトを実現するビューを定義していきます。
結論としては、accounts/views.py
を下記のように変更することで、ユーザー登録用のビュー(Signup
)・ログイン用のビュー(Login
)・ログアウト用のビュー(Logout
)を定義することができます。また、下記における 'login'
や 'index'
という文字列は URL の名前になります。'index'
については、game/urls.py
で設定したゲームプレイ用の URL であり、'login'
に関しては後述でログイン用の URL に設定する URL となります。
from django.contrib.auth.views import LoginView, LogoutView
from django.views.generic import CreateView
from django.urls import reverse_lazy
from .forms import SignupForm, LoginForm
class Signup(CreateView):
form_class = SignupForm
success_url = reverse_lazy('login')
template_name = 'accounts/signup.html'
class Login(LoginView):
form_class = LoginForm
template_name = 'accounts/login.html'
next_page = 'index'
class Logout(LogoutView):
next_page = 'login'
上記のソースコードから分かるように、ログイン用のビューは LoginView
を、ログアウト用のビューは LogoutView
をそれぞれ継承することで簡単に定義することができます。これらの LoginView
や LogoutView
の詳細を知りたい方は下記ページを参照してください。
また、ユーザー登録は「データベースへのレコードの新規登録」の一種なので、ユーザー登録用のビューは上記のように CreateView
を継承することで簡単に定義可能です。この CreateView
に関しては下記ページで解説していますので、詳細に関しては下記ページを参照してください。
これらの LoginView
・LogoutView
・CreateView
は Viewのサブクラス
で、Django では様々な Viewのサブクラス
が定義されており、それを継承することで様々な用途のクラスベースビューが簡単に定義可能です。クラスベースビューに関しては下記ページで解説していますので、クラスベースビューについて詳しく知りたい方は下記ページを参照していただければと思います。
テンプレートファイルの作成
続いてテンプレートファイルを作成していきたいと思います。先ほど変更した accounts/views.py
では、次の2つのテンプレートファイルを利用するようになっています。そのため、下記の2つのファイルを作成していきます。
accounts/templates/accounts/signup.html
accounts/templates/accounts/login.html
また、ログアウトを、前編で作成した「ゲームプレイ用ページ」から実施できるように、このページの基になる下記のテンプレートファイルにログアウトボタンを追加していきます。
game/templates/game/login.html
accounts/templates/accounts/signup.html
最初に、accounts
アプリ向けのテンプレートファイルを作成していきます。
accounts
アプリ向けのテンプレートファイルは、accounts/templates/accounts/
以下に設置する必要があります。ただし、startapp
コマンドでは templates
フォルダ以下が作成されません。そのため、まずは templates
フォルダを作成し、さらに templates
フォルダの中に accounts
フォルダを作成してください。
そして、ここで作成した accounts
フォルダの中に signup.html
を新規作成し、中身を下記のように変更してください。このファイルは、ユーザー登録フォームを表示するテンプレートファイルとなります。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ユーザー登録</title>
</head>
<body>
<h1>ユーザー登録</h1>
<form method="post" action="{% url 'signup' %}">
{% csrf_token %}
<table><tbody>{{ form.as_table }}</tbody></table>
<button type="submit">登録</button>
</form>
</body>
</html>
このテンプレートファイルはビューの Signup
から利用されるもので、Signup
では form_class = SignupForm
を定義しているため、コンテキストの form
キーで SignupForm
(ユーザー登録フォーム) のインスタンスを受け取ることになります。なので、上記のようにテンプレートファイルを作成することで、Signup
でページ表示が行われる際にはユーザー登録フォームが表示されることになります。
また、このフォームのボタンがクリックされた際には 'signup'
という名前が設定された URL に対し、メソッドが POST
の HTTP リクエストが送信されることになります(URL への名前の設定は、後述で作成する accounts/urls.py
で実施します)。
accounts/templates/accounts/login.html
続いて、ログインフォームを表示するテンプレートファイルを作成していきます。
先ほど作成した accounts
フォルダの中に login.html
を新規作成して中身を下記のように変更してください
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ログイン</title>
</head>
<body>
<h1>ログイン</h1>
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<table><tbody>{{ form.as_table }}</tbody></table>
<button type="submit">ログイン</button>
</form>
</body>
</html>
文言やボタンクリック時に送信される HTTP リクエストの URL の名前等は異なりますが、基本的な作りは signup.html
と同様になります。ただし、このテンプレートファイルを利用するビューの Login
では form_class = LoginForm
を定義しているため、Login
でページ表示が行われる際にはログインフォームが表示されることになります。
game/templates/game/guess.html
ログアウトも実施できるよう、ログアウトボタンも設置していきたいと思います。今回は、ゲームをプレイするページにログアウトボタンを設置したいので、前編で作成した game
アプリの guess.html
(game/templates/game/guess.html
) を変更していきます。
具体的には、下記のように guess.html
を変更します。<body>
セクションの最後に、ログアウトボタン用の <form>
を追加しています(右端にログアウトボタンが表示されるようにしています)。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>数当てゲーム</title>
</head>
<body>
<h1>数当てゲーム</h1>
{% if not game.is_finished %}
<h2>{{ game.attempts|add:1 }}回目の予想</h2>
<form method="post" action="{% url 'index' %}">
{% csrf_token %}
<table>{{ form.as_table }}</table>
<button type="submit">回答</button>
</form>
{% else %}
<p>正解です!!!</p>
<p>もう一度プレイする場合は<a href="{% url 'index' %}">ココ</a>をクリックしてください</p>
{% endif %}
{% if guess is not None %}
<h2>前回の予想結果</h2>
<table>
<thead>
<tr>
<th>予想回数</th><th>予想した数字</th><th>ヒット数</th><th>ブロウ数</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ guess.attempts }}</td>
<td>{{ guess.number }}</td>
<td>{{ guess.hit }}</td>
<td>{{ guess.blow }}</td>
</tr>
</tbody>
</table>
{% endif %}
<form method="post" action="{% url 'logout' %}" style="text-align: right">
{% csrf_token %}
<button type="submit">ログアウト</button>
</form>
</body>
</html>
ポイントは2つで、まず、LogoutView
ではメソッドが POST
の HTTP リクエストを受け取った時にログアウトが実施されるようになっているため、ログアウトボタンクリック時にはメソッドが POST
の HTTP リクエストが送信されるよう、<form>
に属性 method="post"
を指定する必要があります。また、メソッドが POST
の HTTP リクエストを受け取った時にはウェブアプリで CSRF 検証が行われるため、{% csrf_token %}
の記述も必要となります。入力フィールドがいらないのでフォームクラスの定義は不要ですが、LogoutView
を継承するビューでログアウトを実現するためには上記の2点のポイントには注意が必要となります。
ログイン関連の URL とビューのマッピング
あとは、accounts/urls.py
を作成し、ログイン関連の URL とビューとのマッピングを行います。
accounts/
フォルダ内に urls.py
を新規作成し、中身を下記のように変更してください。
from django.urls import path
from . import views
urlpatterns = [
path('signup/', views.Signup.as_view(), name='signup'),
path('login/', views.Login.as_view(), name='login'),
path('logout/', views.Logout.as_view(), name='logout'),
]
この accounts/urls.py
での設定は、URL が /accounts/
から始まる時に参照されることになるため、下記の URL を受け取った時にそれぞれのビューが動作することになります。
/accounts/signup/
:Signup
/accounts/login/
:Login
/accounts/logout/
:Logout
また、上記の各 path
関数の name
引数によって URL の名前の設定を行っています。これにより、ビューでのリダイレクト先、フォームからの HTTP リクエスト送信先に指定した “名前” 部分が URL に変換されるようになり、適切な URL へのリダイレクト・HTTP リクエストの送信が行われるようになったことになります。
ということで、以上の変更により、数当てゲームのウェブアプリでユーザー登録・ログイン・ログアウトが実施できるようになったことになります。
スポンサーリンク
非ログインユーザーのアクセス制限
ログイン機能を搭載すれば、ウェブアプリで非ログインユーザーからのアクセスを制限することができるようになります。今回は、数当てゲームを非ログインユーザーからはプレイできないようにするため、数当てゲームのページ(回答フォームを表示するページ)で非ログインユーザーからのアクセスを制限するようにしたいと思います。
この非ログインユーザーからのアクセス制限は、関数ベースビューの場合であれば、@login_required
のデコレーターをビューに適用することで実現できます。
今回の場合は、数当てゲームのページにアクセス制限をかけるため、下記のように、このページのビューである game/views.py
の index
関数に @login_required
を適用してやればよいことになります。
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from .models import Game
from .forms import GuessForm
import random
# 略
@login_required
def index(request):
# 略
ユーザーの識別
ここまでの変更により、まずウェブアプリにログイン機能が搭載され、さらにログインしないと数当てゲームがプレイできなくなりました。つまり、数当てゲームは、必ずログイン中のユーザーからプレイされることになります。
そして、ウェブアプリを利用するユーザーがログイン中である場合、そのユーザーは、関数ベースビューの引数 request
(第1引数)を利用して request.user
から取得可能です。この request.user
は User
のインスタンスとなります。
したがって、前編ではセッション ID で利用者を識別するようにしていましたが、ここまでの変更により、セッション ID ではなくユーザーで利用者を識別することが可能になったことになります。また、ゲームとユーザーとを紐づけて管理しておくことで、ユーザー単位でゲームを管理することができるようになり、各ユーザーが独立してゲームをプレイすることも可能となります。
このような、ユーザーの識別やゲームの管理を導入するために、game
アプリ側の変更を実施していきたいと思います。
Game
の変更
まず、game/models.py
を開いて下記のように変更してください。もともと Game
にはセッション ID を管理するための session_id
フィールドを定義していたのですが、それを削除し、代わりに user
フィールドを定義しています。これにより、ゲームがセッション ID ではなくユーザーと関連付けられて管理できるようになります。
from django.db import models
from django.contrib.auth.models import User
class Game(models.Model):
answer = models.CharField(max_length=4) # 正解
attempts = models.IntegerField(default=0) # 予想回数
is_finished = models.BooleanField(default=False) # 終了したゲーム
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True) # ユーザー
上記のように Game
に user
フィールドを定義すれば、Game
のインスタンスは ForeignKey
の第1引数に指定したモデルクラスのインスタンス、すなわち User
のインスタンスと関連付けを行うことができるようになります。このようなインスタンス同士の関連付けを「リレーション」と呼びます。リレーションに関しては下記ページで詳細を解説していますので、詳しくは下記ページを参照していただければと思います。
セッション ID のように、特定の1つのデータとモデルクラスを紐づける場合は単にフィールドを定義すればよいだけなのですが、ユーザーのように複数のデータから構成されるモデルクラス(ユーザー名やパスワード)との紐づけを行うためにはリレーションを利用する必要があります。
また、上記のように、リレーションフィールドとして ForeignKey
を利用した場合、User
のインスタンスと Game
のインスタンスの間に1対多の関連付けが可能となります。つまり、一人のユーザーに対して複数のゲームを関連付けることができます。一人のユーザーが同時にプレイするゲームは1つのみではあるのですが、クリア済みのゲームの情報も管理できるようにするため、一人のユーザーに対して複数のゲームを関連付けられるようにしています。この、クリア済みのゲームの情報は、次に実現するランキング機能で利用します。
また、Game
では is_finished
フィールドでゲームクリア済か否かを管理するようになっていますので、現在ユーザーがプレイ中のゲームは、そのユーザーに関連付けられた Game
のインスタンスの内、is_finished=False
を満たすものということになります。
game/views.py
の変更
リレーションを利用して2つのインスタンス同士を関連付けた場合、一方のインスタンスから、そのインスタンスに関連付けられた他方のインスタンスを取得することが可能です。
これを利用し、ビューにおける Game
を取得する処理を、ウェブアプリ利用中のユーザーに関連付けられた Game
を取得するように変更していきます。また、今までは Game
のレコードを新規登録する際には session_id
フィールドにセッション ID をセットするようにしていましたが、user
フィールドにウェブアプリ利用中のユーザーをセットするように変更し、ゲームをユーザーに関連付けて管理できるようにしていきます。
前述でも説明したように、関数ベースビューにおいては、ウェブアプリ利用中のユーザーを request.user
から取得することが可能ですので、game/views.py
を下記のように変更することで、上記で示したことを実現することができるようになります。
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from .models import Game
from .forms import GuessForm
import random
# 略
@login_required
def index(request):
# ユーザーに関連付けられたゲームの管理情報を取得
game = request.user.game_set.filter(is_finished=False).first()
if game is None:
# Gameを作成(ユーザーの関連付け)
game = Game.objects.create(
answer=create_answer(), # 正解データ
user=request.user # ユーザー
)
guess = None
if request.method == 'POST':
# フォームからデータが送信されてきた場合
# 受信したデータからフォームを作成
form = GuessForm(request.POST)
if form.is_valid():
# 受信したデータが妥当である場合
# 予想回数をインクリメント
game.attempts += 1
# ユーザーが予想した数を取得
number = form.cleaned_data['number']
# Guessクラスのインスタンスを生成
guess = Guess()
guess.number = number
guess.game = game
guess.attempts = game.attempts
if guess.hit == 4:
# 予想した数が正解の場合
# ゲーム終了フラグをセット
game.is_finished = True
# レコードを更新
game.save()
else:
# フォームからデータが送信されてきていない場合
# 空のフォームを生成
form = GuessForm()
context = {
'game': game,
'guess': guess,
'form': form
}
return render(request, 'game/guess.html', context)
前述で示した models.py
のように Game
に user
フィールドを追加した場合、1つの User
のインスタンスに対して複数の Game
のインスタンスが関連付けできるようになります。そして、その User
のインスタンスに関連付けされた Game
のインスタンスは、User
のインスタンスのデータ属性 game_set
にメソッドを実行させることで取得することが可能です。
例えば all
メソッドを実行すれば、そのメソッドを実行したインスタンスに関連付けられた Game
のインスタンスが全て取得できますし、filter
メソッドを取得すれば、関連付けられた Game
のインスタンスのうち、特定の条件を満たすインスタンスのみが取得できます。
index
関数では、下記のように filter
メソッドを実行させることで is_finished=False
を満たす Game
のインスタンスの取得を実施しています。ゲームクリアしたタイミングで is_finished
に True
をセットするようにしているため、基本的には is_finished=False
を満たすインスタンスは多くても1つということになりますが、念のため、複数のインスタンスが取得できてしまった時のことを考慮し、first
メソッドの実行で複数のインスタンスの中から先頭のインスタンス1つのみを取得するようにしています。
game = request.user.game_set.filter(is_finished=False).first()
また、ビューでセッション ID を利用することは無くなるため、index
関数でセッション ID を発行するような処理は不要となります。そのため、前編で実装したセッション ID を発行する処理は、上記の index
関数では削除しています。
ここまでの説明のように、ログイン機能を搭載すれば、request.user
からウェブアプリを利用しているユーザーを取得することができ、ユーザーに応じた処理やユーザーに応じたページの表示を実現することができるようになります。こういったユーザーに応じた動作をウェブアプリで実現することも多いため、この方法についてはしっかり理解しておきましょう!
ランキング表示機能
続いては、ウェブアプリを変更してランキング表示機能を実現していきたいと思います。
特にゲーム関連のウェブアプリではランキング表示機能を備えているものが多いですよね!
こういったランキング表示機能も Django を利用すれば簡単に実現することができます。今回は「(正解までに要した)試行回数の平均値」でのユーザーのランキングの表示を実現していきたいと思います。
スポンサーリンク
ランキング表示機能の実現方法
このランキング表示は、各ユーザーに対する「(正解までに要した)試行回数の平均値」を算出し、この平均値に対してユーザーを昇順にソートして出力することで実現することができます。
モデルクラスの Game
では attempts
フィールドが定義されており、このフィールドで、そのゲームに対する試行回数が管理されるようになっています。さらに、 is_finished
フィールドで、そのゲームが正解済み(クリア済み)であるかどうかが判断できるようになっています。そのため、各ユーザーに関連付けられた is_finished=True
を満たす Game
のインスタンスを取得し、その取得結果に対して下記を計算すれば、各ユーザーの「試行回数の平均値」を求めることができます。
- 全てのインスタンスの
attempts
フィールドの和を求める - その和をインスタンスの総数で割る
つまり、既に「試行回数の平均値」を求めるために必要な情報はデータベースの Game
のテーブルに保存されるようになっており、ユーザーに関連付けられた Game
のインスタンス、すなわちユーザーがプレイしたゲームは、前述の通りビューで request.user
から取得可能です。なので、あとは「試行回数の平均値」の算出やソートを実施し、その結果をテンプレートファイルに埋め込むようにすれば、ランキングの表示を実現することができることになります。
ランキング表示のビュー
まずは、ランキング表示用のビューを定義していきます。今回は、game/views.py
にランキング表示用のビューを ranking
関数として定義していきたいと思います。
具体的には、とりあえず下記のように ranking
関数を定義すれば、試行回数の平均値に対して昇順にソートされた「User
のインスタンスと試行回数の平均値を値とする辞書のリスト」が作成でき、それを要素とするコンテキストをテンプレートファイル ranking.html
から利用できるようになります。
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from .models import Game
from .forms import GuessForm
import random
# 略
@login_required
def ranking(request):
# 全ユーザーを取得
users = User.objects.all()
rankings = []
for user in users:
# ユーザーがクリアしたゲームを取得
games = user.game_set.filter(is_finished=True)
# ユーザーがクリアしたゲームの個数
num_game = len(games)
sum_attempts = 0
if num_game > 0:
# 試行回数の総和を計算
for game in games:
sum_attempts += game.attempts
# 試行回数の平均を計算
average_attempts = sum_attempts / num_game
# リストにユーザーと平均試行回数を値とする辞書を追加
rankings.append(
{
'user': user,
'average_attempts': average_attempts
}
)
# averageキーに対してリスト内の要素をソート
sorted_rankings = sorted(rankings, key=lambda x: x['average_attempts'])
context = {
'rankings': sorted_rankings
}
return render(request, 'game/ranking.html', context)
ただ、上記のビューでランキング表示を行うことは可能なのですが、上記の ranking
関数では N + 1 問題が発生してしまってランキングの表示が遅くなる可能性が高いです。N + 1 問題とは、レコード数(インスタンス数)に比例してデータベースへのクエリの発行が増加してしまうことを言います。ranking
関数では、User
のレコード数に比例して Game
のレコードを取得するクエリの発行数が増加してしまうことになります。そのため、登録されているユーザー数が増えると、一気にランキング表示が重くなることになります。
この N + 1 問題は、今回の場合であれば prefetch_related
メソッドを利用することで解決することができます。N + 1 問題の詳細や、その解決方法については下記ページで解説しているので、詳しく知りたい方は下記ページを参考にしてください。
また、ranking
関数では for
ループで試行回数の平均を求めていますが、これも少し処理効率が悪いです。詳細な説明は省略しますが、Django には平均値や総和等の集計を行うための関数が用意されており、これを利用する方が処理効率が向上します。今回は平均値の集計を行うため、Avg
関数を利用することで処理効率を向上させることができます。
そのため、上記の ranking
関数でもランキング表示を実現することはできるのですが、処理効率向上のために下記のように変更したいと思います。
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.db.models import Avg, Prefetch
from .models import Game
from .forms import GuessForm
import random
# 略
@login_required
def ranking(request):
# 全ユーザーとクリア済みのゲームを取得
users = User.objects.prefetch_related(
Prefetch('game_set', Game.objects.filter(is_finished=True))
)
rankings = []
for user in users:
# ユーザーに関連付けられたゲームを取得
games = user.game_set.all()
# ユーザーがクリアしたゲームの個数
num_game = len(games)
if num_game > 0:
# 試行回数の平均を計算
average_attempts = games.aggregate(Avg('attempts'))['attempts__avg']
# リストにユーザーと平均試行回数を値とする辞書を追加
rankings.append(
{
'user': user,
'average_attempts': average_attempts
}
)
# averageキーに対してリスト内の要素をソート
sorted_rankings = sorted(rankings, key=lambda x: x['average_attempts'])
context = {
'rankings': sorted_rankings
}
return render(request, 'game/ranking.html', context)
これでもランキング表示が遅いという場合は、User
に平均試行回数を管理するフィールドを追加し、そのフィールドをゲームクリアのタイミングで更新するようにしてやるのも手だと思います。
ランキング表示のテンプレート
続いて、ランキング表示を行うテンプレートファイルを作成していきます。
先ほど作成したビューによって、User
のインスタンスと平均試行回数とを値とする辞書のリストがコンテキストとして渡されることになりますので、for
ループでリスト内の各要素の必要な情報を出力してやればランキング表示を実現することができることになります。
また、ranking
関数からは game/ranking.html
のテンプレートファイルが利用されるようになっているため、game/templates/game/
内に ranking.html
という名前のテンプレートファイルを作成していくことになります。
ということで、game/templates/game/
内に ranking.html
を新規作成し、中身を下記のように変更してください。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ランキング</title>
</head>
<body>
<h1>ランキング</h1>
<table>
<thead>
<tr>
<th>ユーザー</th>
<th>平均試行回数</th>
</tr>
</thead>
<tbody>
{% for ranking in rankings %}
<tr>
<td>{{ ranking.user.username }}</td>
<td>{{ ranking.average_attempts }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</body>
</html>
スポンサーリンク
ランキング表示用 URL の設定
あとは、下記のように game/urls.py
を編集し、ランキング表示用の URL の設定、および、その URL と ranking
関数とのマッピングを行えばランキング表示機能が完成することになります。
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('ranking/', views.ranking, name='ranking')
]
これにより、下記 URL にアクセスしたときにランキングが表示されるようになります。下記 URL にアクセスしたときに実行される ranking
関数にも @login_required
を適用しているため、ログインしないとランキングが表示されないので注意してください。
/game/ranking/
以上で、数当てゲームのランキング表示が完成したことになります。
特にビューのコードが難しくなってしまいましたが、このランキング表示の実装を通じて感じていただきたいのは「データベースに情報さえ保存しておけば後からそれを加工して新たな機能をウェブアプリに追加可能である」という点になります。このランキング表示に関しても、Game
で管理していた試行回数(attempts
)を加工し、さらにソートして見せ方を工夫することで追加できた機能となります。
ゲームなどをウェブアプリで開発する場合、データベースへの情報の保存無しに実現できるようなものも多いですが、あえてデータベースに情報を保存するようにすることで、後からの機能の追加が容易に実現できるようになります。この点は、開発するウェブアプリを発展させていくためにも重要なポイントになりますので、是非覚えておいてください。
また、ウェブアプリではランキング機能を搭載することも多いと思いますので、是非ランキング機能の実現方法についても理解しておきましょう!
回答履歴の表示機能
ここまでの解説内容により、数当てゲームがプレイでき、さらにログイン機能やランキング表示を備えたウェブアプリを開発できるようになったことになります。
ただ、現状のウェブアプリでは回答履歴が表示されないので、今まで予想した数字や、その数字の回答結果(Hit 数 / Blow 数)を自身で覚えておく必要があってゲームがプレイしにくいです。そのため、最後に回答履歴の表示機能を追加していきたいと思います。この回答履歴は、ゲームをプレイするページの下部分に表示するようにしたいと思います。
回答履歴表示の実現方法
回答履歴を表示するためには、回答履歴をデータベースで管理するようにウェブアプリを変更する必要があります。
逆に言えば、回答履歴の表示を実現するために最低限必要なことは、回答履歴のデータベースでの管理および、管理しているデータの出力くらいのみとなります。なので、割と簡単に回答履歴の表示は実現可能です。
スポンサーリンク
データベースでデータを管理するメリット
先ほどの解説のように、回答履歴をデータベースで管理するようにすることで「回答履歴を表示する」ことができるようになるというメリットが得られます。このように、ウェブアプリでは、データベースで管理するデータを増やすことで機能追加を容易に行うことができるようになります。また、ランキング表示機能がそうであったように、データベースで管理するデータの見せ方を工夫することで新機能が実現できることもあります。
また、ウェブアプリを運用していくときにポイントになるのが、データベースで管理するデータ自体に価値があるかもしれないと言う点になります。例えば「数当てゲームの回答履歴」というデータは人間の思考パターンを研究するのに役立つかもしれません。そのため、このデータを用いて自身で研究することもできますし、もしかしたら第三者が何らかの理由でデータが欲しくてデータを購入してくれるかもしれません。
数当てゲームの場合は、そこまで管理するデータに価値はないかもしれませんが、他のウェブアプリの場合はデータ自体に価値があることもありますので、そういった価値のありそうなデータはデータベースに保存するようにしておくのがよいと思います。そして、それが新たなビジネスを開拓していくことにつながる可能性もあります。
ただし、何でもかんでもデータベースに保存するようにするとデータベースに必要な記憶容量が膨大になってコストが高くなってしまいますし、利用目的を明確に示してユーザーからの利用の許可を得ることや、適切にデータを管理することも重要となるので注意も必要です。ですが、こういったデータ自体に価値があるかもしれないということは頭の片隅にでも置いておくとよいと思います。
回答履歴管理用のモデルクラス
前置きが長くなりましたが、ここから実際に回答履歴の表示機能を開発していきたいと思います。
まず、今までデータベースで管理していなかったデータをデータベースで管理するようにするので、新たにモデルクラス(テーブル)を定義する必要があります。今回は、回答履歴として「回答」「試行回数」「Hit 数」「Blow 数」をデータベースで管理するようにしていきます。また、どのゲームに対する回答履歴であるかを判断できるように Game
のインスタンスと関連付けできるようなモデルクラスを定義していきます。さらに、Hit 数と Blow 数を計算するメソッドも定義したいと思います。
実は、前編で、同様のデータを管理し、さらに同様のメソッドを持つ Guess
クラスを game/views.py
に定義しています。これまではデータベースに回答履歴を管理する必要が無かったので単なるクラスとして定義していましたが、要は、これをモデルクラス(models.Model
のサブクラス)として定義することで、回答履歴をデータベースに保存することができるようになります。
具体的には、game/models.py
を下記のように変更して Guess
をモデルクラスとして定義してやることで、回答履歴をデータベースで管理できるようになります。
from django.db import models
from django.contrib.auth.models import User
class Game(models.Model):
answer = models.CharField(max_length=4) # 正解
attempts = models.IntegerField(default=0) # 予想回数
is_finished = models.BooleanField(default=False) # 終了したゲーム
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True) # ユーザー
class Guess(models.Model):
number = models.CharField(max_length=4) # 回答
attempts = models.IntegerField() # 予想回数
hit = models.IntegerField() # Hit数
blow = models.IntegerField() # Blow数
game = models.ForeignKey(Game, on_delete=models.CASCADE) # ゲーム
def set_hit_blow(self):
# ヒット数(桁まで一致している数字の個数)
self.hit = sum(1 for a, g in zip(self.game.answer, self.number) if a == g)
# ブロウ数(桁は一致していないが正解に含まれている数字の個数)
self.blow = sum(1 for g in self.number if g in self.game.answer) - self.hit
上記のように game
フィールドを定義することで、1つの Game
のインスタンスに複数の Guess
を関連付けて、つまり1つのゲームに対して複数の回答履歴を関連付けて管理することができるようになります。
回答履歴表示のためのビューの変更
続いて、ゲームをプレイするページのビュー、つまり game/views.py
の index
関数を回答履歴が表示できるように変更していきます。
前述の通り、今までは game/views.py
で Guess
クラスを定義していましたが、game/models.py
でモデルクラスとして Guess
を定義したため、game/views.py
の Guess
クラスの定義は削除してしまってよいです。代わりに、game/views.py
に models
からの Guess
の import
を追加します。
また、元々の Guess
と新たに定義したモデルクラス Guess
とではデータ属性(フィールド)が同じなので、index
関数内部で Guess
のインスタンスのデータ属性に値をセットする処理は変更不要となります。ただし、回答の履歴を全て表示できるよう、回答履歴をデータベースに保存していく必要があるため、Guess
のインスタンスに save
メソッドを実行させる処理の追加が必要となります。
あとは、Guess
のインスタンスを全て取得し、それをコンテキストにセットするようにしてやれば、テンプレートファイルに全回答履歴を渡すことができます。そして、それによって、回答履歴を含む HTML を生成することができるようになり、その HTML によってクライアント側で回答履歴が表示されるようになります。
ということで、結論としては game/views.py
を下記のように変更することで、回答履歴を表示するビューが実現できることになります。
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.db.models import Avg, Prefetch
from .models import Game, Guess
from .forms import GuessForm
import random
def create_answer():
# ランダムな4桁の数字(重複無し)の文字列を生成
return ''.join(map(str, random.sample(range(10), 4)))
@login_required
def index(request):
# ユーザーに関連付けられたゲームの管理情報を取得
game = request.user.game_set.filter(is_finished=False).first()
if game is None:
# Gameを作成(ユーザーの関連付け)
game = Game.objects.create(
answer=create_answer(), # 正解データ
user=request.user # ユーザー
)
guess = None
if request.method == 'POST':
# フォームからデータが送信されてきた場合
# 受信したデータからフォームを作成
form = GuessForm(request.POST)
if form.is_valid():
# 受信したデータが妥当である場合
# 予想回数をインクリメント
game.attempts += 1
# ユーザーが予想した数を取得
number = form.cleaned_data['number']
# Guessクラスのインスタンスを生成
guess = Guess()
guess.number = number
guess.game = game
guess.attempts = game.attempts
# Hit数とBlow数を計算
guess.set_hit_blow()
# 回答履歴を保存
guess.save()
if guess.hit == 4:
# 予想した数が正解の場合
# ゲーム終了フラグをセット
game.is_finished = True
# レコードを更新
game.save()
else:
# フォームからデータが送信されてきていない場合
# 空のフォームを生成
form = GuessForm()
# gameに対する回答履歴を取得
guesses = game.guess_set.all()
context = {
'game': game,
'guesses': guesses,
'form': form
}
return render(request, 'game/guess.html', context)
@login_required
def ranking(request):
# 全ユーザーとクリア済みのゲームを取得
users = User.objects.prefetch_related(
Prefetch('game_set', Game.objects.filter(is_finished=True))
)
rankings = []
for user in users:
# ユーザーに関連付けられたゲームを取得
games = user.game_set.all()
# ユーザーがクリアしたゲームの個数
num_game = len(games)
if num_game > 0:
# 試行回数の平均を計算
average_attempts = games.aggregate(Avg('attempts'))['attempts__avg']
# リストにユーザーと平均試行回数を値とする辞書を追加
rankings.append(
{
'user': user,
'average_attempts': average_attempts
}
)
# averageキーに対してリスト内の要素をソート
sorted_rankings = sorted(rankings, key=lambda x: x['average_attempts'])
context = {
'rankings': sorted_rankings
}
return render(request, 'game/ranking.html', context)
一点補足しておくと、上記の index
関数の場合、正解を回答したとしても、つまりゲームをクリアしたとしても回答履歴はデータベースに残り続けることになります。回答履歴を残し続けているのは、ユーザーの回答履歴を価値のあるデータと考え、今後ユーザーの回答履歴を利用した分析や研究を行うこともできるようにするためになります。ですが、不要なデータと考えるのであればゲームクリア時に回答履歴を全て削除しても問題ないです。削除してしまった方がデータベースに必要な記憶容量が節約することができます。
スポンサーリンク
回答履歴表示のためのテンプレートの変更
あとは、guess.html
(テンプレートファイル) を変更してやれば、回答履歴の表示機能が完成します。
今までは、guess.html
では単純に Guess
のインスタンスを1つだけコンテキストとして受け取り、そのインスタンスの情報を出力するようになっていました。ですが、先ほどの index
関数の変更によって Guess
のインスタンスの “リスト” を受け取るようになったため、guess.html
においても、テンプレートタグを利用してリストに対して for
ループを組み、そのループの中で Guess
のインスタンスの情報を1つずつ出力するように変更する必要があります。
具体的には、下記のように guess.html
を変更することで、コンテキストとして受け取った全回答履歴を表示することができるようになります。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>数当てゲーム</title>
</head>
<body>
<h1>数当てゲーム</h1>
{% if not game.is_finished %}
<h2>{{ game.attempts|add:1 }}回目の予想</h2>
<form method="post" action="{% url 'index' %}">
{% csrf_token %}
<table><tbody>{{ form.as_table }}</tbody></table>
<button type="submit">回答</button>
</form>
{% else %}
<p>正解です!!!</p>
<p>もう一度プレイする場合は<a href="{% url 'index' %}">ココ</a>をクリックしてください</p>
{% endif %}
{% if guesses|length != 0 %}
<h2>回答履歴</h2>
<table>
<thead>
<tr>
<th>試行回数</th><th>あなたの予想</th><th>ヒット数</th><th>ブロウ数</th>
</tr>
</thead>
<tbody>
{% for guess in guesses %}
<tr>
<td>{{ guess.attempts }}</td>
<td>{{ guess.number }}</td>
<td>{{ guess.hit }}</td>
<td>{{ guess.blow }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<form method="post" action="{% url 'logout' %}" style="text-align: right">
{% csrf_token %}
<button type="submit">ログアウト</button>
</form>
</body>
</html>
動作確認
最後に、ここまで数当てゲームのウェブアプリを拡張して追加した「ログイン機能」「ランキング表示機能」「回答履歴表示機能」の動作確認を実施していきたいと思います。
動作確認のための前準備
まずは、動作確認を行うための前準備を実施していきます。
マイグレーションの実施
新たにモデルクラスの定義を追加し、さらに元々定義していたモデルクラスの変更も行いましたのでマイグレーションが必要となります。
そのため、number_guess
フォルダ内(manage.py
が存在するフォルダ内)で下記の2つのコマンドを実行してマイグレーションを実施してください。
% python manage.py makemigrations
% python manage.py migrate
おそらく、エラー等発生せずにコマンドが完了すると思いますが、もしエラーが発生したのであれば下記を削除してから再度コマンドを実行してみてください。
game/migrations/
フォルダ内の “__init__.py
以外” のファイルdb.sqlite3
開発用ウェブサーバーの起動
続いて、同じフォルダ内で下記コマンドを実行して開発用ウェブサーバーを起動してください。これで動作確認の前準備は完了です。
% python manage.py runserver
スポンサーリンク
ログイン関連・回答履歴表示の動作確認
では、ウェブアプリの動作確認を実施していきます。最初に、ログイン関連と回答履歴表示の動作確認を実施していきます。
最初に、非ログインユーザーが「数当てゲームをプレイ不可」であることを確認したいと思います。
まず、ウェブブラウザを起動し、下記の URL を開いてください。この URL は、数当てゲームのプレイ用ページの URL となります。
http://localhost:8000/game/
実際に URL を開いてみると分かると思いますが、この URL を開いてもゲームのプレイ用ページは表示されず、ログインフォームが表示されるはずです。これは、ゲームのプレイ用ページにはアクセス制限がかかっており、非ログインユーザーからはアクセスできないようになっているためです。このように、ログイン機能を搭載することで、非ログインユーザーからのアクセスを拒否することが可能となります。
次は、ログインを実施することで先ほど示した URL にアクセスすることができるようになることを確認していきます。ログインを行うためにはユーザー登録が必要となりますので、まずはウェブブラウザで下記の URL を開いてください。
http://localhost:8000/accounts/signup/
この URL を開くと下図のようなページが表示されますので、ここでユーザー名・メールアドレス・パスワード(確認用も含めて2つ)を入力して 登録
ボタンをクリックしてください。
ユーザーの作成に成功した場合は下図で示すログインフォームが表示されるはずです。ここで、先ほど入力したものと同じユーザー名とパスワードを入力し、さらに ログイン
ボタンをクリックしてください。
ログインに成功した場合は、下図で示す数当てゲームのプレイ用ページが表示されるはずです。ここで、数当てゲームをプレイすることができることが確認できるはずです。
ウェブブラウザのアドレスバーを確認していただければ分かるように、このページは非ログイン時にはアクセスできなかった下記 URL のページとなります。その URL にアクセスできるようになったのはログインを実施したからになります。
http://localhost:8000/game/
このように、ログイン機能を搭載することで、ログイン中のユーザーのみが利用可能なウェブアプリを簡単に実現することができます。非ログインユーザーからのアクセスを拒否するようなウェブアプリを開発することも多いので、この実現方法についてもしっかり覚えておきましょう!
また、フォームに4桁の半角数字を入力して 回答
ボタンをクリックすれば、その入力した数字に対する Hit 数 / Blow 数がフォームの下側の回答履歴欄に表示されることも確認できると思います。また、何回も回答を行った場合に、それまでに回答した全ての履歴が回答履歴欄に表示されることが確認できると思います。
この、回答履歴が全て表示されるようになったという点が、今回追加した回答履歴の表示機能の効果となります。
また、PC に複数のウェブブラウザがインストールされているのであれば、もう1つのウェブブラウザを起動し、ここまでと同様の手順でログインを実施してみてください。ユーザーの登録は不要で、ログインは、先ほど登録したユーザーのユーザー名とパスワードを入力して実施してください。ログインに成功すると、元々ゲームをプレイしていたウェブブラウザに表示される回答履歴と同じものが新しく開いたウェブブラウザにも表示されることが確認できるはずです。
このように、異なるウェブブラウザからウェブアプリを利用したとしても、同じユーザーでログインすれば今までプレイしていたゲームを継続してプレイすることが可能です。これは、セッション ID でユーザーを識別していたときには実現できなかったことであり、ログインを搭載して真の意味でのユーザーの識別が行われるようになったことで実現できるようになった動作となります。
続いて、ログアウトの動作確認を行います。現在表示されているゲームプレイ用のページには ログアウト
ボタンが設置されているはずなので、その ログアウト
ボタンをクリックしてみてください。すると、下図のようにログインフォームが表示されるはずです。
ここで、再度ウェブブラウザで下記 URL を開いてみてください。今までであれば、下記 URL を開くとゲームプレイ用のページが表示されていたのですが、今回はゲームプレイ用のページが表示されず、ログインフォームが表示され続けることになると思います。これは、前述の通り、ゲームプレイ用のページが非ログインユーザーからアクセス不可であるからで、この動作より、ログアウト
ボタンのクリックによって正常にログアウトが動作したことが確認できたことになります。
http://localhost:8000/game/
ランキング表示の動作確認
最後に、ランキングの表示を確認していきます。
ランキングの表示を確認するためには、ユーザーを複数登録し、それぞれのユーザーで少なくとも一回以上ゲームをクリアする必要があります。複数ユーザーでゲームをクリアした後に、いずれかのユーザーでログインした状態で下記 URL を開いてみてください。
http://localhost:8000/game/ranking/
URL を開いたときに、下図のようなランキングが表示されていればランキング表示の動作確認も OK となります。
ポイントは、平均試行回数に対して昇順にソートされてユーザー名が列挙されている点で、このようにソートを利用することでランキングの表示も簡単に実現することができます。
特にゲーム関連のウェブアプリではランキング表示を行うことも多いと思いますので、ランキング表示方法についてもしっかり覚えておきましょう!また、上記のランキング表示は、元々データベースで管理していたデータを加工して実現した機能になります。こういったデータの加工や、データの見せ方の工夫によって新機能を実現することができることも多いですので、このことについても是非覚えておいてください。
まとめ
このページでは、数当てゲームの拡張について解説しました!
具体的には、下記の3つの機能の追加について解説しました。
- ログイン機能
- ランキング表示機能
- 回答履歴の表示機能
最初の「ログイン機能」に関してはウェブアプリに搭載することも多いため、この実現方法についてはしっかり理解しておくことをオススメします。さらに、ログイン機能の搭載によって非ログインユーザーのアクセス制限が可能であるという点や、ユーザーの識別が可能となり、ユーザーに応じたレコードの取得や表示等が実現可能となる点も覚えておきましょう!
また、「ランキング表示機能」「回答履歴の表示機能」の解説からも分かるように、ウェブアプリではデータベースへのデータの保存が非常に重要です。ウェブアプリで実現可能な機能はデータベースに保存するデータによって決まると言っても過言ではありません。データベースに保存されるデータを加工したり、見せ方を工夫することで新たな機能が生まれたり、さらに機能追加のためにデータベースに保存するデータを追加するようなことも必要となります。このデータベースへのデータの保存の重要性についても、このページの解説を通じて実感していただければと思います!
とりあえず、今回は先ほど挙げた3つの機能を追加しましたが、実際に使ってみると使用感や見た目に不満を持つ方も多いと思います。こういった方は、是非自身で不満点の改善に挑戦してみてください。こういった改善を、自身でこだわりを持ちながら取り組むことは知識・技術の向上に繋がりやすいですし、割と楽しくプログラミングに取り組むこともできると思います。今回開発した数当てゲームに限らず、いろんなゲームの改善や拡張に挑戦してみてください!