【Python/Django】数当てゲーム(Hit&Blow)を拡張【後編】

Djangoでの数当てゲームの拡張の仕方・ポイントの解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、前回開発した「数当てゲーム」の拡張を行っていきます。

このサイトでは Django での数当てゲームの作り方を前編・後編構成で解説しており、このページは「後編」となります。下記ページの前編では、”最低限、数当てゲームがプレイ可能なウェブアプリ” の作り方についてのみ解説しました。この後編では、前編で開発したウェブアプリの機能拡張の例を示していきます。

Djangoでの数当てゲームの作り方の解説ページアイキャッチ 【Python/Django】数当てゲーム(Hit&Blow)を開発【前編】

具体的には、数当てゲームに下記の機能を追加していきます。

  • ログイン機能
  • ランキング表示機能
  • 回答履歴の表示機能

特に上側2つの機能は、ウェブアプリでゲームを開発する時に搭載する機会の多い機能になりますので、これらの実現方法・実装方法を理解しておけば、今後のウェブアプリ開発で役に立つと思います。

また、これらの機能は、前編で開発したウェブアプリに変更を加えていくことで追加していきます。そのため、下記の前編の解説ページを読まれていない方は、先に下記ページを読んでいただくことをオススメします。

Djangoでの数当てゲームの作り方の解説ページアイキャッチ 【Python/Django】数当てゲーム(Hit&Blow)を開発【前編】

ログイン機能

まず、ログイン機能を実現していきます。

ログイン機能の必要性

前編では、各クライアントから独立してゲームがプレイできるように、クライアントから送信されてくる「セッション ID」を利用してクライアントの識別を行うようにしてきました。

ただし、あくまでもセッションで識別可能なのは「クライアント」です。つまり、ユーザーが利用しているウェブブラウザやデバイス(PC やスマホ等)を識別することは可能なのですが、ユーザー自体は識別できません。なので、同じユーザーがウェブアプリを利用したとしても、異なるウェブブラウザから利用すると異なるユーザーと判断されることになります。

異なるウェブブラウザ・デバイスを利用した場合に異なるユーザーと判断される様子

また、セッションの有効期限が切れたりクライアント側でセッションが削除されたりすると、同じクライアントからウェブアプリを利用したとしてもウェブアプリで異なるクライアントと判断されることになるため、このような場合はゲームを継続してプレイすることができません。

セッションIDが削除されることで、今までと異なるユーザーと判断される様子

セッションを利用したクライアントの識別は実現が簡単ではあるのですが、この識別には上記のような問題があります。

このような問題は、ウェブアプリにログイン機能を搭載することで解決可能です。まず、ログイン機能を搭載した場合、ログイン時に入力した情報に基づいてウェブアプリを利用中のユーザーが特定されることになるため、ログイン時に同じ情報を入力すれば、異なるデバイス・異なるウェブブラウザを利用していても同じユーザーとして識別されることになります。また、セッションが削除されたとしても再度同じ情報を入力してログインを行えば同じユーザーとして識別されることになるため、今までと同じゲームを継続してプレイすることができます。

異なるウェブブラウザ・デバイスを利用した場合でもログインすれば同じユーザーと判断される様子

このように、セッションでのユーザーの識別の問題はログイン機能によって解決可能です。

スポンサーリンク

ログイン機能の実現

ということで、ここからはログイン機能の実現方法について解説していきます。

ログイン機能を実現するために、まず必須となるのがログインフォームとなります。このログインフォームにユーザー名やパスワード等をユーザーに入力してもらうことで、ログインが実施されることになります。

ログインフォームの説明図

ただし、ログインフォームさえ導入すればログインが実現できるというわけではありません。ざっと挙げると、ログインを実現し、さらにログイン機能を意味のあるものとするためには下記のようなものも必要となります。

  1. ユーザー登録フォーム
  2. ログインフォーム
  3. ログアウトボタン(ログアウトフォーム)
  4. 非ログインユーザーのアクセス制限
  5. ユーザーに応じた処理や表示の切り替え

本来であれば、上記に加えて「カスタムユーザー」の定義も必要となります。カスタムユーザーとは、開発者がカスタマイズした「ユーザー管理モデルクラス」のことになります。ですが、今回は実装を楽にするため、カスタムユーザーの代わりに Django にあらかじめ定義されている User というモデルクラスを利用することにしたいと思います。もし、カスタムユーザーについて詳しく知りたいという方がおられましたら、下記ページを参照していただければと思います。

【Django入門9】カスタムユーザーによるユーザー管理

ということで、ここから上記の 1. 〜 5. を導入することで、数当てゲームにログイン機能を搭載していきます。ただ、1. ~ 5. の詳細に関しては別途下記ページで解説済みですので、ここでは変更内容をサラッと解説するのみとさせていただきたいと思います。詳細を知りたい方は、お手数をおかけしますが下記ページを参照していただければと思います。

ログインの実現方法の解説ページアイキャッチ 【Django入門10】ログイン機能の実現

ログイン用アプリの作成・登録

では、下記の前編のページで開発したウェブアプリを変更し、ログイン機能を実装していきたいと思います。

