diff --git a/common/utils.py b/common/utils.py index 9355b60..ec74aca 100644 --- a/common/utils.py +++ b/common/utils.py @@ -5,11 +5,34 @@ from functools import reduce, wraps from typing import Any, Callable, Generator, Literal, TypeVar from urllib.parse import urlencode +from django.core.paginator import Page, Paginator from django.db.models import Q from django.http import HttpRequest from django.shortcuts import redirect +def paginate(request: HttpRequest, queryset, per_page: int = 10): + """Standard list-view pagination. + + Reads ``page`` and ``limit`` from the query string (``limit=0`` disables + pagination) and returns ``(object_list, page_obj, elided_page_range)`` ready + to hand to ``paginated_table_content``. + """ + page_number = request.GET.get("page", 1) + limit = int(request.GET.get("limit", per_page)) + object_list = queryset + page_obj: Page | None = None + if limit != 0: + page_obj = Paginator(queryset, limit).get_page(page_number) + object_list = page_obj.object_list + elided_page_range = ( + page_obj.paginator.get_elided_page_range(page_number, on_each_side=1, on_ends=1) + if page_obj + else None + ) + return object_list, page_obj, elided_page_range + + def safe_division(numerator: int | float, denominator: int | float) -> int | float: """ Divides without triggering division by zero exception. diff --git a/games/views/device.py b/games/views/device.py index 4e381dc..f73b31b 100644 --- a/games/views/device.py +++ b/games/views/device.py @@ -1,5 +1,4 @@ from django.contrib.auth.decorators import login_required -from django.core.paginator import Paginator from django.http import HttpRequest, HttpResponse from django.shortcuts import get_object_or_404, redirect from django.urls import reverse @@ -14,24 +13,15 @@ from common.components import ( ) from common.layout import render_page from common.time import dateformat, local_strftime +from common.utils import paginate from games.forms import DeviceForm from games.models import Device @login_required def list_devices(request: HttpRequest) -> HttpResponse: - page_number = request.GET.get("page", 1) - limit = request.GET.get("limit", 10) - devices = Device.objects.order_by("-created_at") - page_obj = None - if int(limit) != 0: - paginator = Paginator(devices, limit) - page_obj = paginator.get_page(page_number) - devices = page_obj.object_list - elided_page_range = ( - page_obj.paginator.get_elided_page_range(page_number, on_each_side=1, on_ends=1) - if page_obj - else None + devices, page_obj, elided_page_range = paginate( + request, Device.objects.order_by("-created_at") ) data = { diff --git a/games/views/game.py b/games/views/game.py index 31962cf..2232ddd 100644 --- a/games/views/game.py +++ b/games/views/game.py @@ -41,7 +41,7 @@ from common.time import ( local_strftime, timeformat, ) -from common.utils import build_dynamic_filter, safe_division, truncate +from common.utils import build_dynamic_filter, paginate, safe_division, truncate from games.forms import GameForm from games.models import Game from games.views.general import use_custom_redirect @@ -50,10 +50,7 @@ from games.views.playevent import create_playevent_tabledata @login_required def list_games(request: HttpRequest, search_string: str = "") -> HttpResponse: - page_number = request.GET.get("page", 1) - limit = request.GET.get("limit", 10) games = Game.objects.order_by("-created_at") - page_obj = None search_string = request.GET.get("search_string", search_string) if search_string != "": filters = [ @@ -74,16 +71,7 @@ def list_games(request: HttpRequest, search_string: str = "") -> HttpResponse: search_status = Game.Status[search_string.upper()] filters.append(Q(status=search_status)) games = games.filter(build_dynamic_filter(filters, "|")) - if int(limit) != 0: - paginator = Paginator(games, limit) - page_obj = paginator.get_page(page_number) - games = page_obj.object_list - - elided_page_range = ( - page_obj.paginator.get_elided_page_range(page_number, on_each_side=1, on_ends=1) - if page_obj - else None - ) + games, page_obj, elided_page_range = paginate(request, games) data = { "header_action": Div( diff --git a/games/views/general.py b/games/views/general.py index 2a1461e..7ec223c 100644 --- a/games/views/general.py +++ b/games/views/general.py @@ -109,7 +109,8 @@ def stats_alltime(request: HttpRequest) -> HttpResponse: this_year_purchases_unfinished_dropped_nondropped = ( this_year_purchases_without_refunded.filter( - ~Q(games__status="f") & ~Q(games__playevents__ended__isnull=False) + ~Q(games__status=Game.Status.FINISHED) + & ~Q(games__playevents__ended__isnull=False) ) .filter(infinite=False) .filter(Q(type=Purchase.GAME) | Q(type=Purchase.DLC)) @@ -117,14 +118,16 @@ def stats_alltime(request: HttpRequest) -> HttpResponse: this_year_purchases_unfinished = ( this_year_purchases_unfinished_dropped_nondropped.filter( - ~Q(games__status="r") & ~Q(games__status="a") + ~Q(games__status=Game.Status.RETIRED) + & ~Q(games__status=Game.Status.ABANDONED) ) ) this_year_purchases_dropped = ( this_year_purchases.filter( - ~Q(games__status="f") & ~Q(games__playevents__ended__isnull=False) + ~Q(games__status=Game.Status.FINISHED) + & ~Q(games__playevents__ended__isnull=False) ) - .filter(Q(games__status="a") | Q(date_refunded__isnull=False)) + .filter(Q(games__status=Game.Status.ABANDONED) | Q(date_refunded__isnull=False)) .filter(infinite=False) .filter(Q(type=Purchase.GAME) | Q(type=Purchase.DLC)) ) @@ -338,7 +341,8 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse: # only Game and DLC this_year_purchases_unfinished_dropped_nondropped = ( this_year_purchases_without_refunded.filter( - ~Q(games__status="f") & ~Q(games__playevents__ended__year=year) + ~Q(games__status=Game.Status.FINISHED) + & ~Q(games__playevents__ended__year=year) ) .filter(infinite=False) .filter(Q(type=Purchase.GAME) | Q(type=Purchase.DLC)) @@ -347,15 +351,17 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse: # unfinished = not finished AND not dropped this_year_purchases_unfinished = ( this_year_purchases_unfinished_dropped_nondropped.filter( - ~Q(games__status="r") & ~Q(games__status="a") + ~Q(games__status=Game.Status.RETIRED) + & ~Q(games__status=Game.Status.ABANDONED) ) ) # dropped = abandoned OR retired OR refunded (OR logic for transition) this_year_purchases_dropped = ( this_year_purchases.filter( - ~Q(games__status="f") & ~Q(games__playevents__ended__year=year) + ~Q(games__status=Game.Status.FINISHED) + & ~Q(games__playevents__ended__year=year) ) - .filter(Q(games__status="a") | Q(date_refunded__isnull=False)) + .filter(Q(games__status=Game.Status.ABANDONED) | Q(date_refunded__isnull=False)) .filter(infinite=False) .filter(Q(type=Purchase.GAME) | Q(type=Purchase.DLC)) ) @@ -432,7 +438,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse: backlog_decrease_count = ( Purchase.objects.filter(date_purchased__year__lt=year) - .filter(games__status="f") + .filter(games__status=Game.Status.FINISHED) .filter(games__playevents__ended__year=year) .count() ) diff --git a/games/views/platform.py b/games/views/platform.py index e9139e2..6cfe19b 100644 --- a/games/views/platform.py +++ b/games/views/platform.py @@ -1,5 +1,4 @@ from django.contrib.auth.decorators import login_required -from django.core.paginator import Paginator from django.http import HttpRequest, HttpResponse from django.shortcuts import get_object_or_404, redirect from django.urls import reverse @@ -14,6 +13,7 @@ from common.components import ( ) from common.layout import render_page from common.time import dateformat, local_strftime +from common.utils import paginate from games.forms import PlatformForm from games.models import Platform from games.views.general import use_custom_redirect @@ -21,18 +21,8 @@ from games.views.general import use_custom_redirect @login_required def list_platforms(request: HttpRequest) -> HttpResponse: - page_number = request.GET.get("page", 1) - limit = request.GET.get("limit", 10) - platforms = Platform.objects.order_by("name") - page_obj = None - if int(limit) != 0: - paginator = Paginator(platforms, limit) - page_obj = paginator.get_page(page_number) - platforms = page_obj.object_list - elided_page_range = ( - page_obj.paginator.get_elided_page_range(page_number, on_each_side=1, on_ends=1) - if page_obj - else None + platforms, page_obj, elided_page_range = paginate( + request, Platform.objects.order_by("name") ) data = { diff --git a/games/views/playevent.py b/games/views/playevent.py index 35813bb..5cda6fc 100644 --- a/games/views/playevent.py +++ b/games/views/playevent.py @@ -3,7 +3,6 @@ from datetime import datetime, timedelta from typing import Any, Callable, TypedDict from django.contrib.auth.decorators import login_required -from django.core.paginator import Paginator from django.db.models import QuerySet from django.db.models.manager import BaseManager from django.http import HttpRequest, HttpResponse, HttpResponseRedirect @@ -20,6 +19,7 @@ from common.components import ( ) from common.layout import render_page from common.time import dateformat, format_duration, local_strftime +from common.utils import paginate from games.forms import PlayEventForm from games.models import Game, PlayEvent, Session @@ -125,18 +125,8 @@ def _get_formatted_playtime_for_game_sessions_in_range( @login_required def list_playevents(request: HttpRequest) -> HttpResponse: - page_number = request.GET.get("page", 1) - limit = request.GET.get("limit", 10) - playevents = PlayEvent.objects.order_by("-created_at") - page_obj = None - if int(limit) != 0: - paginator = Paginator(playevents, limit) - page_obj = paginator.get_page(page_number) - playevents = page_obj.object_list - elided_page_range = ( - page_obj.paginator.get_elided_page_range(page_number, on_each_side=1, on_ends=1) - if page_obj - else None + playevents, page_obj, elided_page_range = paginate( + request, PlayEvent.objects.order_by("-created_at") ) data = create_playevent_tabledata(playevents, request=request) content = paginated_table_content( diff --git a/games/views/purchase.py b/games/views/purchase.py index f41cf33..60bef0f 100644 --- a/games/views/purchase.py +++ b/games/views/purchase.py @@ -1,6 +1,5 @@ from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.core.paginator import Paginator from django.http import ( HttpRequest, HttpResponse, @@ -35,6 +34,7 @@ from common.components import ( ) from common.layout import render_page from common.time import dateformat +from common.utils import paginate from games.forms import PurchaseForm from games.models import Game, Purchase from games.views.general import use_custom_redirect @@ -95,18 +95,8 @@ def _render_purchase_row(purchase): @login_required def list_purchases(request: HttpRequest) -> HttpResponse: - page_number = request.GET.get("page", 1) - limit = request.GET.get("limit", 10) - purchases = Purchase.objects.order_by("-date_purchased", "-created_at") - page_obj = None - if int(limit) != 0: - paginator = Paginator(purchases, limit) - page_obj = paginator.get_page(page_number) - purchases = page_obj.object_list - elided_page_range = ( - page_obj.paginator.get_elided_page_range(page_number, on_each_side=1, on_ends=1) - if page_obj - else None + purchases, page_obj, elided_page_range = paginate( + request, Purchase.objects.order_by("-date_purchased", "-created_at") ) data = { diff --git a/games/views/session.py b/games/views/session.py index 8296434..8f76788 100644 --- a/games/views/session.py +++ b/games/views/session.py @@ -1,7 +1,6 @@ from typing import Any from django.contrib.auth.decorators import login_required -from django.core.paginator import Paginator from django.db.models import Q from django.http import HttpRequest, HttpResponse from django.middleware.csrf import get_token @@ -32,15 +31,13 @@ from common.time import ( local_strftime, timeformat, ) -from common.utils import truncate +from common.utils import paginate, truncate from games.forms import SessionForm from games.models import Device, Game, Session @login_required def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse: - page_number = request.GET.get("page", 1) - limit = request.GET.get("limit", 10) sessions = Session.objects.order_by("-timestamp_start", "created_at") device_list = Device.objects.order_by("name") search_string = request.GET.get("search_string", search_string) @@ -56,17 +53,7 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse last_session = sessions.latest() except Session.DoesNotExist: last_session = None - page_obj = None - if int(limit) != 0: - paginator = Paginator(sessions, limit) - page_obj = paginator.get_page(page_number) - sessions = page_obj.object_list - - elided_page_range = ( - page_obj.paginator.get_elided_page_range(page_number, on_each_side=1, on_ends=1) - if page_obj - else None - ) + sessions, page_obj, elided_page_range = paginate(request, sessions) data = { "header_action": Div( diff --git a/games/views/statuschange.py b/games/views/statuschange.py index d9053f2..bc81c28 100644 --- a/games/views/statuschange.py +++ b/games/views/statuschange.py @@ -1,5 +1,4 @@ from django.contrib.auth.decorators import login_required -from django.core.paginator import Paginator from django.http import HttpRequest, HttpResponse from django.shortcuts import get_object_or_404, redirect from django.urls import reverse @@ -16,6 +15,7 @@ from common.components import ( ) from common.layout import render_page from common.time import dateformat, local_strftime +from common.utils import paginate from games.forms import GameStatusChangeForm from games.models import GameStatusChange @@ -36,8 +36,8 @@ def edit_statuschange(request: HttpRequest, statuschange_id: int) -> HttpRespons statuschange = get_object_or_404(GameStatusChange, id=statuschange_id) form = GameStatusChangeForm(request.POST or None, instance=statuschange) if form.is_valid(): - form.save() - return redirect("games:list_platforms") + saved = form.save() + return redirect("games:view_game", game_id=saved.game.id) return render_page( request, AddForm(form, request=request), title="Edit status change" ) @@ -45,18 +45,8 @@ def edit_statuschange(request: HttpRequest, statuschange_id: int) -> HttpRespons @login_required def list_statuschanges(request: HttpRequest) -> HttpResponse: - page_number = request.GET.get("page", 1) - limit = request.GET.get("limit", 10) - statuschanges = GameStatusChange.objects.select_related("game").all() - page_obj = None - if int(limit) != 0: - paginator = Paginator(statuschanges, limit) - page_obj = paginator.get_page(page_number) - statuschanges = page_obj.object_list - elided_page_range = ( - page_obj.paginator.get_elided_page_range(page_number, on_each_side=1, on_ends=1) - if page_obj - else None + statuschanges, page_obj, elided_page_range = paginate( + request, GameStatusChange.objects.select_related("game").all() ) data = {