소스 검색

Add pagination and category pages

Colin Powell 3 년 전
부모
커밋
db008e6504

+ 3 - 1
emus/settings.py

@@ -42,8 +42,9 @@ INSTALLED_APPS = [
     "django.contrib.staticfiles",
     "django_extensions",
     "emus",
-    "games",
+    "mathfilters",
     "search",
+    "games",
     "rest_framework",
 ]
 
@@ -186,6 +187,7 @@ GAME_SYSTEM_SLUG_MAP = {
     "n64": "Nintendo 64",
     "nds": "Nintendo DS",
     "ngp": "Neo Geo Pocket",
+    "neogeo": "Neo Geo",
     "ngpc": "Neo Geo Pocket Color",
     "nes": "Nintendo",
     "pcengine": "PC Engine/TurboGrafix 16",

+ 1 - 1
emus/urls.py

@@ -22,8 +22,8 @@ urlpatterns = [
     path("admin/", admin.site.urls),
     path("api-auth/", include("rest_framework.urls")),
     path("api/v1/", include(router.urls)),
-    path("games/", include(games_urls, namespace="games")),
     path("search/", include(search_urls, namespace="search")),
+    path("", include(games_urls, namespace="games")),
 ]
 
 if settings.DEBUG:

+ 15 - 1
games/api/serializers.py

@@ -1,6 +1,6 @@
+from games.models import Developer, Game, GameSystem, Genre, Publisher
 from rest_framework import serializers
 
-from games.models import Developer, Game, GameSystem, Publisher
 
 # Serializers define the API representation.
 class GameSerializer(serializers.HyperlinkedModelSerializer):
@@ -14,6 +14,7 @@ class GameSerializer(serializers.HyperlinkedModelSerializer):
             "players",
             "rating",
             "game_system",
+            "genre",
         )
 
 
@@ -21,6 +22,7 @@ class DeveloperSerializer(serializers.HyperlinkedModelSerializer):
     class Meta:
         model = Developer
         fields = (
+            "id",
             "name",
             "slug",
         )
@@ -30,6 +32,17 @@ class PublisherSerializer(serializers.HyperlinkedModelSerializer):
     class Meta:
         model = Publisher
         fields = (
+            "id",
+            "name",
+            "slug",
+        )
+
+
+class GenreSerializer(serializers.HyperlinkedModelSerializer):
+    class Meta:
+        model = Genre
+        fields = (
+            "id",
             "name",
             "slug",
         )