Djangoでの数当てゲームの作り方の解説ページアイキャッチ 【Python/Django】数当てゲーム(Hit&Blow)を開発【前編】

現状、数当てゲームのウェブアプリは number_guess プロジェクトに game アプリを登録する構成で開発してきています。

今回は、この構成に対し、ログイン及びユーザー管理を専用とするアプリとして accounts を追加し、この accounts のファイルを編集してログイン・ユーザー管理関連の機能を実現していきます。

accountsアプリの位置付けの説明図

ということで、まずは number_guess フォルダ(manage.py が存在するフォルダ)で下記コマンドを実行して number_guess プロジェクトに accounts アプリを作成してください。

% python manage.py startapp accounts

続いて、下記のように number_guess/settings.pyINSTALLED_APPS'accounts', を追加してください。

INSTALLED_APPS
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 は後ほど作成します)。

number_guess/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 がログインフォームとなります。

accounts/forms.py
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 のサブクラスとして定義する必要があります。

【Django入門9】カスタムユーザーによるユーザー管理

さらに、下記ページでも解説しているように、ログインフォームは AuthenticationForm のサブクラスとして定義する必要があります(そのまま AuthenticationForm を利用しても良いです)。

ログインの実現方法の解説ページアイキャッチ 【Django入門10】ログイン機能の実現

詳細は上記の各ページで解説していますが、これらのサブクラスとしてフォームを定義することでパスワードが暗号化されるようになり、セキュリティの高いウェブアプリを実現することができます。

また、上記における User は Django であらかじめ定義されているユーザーを管理するモデルクラスとなります。django.contrib.auth.models から import してやれば、別途モデルクラスを定義することなくウェブアプリでユーザーを管理することができるようになります。

スポンサーリンク

ビューの定義

続いて、ユーザー登録・ログイン・ログアウトを実現するビューを定義していきます。

結論としては、accounts/views.py を下記のように変更することで、ユーザー登録用のビュー(Signup)・ログイン用のビュー(Login)・ログアウト用のビュー(Logout)を定義することができます。また、下記における 'login''index' という文字列は URL の名前になります。'index' については、game/urls.py で設定したゲームプレイ用の URL であり、'login' に関しては後述でログイン用の URL に設定する URL となります。

accoutns/views.py
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 をそれぞれ継承することで簡単に定義することができます。これらの LoginViewLogoutView の詳細を知りたい方は下記ページを参照してください。

LoginViewの使い方の解説ページアイキャッチ 【Django】LoginViewの使い方(クラスベースビューでのログインの実現) LogoutViewの使い方の解説ページアイキャッチ 【Django】LogoutViewの使い方(クラスベースビューでのログアウトの実現)

また、ユーザー登録は「データベースへのレコードの新規登録」の一種なので、ユーザー登録用のビューは上記のように CreateView を継承することで簡単に定義可能です。この CreateView に関しては下記ページで解説していますので、詳細に関しては下記ページを参照してください。

CreateViewでのクラスベースビューの実現方法解説ページアイキャッチ 【Django】CreateViewの使い方(クラスベースビューでの新規登録ページの実現)

これらの LoginViewLogoutViewCreateView は Viewのサブクラス で、Django では様々な Viewのサブクラス が定義されており、それを継承することで様々な用途のクラスベースビューが簡単に定義可能です。クラスベースビューに関しては下記ページで解説していますので、クラスベースビューについて詳しく知りたい方は下記ページを参照していただければと思います。

クラスベースビューの解説ページアイキャッチ 【Django入門15】クラスベースビューの基本

テンプレートファイルの作成

続いてテンプレートファイルを作成していきたいと思います。先ほど変更した 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 を新規作成し、中身を下記のように変更してください。このファイルは、ユーザー登録フォームを表示するテンプレートファイルとなります。 

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 を新規作成して中身を下記のように変更してください

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> を追加しています(右端にログアウトボタンが表示されるようにしています)。

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>{{ 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 を新規作成し、中身を下記のように変更してください。

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.pyindex 関数に @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.userUser のインスタンスとなります。

したがって、前編ではセッション ID で利用者を識別するようにしていましたが、ここまでの変更により、セッション ID ではなくユーザーで利用者を識別することが可能になったことになります。また、ゲームとユーザーとを紐づけて管理しておくことで、ユーザー単位でゲームを管理することができるようになり、各ユーザーが独立してゲームをプレイすることも可能となります。

このような、ユーザーの識別やゲームの管理を導入するために、game アプリ側の変更を実施していきたいと思います。

Game の変更

まず、game/models.py を開いて下記のように変更してください。もともと Game にはセッション ID を管理するための session_id フィールドを定義していたのですが、それを削除し、代わりに user フィールドを定義しています。これにより、ゲームがセッション ID ではなくユーザーと関連付けられて管理できるようになります。

userフィールドの追加
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) # ユーザー

上記のように Gameuser フィールドを定義すれば、Game のインスタンスは ForeignKey の第1引数に指定したモデルクラスのインスタンス、すなわち User のインスタンスと関連付けを行うことができるようになります。このようなインスタンス同士の関連付けを「リレーション」と呼びます。リレーションに関しては下記ページで詳細を解説していますので、詳しくは下記ページを参照していただければと思います。

Djangoのリレーションの解説ページアイキャッチ 【Django入門7】リレーションの基本

セッション ID のように、特定の1つのデータとモデルクラスを紐づける場合は単にフィールドを定義すればよいだけなのですが、ユーザーのように複数のデータから構成されるモデルクラス(ユーザー名やパスワード)との紐づけを行うためにはリレーションを利用する必要があります。

また、上記のように、リレーションフィールドとして ForeignKey を利用した場合、User のインスタンスと Game のインスタンスの間に1対多の関連付けが可能となります。つまり、一人のユーザーに対して複数のゲームを関連付けることができます。一人のユーザーが同時にプレイするゲームは1つのみではあるのですが、クリア済みのゲームの情報も管理できるようにするため、一人のユーザーに対して複数のゲームを関連付けられるようにしています。この、クリア済みのゲームの情報は、次に実現するランキング機能で利用します。

UserとGameとが1対多の関係であることを示す図

また、Game では is_finished フィールドでゲームクリア済か否かを管理するようになっていますので、現在ユーザーがプレイ中のゲームは、そのユーザーに関連付けられた Game のインスタンスの内、is_finished=False を満たすものということになります。

game/views.py の変更

リレーションを利用して2つのインスタンス同士を関連付けた場合、一方のインスタンスから、そのインスタンスに関連付けられた他方のインスタンスを取得することが可能です。

これを利用し、ビューにおける Game を取得する処理を、ウェブアプリ利用中のユーザーに関連付けられた Game を取得するように変更していきます。また、今までは Game のレコードを新規登録する際には session_id フィールドにセッション ID をセットするようにしていましたが、user フィールドにウェブアプリ利用中のユーザーをセットするように変更し、ゲームをユーザーに関連付けて管理できるようにしていきます。

GameのインスタンスをUserのインスタンスと関連付ける様子

前述でも説明したように、関数ベースビューにおいては、ウェブアプリ利用中のユーザーを request.user から取得することが可能ですので、game/views.py を下記のように変更することで、上記で示したことを実現することができるようになります。

game/views.py(User)
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 のように Gameuser フィールドを追加した場合、1つの User のインスタンスに対して複数の Game のインスタンスが関連付けできるようになります。そして、その User のインスタンスに関連付けされた Game のインスタンスは、User のインスタンスのデータ属性 game_set にメソッドを実行させることで取得することが可能です。

user1に関連づけられたGameのインスタンスがuser1.game_setにメソッドを実行させることで取得可能であることを示す図

例えば all メソッドを実行すれば、そのメソッドを実行したインスタンスに関連付けられた Game のインスタンスが全て取得できますし、filter メソッドを取得すれば、関連付けられた Game のインスタンスのうち、特定の条件を満たすインスタンスのみが取得できます。

index 関数では、下記のように filter メソッドを実行させることで is_finished=False を満たす Game のインスタンスの取得を実施しています。ゲームクリアしたタイミングで is_finishedTrue をセットするようにしているため、基本的には 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 から利用できるようになります。

ranking関数
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 問題の詳細や、その解決方法については下記ページで解説しているので、詳しく知りたい方は下記ページを参考にしてください。

DjangoにおけるN+1問題の解説ページアイキャッチ 【Django入門14】N+1問題とselect_related・prefetch_relatedでの解決

また、ranking 関数では for ループで試行回数の平均を求めていますが、これも少し処理効率が悪いです。詳細な説明は省略しますが、Django には平均値や総和等の集計を行うための関数が用意されており、これを利用する方が処理効率が向上します。今回は平均値の集計を行うため、Avg 関数を利用することで処理効率を向上させることができます。

そのため、上記の ranking 関数でもランキング表示を実現することはできるのですが、処理効率向上のために下記のように変更したいと思います。

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 を新規作成し、中身を下記のように変更してください。

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 関数とのマッピングを行えばランキング表示機能が完成することになります。

game/urls.py
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 をモデルクラスとして定義してやることで、回答履歴をデータベースで管理できるようになります。

game/models.py
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つのゲームに対して複数の回答履歴を関連付けて管理することができるようになります。

1つのGameのインスタンスに対して複数のGuessのインスタンスが関連付けられる様子

回答履歴表示のためのビューの変更

続いて、ゲームをプレイするページのビュー、つまり 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 を下記のように変更することで、回答履歴を表示するビューが実現できることになります。

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 を変更することで、コンテキストとして受け取った全回答履歴を表示することができるようになります。

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つの機能を追加しましたが、実際に使ってみると使用感や見た目に不満を持つ方も多いと思います。こういった方は、是非自身で不満点の改善に挑戦してみてください。こういった改善を、自身でこだわりを持ちながら取り組むことは知識・技術の向上に繋がりやすいですし、割と楽しくプログラミングに取り組むこともできると思います。今回開発した数当てゲームに限らず、いろんなゲームの改善や拡張に挑戦してみてください!

同じカテゴリのページ一覧を表示