@@ -39,6 +52,7 @@ class GameSystemSerializer(serializers.HyperlinkedModelSerializer):
     class Meta:
         model = GameSystem
         fields = (
+            "id",
             "name",
             "retropie_slug",
             "slug",

+ 7 - 1
games/api/views.py

@@ -2,9 +2,10 @@ from games.api.serializers import (
     DeveloperSerializer,
     GameSerializer,
     GameSystemSerializer,
+    GenreSerializer,
     PublisherSerializer,
 )
-from games.models import Developer, Game, GameSystem, Publisher
+from games.models import Developer, Game, GameSystem, Genre, Publisher
 from rest_framework import viewsets
 
 
@@ -23,6 +24,11 @@ class DeveloperViewSet(viewsets.ModelViewSet):
     serializer_class = DeveloperSerializer
 
 
+class GenreViewSet(viewsets.ModelViewSet):
+    queryset = Genre.objects.all()
+    serializer_class = GenreSerializer
+
+
 class GameSystemViewSet(viewsets.ModelViewSet):
     queryset = GameSystem.objects.all()
     serializer_class = GameSystemSerializer

+ 6 - 3
games/models.py

@@ -51,15 +51,18 @@ class BaseModel(TimeStampedModel):
 
 
 class Genre(BaseModel):
-    ...
+    def get_absolute_url(self):
+        return reverse("games:genre_detail", args=[self.slug])
 
 
 class Publisher(BaseModel):
-    ...
+    def get_absolute_url(self):
+        return reverse("games:publisher_detail", args=[self.slug])
 
 
 class Developer(BaseModel):
-    ...
+    def get_absolute_url(self):
+        return reverse("games:developer_detail", args=[self.slug])
 
 
 class GameSystem(BaseModel):

+ 32 - 5
games/urls.py

@@ -3,13 +3,40 @@ from games.api.views import GameViewSet
 from rest_framework import routers
 
 # importing views from views..py
-from .views import GameList, GameDetail, GameSystemDetail, GamePlayDetail
+import games.views as views
 
 app_name = "games"
 
 urlpatterns = [
-    path("", GameList.as_view(), name="game_list"),
-    path("<str:slug>/", GameDetail.as_view(), name="game_detail"),
-    path("<str:slug>/play/", GamePlayDetail.as_view(), name="game_play_detail"),
-    path("system/<str:slug>/", GameSystemDetail.as_view(), name="game_system_detail"),
+    path(
+        "",
+        views.GameList.as_view(),
+        name="game_list",
+    ),
+    path(
+        "<str:slug>/",
+        views.GameDetail.as_view(),
+        name="game_detail",
+    ),
+    path(
+        "<str:slug>/play/",
+        views.GamePlayDetail.as_view(),
+        name="game_play_detail",
+    ),
+    path(
+        "system/<str:slug>/",
+        views.GameSystemDetail.as_view(),
+        name="game_system_detail",
+    ),
+    path("genre/<str:slug>/", views.GenreDetail.as_view(), name="genre_detail"),
+    path(
+        "publisher/<str:slug>/",
+        views.PublisherDetail.as_view(),
+        name="publisher_detail",
+    ),
+    path(
+        "developer/<str:slug>/",
+        views.DeveloperDetail.as_view(),
+        name="developer_detail",
+    ),
 ]

+ 49 - 2
games/views.py

@@ -1,12 +1,15 @@
 from django.shortcuts import render
 from django.views.generic import DetailView, ListView
 from django.views.generic.base import TemplateView
+from django.views.generic.list import MultipleObjectMixin
 
-from .models import Game, GameSystem
+
+from .models import Game, GameSystem, Genre, Developer, Publisher
 
 
 class GameList(ListView):
     model = Game
+    paginate_by = 20
     queryset = Game.objects.order_by("-created")[:20]
 
 
@@ -19,5 +22,49 @@ class GamePlayDetail(DetailView):
     model = Game
 
 
-class GameSystemDetail(DetailView):
+class GameSystemDetail(DetailView, MultipleObjectMixin):
     model = GameSystem
+    paginate_by = 20
+
+    def get_context_data(self, **kwargs):
+        object_list = Game.objects.filter(game_system=self.get_object())
+        context = super(GameSystemDetail, self).get_context_data(
+            object_list=object_list, **kwargs
+        )
+        return context
+
+
+class GenreDetail(DetailView, MultipleObjectMixin):
+    model = Genre
+    paginate_by = 20
+
+    def get_context_data(self, **kwargs):
+        object_list = Game.objects.filter(genre=self.get_object())
+        context = super(GenreDetail, self).get_context_data(
+            object_list=object_list, **kwargs
+        )
+        return context
+
+
+class PublisherDetail(DetailView):
+    model = Publisher
+    paginate_by = 20
+
+    def get_context_data(self, **kwargs):
+        object_list = Game.objects.filter(publisher=self.get_object())
+        context = super(PublisherDetail, self).get_context_data(
+            object_list=object_list, **kwargs
+        )
+        return context
+
+
+class DeveloperDetail(DetailView):
+    model = Developer
+    paginate_by = 20
+
+    def get_context_data(self, **kwargs):
+        object_list = Game.objects.filter(developer=self.get_object())
+        context = super(DeveloperDetail, self).get_context_data(
+            object_list=object_list, **kwargs
+        )
+        return context

+ 13 - 1
poetry.lock

@@ -90,6 +90,14 @@ python-versions = ">=3.6"
 [package.dependencies]
 Django = ">=2.2"
 
+[[package]]
+name = "django-mathfilters"
+version = "1.0.0"
+description = "A set of simple math filters for Django"
+category = "main"
+optional = false
+python-versions = "*"
+
 [[package]]
 name = "djangorestframework"
 version = "3.13.1"
@@ -229,7 +237,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.8"
-content-hash = "45cc84e2a641f09941628bbee75fb65069fd217a19d6b67817061ad0a0dc7d61"
+content-hash = "25c74c3d2bda20d57b10980b7474309594923f6fb4cd553bfb64eca200efec8e"
 
 [metadata.files]
 asgiref = [
@@ -278,6 +286,10 @@ django-filter = [
     {file = "django-filter-21.1.tar.gz", hash = "sha256:632a251fa8f1aadb4b8cceff932bb52fe2f826dd7dfe7f3eac40e5c463d6836e"},
     {file = "django_filter-21.1-py3-none-any.whl", hash = "sha256:f4a6737a30104c98d2e2a5fb93043f36dd7978e0c7ddc92f5998e85433ea5063"},
 ]
+django-mathfilters = [
+    {file = "django-mathfilters-1.0.0.tar.gz", hash = "sha256:c9b892ef6dfc893683e75cfd0279c187a601ca68f4684c38f9da44657fb64b07"},
+    {file = "django_mathfilters-1.0.0-py3-none-any.whl", hash = "sha256:64200a21bb249fbf27be601d4bbb788779e09c6e063170c097cd82c4d18ebb83"},
+]
 djangorestframework = [
     {file = "djangorestframework-3.13.1-py3-none-any.whl", hash = "sha256:24c4bf58ed7e85d1fe4ba250ab2da926d263cd57d64b03e8dcef0ac683f8b1aa"},
     {file = "djangorestframework-3.13.1.tar.gz", hash = "sha256:0c33407ce23acc68eca2a6e46424b008c9c02eceb8cf18581921d0092bc1f2ee"},

+ 1 - 0
pyproject.toml

@@ -18,6 +18,7 @@ django-filter = "^21.1"
 Pillow = "^9.0.1"
 psycopg2 = {version = "^2.9.3", extras = ["production"]}
 dj-database-url = "^0.5.0"
+django-mathfilters = "^1.0.0"
 
 [build-system]
 requires = ["poetry-core>=1.0.0"]

+ 2 - 0
search/views.py

@@ -19,5 +19,7 @@ def search(request):
             | Q(game_system__name__icontains=query)
             | Q(publisher__name__icontains=query)
             | Q(genre__name__icontains=query)
+            | Q(publisher__name__icontains=query)
+            | Q(developer__name__icontains=query)
         ).distinct()
     return render(request, "search/search.html", {"query": query, "results": results})

+ 9 - 14
templates/base.html

@@ -29,21 +29,16 @@
             min-height: 3em;
             border-right: 1px solid #777;
         }
-
-        .card {
-            width: 28rem;
-            margin: 2rem;
-            padding:1rem;
-        }
-
-        .image-grid-container {
-            display: grid;
-
-            /* For 2 columns */
-            grid-template-columns: auto auto;
-        }
-         .gba { background: #aa11FF;}
+         .card img { width:18em; padding: 1em; }
+         .card-block { padding: 1em 0 1em 0; }
+         .system-badge { padding: 1em; font-size: normal; }
+         .gba { background: #777777;}
          .virtualboy { background: #99aa11; }
+         .ps2 { background:  #111caa; }
+         .pcengine { background: #55b4cc; }
+         .gc { background:  #aa11ff; }
+         .snes { background: #bb1111; }
+         .scummvm { background: #cccc11; color:black; }
         </style>
         {% block head_extra %}{% endblock %}
 

+ 29 - 0
templates/games/_game_card.html

@@ -0,0 +1,29 @@
+<div class="card">
+    <div class="row no-gutters">
+        <div class="col-auto">
+            <img src="{{game.screenshot.url}}" class="img-fluid" alt="Screenshot of {{game.name}}">
+        </div>
+        <div class="col">
+            <div class="card-block px-2">
+                <h4 class="card-title">{{game.name}}</h4>
+                <p class="card-text">{{game.description|truncatechars:220}}</p>
+                <a href="{{game.get_absolute_url}}" class="btn btn-primary">More</a>
+                <a href="{{game.rom_file.url}}" class="btn btn-alert">Download</a>
+            </div>
+        </div>
+    </div>
+    <div class="card-footer w-100 text-muted">
+        <a href="{{game.game_system.get_absolute_url}}">
+            <span class="system-badge badge badge-success {{game.game_system.retropie_slug}}">{{game.game_system.name|upper}}</span>
+        </a>
+
+        <div id="genre-badges" style="float:right">
+        {% for genre in game.genre.all %}
+        <a href="{{genre.get_absolute_url}}">
+            <span class="badge">{{genre.name}}</span>
+        </a>
+        {% endfor %}
+        </div>
+    </div>
+</div>
+<br />

+ 35 - 0
templates/games/_pagination.html

@@ -0,0 +1,35 @@
+{% load mathfilters %}
+
+{% if page_obj.has_other_pages %}
+<div class="pagination">
+  <nav aria-label="Page navigation">
+    <ul class="pagination">
+      {% if page_obj.has_previous %}
+      <li class="page-item">
+        <a class="page-link" href="?page={{page_obj.number|sub:1}}" aria-label="Previous">
+          <span aria-hidden="true">&laquo;</span>
+          <span class="sr-only">Previous</span>
+        </a>
+      </li>
+      {% endif %}
+      {% for i in page_obj.paginator.page_range %}
+      {% if page_obj.number == i %}
+        <li class="page-item active">
+        <a class="page-link" href="#">{{i}} <span class="sr-only">(current)</span></a>
+        </li>
+      {% else %}
+          <li class="page-item"><a class="page-link" href="?page={{i}}">{{i}}</a></li>
+      {% endif %}
+      {% endfor %}
+      {% if page_obj.has_next %}
+      <li class="page-item">
+        <a class="page-link" href="?page={{page_obj.number|add:1}}" aria-label="Next">
+          <span aria-hidden="true">&raquo;</span>
+          <span class="sr-only">Next</span>
+        </a>
+      </li>
+      {% endif %}
+    </ul>
+  </nav>
+</div>
+{% endif %}

+ 15 - 0
templates/games/developer_detail.html

@@ -0,0 +1,15 @@
+{% extends "base.html" %}
+
+{% block title %}Developer: {{object.name}}{% endblock %}
+
+{% block content %}
+    <h4>{{object.game_set.count}} games</h4>
+     <div class="d-flex flex-column">
+        <div class="image-grid-container">
+        {% for  game in object_list.game_set.all %}
+            {% include "games/_game_card.html" %}
+        {% endfor %}
+        </div>
+     </div>
+     {% include "games/_pagination.html" %}
+{% endblock %}

+ 4 - 1
templates/games/game_detail.html

@@ -16,13 +16,16 @@
         <dd>{{object.players}}</dd>
 
         <dt>Game System</dt>
-        <dd>{{object.game_system}}</dd>
+        <dd>{{object.game_system.name}}</dd>
 
         <dt>Genre</dt>
         <dd>{% for genre in object.genre.all %}{{genre}}{% if not forloop.last %}, {% endif %}{% endfor %}</dd>
 
         <dt>Region</dt>
         <dd>{{object.region}}</dd>
+
+        <dt>Publisher/Developer</dt>
+        <dd>{{object.publisher}}/{{object.developer}}</dd>
     </dl>
 
     <p>{{object.description}}</p>

+ 3 - 15
templates/games/game_list.html

@@ -6,22 +6,10 @@
      <div class="d-flex flex-column">
         <div class="image-grid-container">
         {% for  game in object_list %}
-           <div class="card d-flex flex-column">
-                <span class="badge badge-success {{game.game_system.retropie_slug}}">{{game.game_system.name}}</span>
-                <br />
-                {% if game.screenshot %}
-                <img class="card-img-top" src="{{game.screenshot.url}}" alt="Card image cap">
-                {% elif game.marquee %}
-                <img class="card-img-top" src="{{game.screenshot.url}}" alt="Card image cap">
-                {% endif %}
-                <div class="card-body">
-                    <h5 class="card-title">{{game.name}}</h5>
-                    <p class="card-text">{{game.description|truncatechars:220}}</p>
-                    <a href="{{game.get_absolute_url}}" class="btn btn-primary">More</a>
-                    <a href="{{game.rom_file.url}}" class="btn btn-alert">Download</a>
-                </div>
-            </div>
+            {% include 'games/_game_card.html' %}
         {% endfor %}
         </div>
     </div>
+
+     {% include "games/_pagination.html" %}
 {% endblock %}

+ 8 - 11
templates/games/gamesystem_detail.html

@@ -4,16 +4,13 @@
 
 {% block content %}
     <h4>Browsing ({{object.game_set.count}}) games</h4>
-    {% for  game in object.game_set.all %}
-        <div class="card d-flex float-left" style="width: 12em; padding:1em">
-        <a href="{{game.get_absolute_url}}">
-            {% if game.screenshot %}
-            <img class="card-img-top" src="{{game.screenshot.url}}" alt="Card image cap">
-            {% else %}
-            <i>{{game.name}}</i>
-            {% endif %}
-        </a>
+     <div class="d-flex flex-column">
+        <div class="image-grid-container">
+        {% for  game in object_list.all %}
+            {% include "games/_game_card.html" %}
+        {% endfor %}
         </div>
-    {% endfor %}
-    </ul>
+     </div>
+
+     {% include "games/_pagination.html" %}
 {% endblock %}

+ 16 - 0
templates/games/genre_detail.html

@@ -0,0 +1,16 @@
+{% extends "base.html" %}
+
+{% block title %}Genre: {{object.name}}{% endblock %}
+
+{% block content %}
+    <h4>{{object.game_set.count}} games</h4>
+     <div class="d-flex flex-column">
+        <div class="image-grid-container">
+        {% for  game in object_list.all %}
+            {% include "games/_game_card.html" %}
+        {% endfor %}
+        </div>
+     </div>
+
+     {% include "games/_pagination.html" %}
+{% endblock %}

+ 16 - 0
templates/games/publisher_detail.html

@@ -0,0 +1,16 @@
+{% extends "base.html" %}
+
+{% block title %}Publisher: {{object.name}}{% endblock %}
+
+{% block content %}
+    <h4>{{object.game_set.count}} games</h4>
+     <div class="d-flex flex-column">
+        <div class="image-grid-container">
+        {% for  game in object_list.game_set.all %}
+            {% include "games/_game_card.html" %}
+        {% endfor %}
+        </div>
+     </div>
+
+     {% include "games/_pagination.html" %}
+{% endblock %}

+ 8 - 14
templates/search/search.html

@@ -5,20 +5,14 @@
 {% block content %}
 {% regroup results by game_system as game_system_list %}
     {% for system in game_system_list %}
-        <h2>{{system.grouper.name}} <a href="{{system.grouper.get_absolute_url}}">&raquo;</a></h2>
+    <h2>{{system.grouper.name}} <a href="{{system.grouper.get_absolute_url}}">&raquo;</a></h2>
 
-        <div class="container">
-        {% for  game in system.list %}
-            <div class="card d-flex float-left">
-                <img class="card-img-top" src="{{game.screenshot.url}}" alt="Card image cap">
-                <div class="card-body">
-                    <h5 class="card-title">{{game.name}}</h5>
-                    <p class="card-text">{{game.description|truncatechars:220}}</p>
-                    <a href="{{game.get_absolute_url}}" class="btn btn-primary">More</a>
-                    <a href="{{game.rom_file.url}}" class="btn btn-alert">Download</a>
-                </div>
-            </div>
-        {% endfor %}
-        </div>
+    <div class="d-flex flex-column">
+       <div class="image-grid-container">
+       {% for  game in system.list %}
+           {% include 'games/_game_card.html' %}
+       {% endfor %}
+       </div>
+    </div>
     {% endfor %}
 {% endblock %}