Introduce game status, playevents
This commit is contained in:
@ -32,6 +32,7 @@ from common.utils import build_dynamic_filter, safe_division, truncate
|
||||
from games.forms import GameForm
|
||||
from games.models import Game, Purchase
|
||||
from games.views.general import use_custom_redirect
|
||||
from games.views.playevent import create_playevent_tabledata
|
||||
|
||||
|
||||
@login_required
|
||||
@ -351,7 +352,34 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
||||
],
|
||||
}
|
||||
|
||||
playevents = game.playevents.all()
|
||||
playevent_count = playevents.count()
|
||||
playevent_data = create_playevent_tabledata(playevents, exclude_columns=["Game"])
|
||||
|
||||
statuschanges = game.status_changes.all()
|
||||
statuschange_count = statuschanges.count()
|
||||
statuschange_data = {
|
||||
"columns": [
|
||||
"Old Status",
|
||||
"New Status",
|
||||
"Timestamp",
|
||||
],
|
||||
"rows": [
|
||||
[
|
||||
statuschange.get_old_status_display()
|
||||
if statuschange.old_status
|
||||
else "-",
|
||||
statuschange.get_new_status_display(),
|
||||
local_strftime(statuschange.timestamp, dateformat),
|
||||
]
|
||||
for statuschange in statuschanges
|
||||
],
|
||||
}
|
||||
|
||||
context: dict[str, Any] = {
|
||||
"statuschange_data": statuschange_data,
|
||||
"statuschange_count": statuschange_count,
|
||||
"statuschanges": statuschanges,
|
||||
"game": game,
|
||||
"playrange": playrange,
|
||||
"purchase_count": game.purchases.count(),
|
||||
@ -366,6 +394,8 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
||||
"title": f"Game Overview - {game.name}",
|
||||
"hours_sum": total_hours,
|
||||
"purchase_data": purchase_data,
|
||||
"playevent_data": playevent_data,
|
||||
"playevent_count": playevent_count,
|
||||
"session_data": session_data,
|
||||
"session_page_obj": session_page_obj,
|
||||
"session_elided_page_range": (
|
||||
|
@ -305,27 +305,39 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
||||
sessions__in=this_year_sessions
|
||||
).distinct()
|
||||
|
||||
this_year_purchases = Purchase.objects.filter(date_purchased__year=year)
|
||||
this_year_purchases_with_currency = this_year_purchases.prefetch_related("games")
|
||||
this_year_purchases_without_refunded = this_year_purchases_with_currency.filter(
|
||||
date_refunded=None
|
||||
).exclude(ownership_type=Purchase.DEMO)
|
||||
this_year_purchases_refunded = this_year_purchases_with_currency.refunded()
|
||||
this_year_purchases = Purchase.objects.filter(
|
||||
date_purchased__year=year
|
||||
).prefetch_related("games")
|
||||
# purchased this year
|
||||
# not refunded
|
||||
this_year_purchases_without_refunded = Purchase.objects.filter(
|
||||
date_refunded=None, date_purchased__year=year
|
||||
)
|
||||
|
||||
# purchased this year
|
||||
# not refunded
|
||||
# not finished
|
||||
# not infinite
|
||||
# only Game and DLC
|
||||
this_year_purchases_unfinished_dropped_nondropped = (
|
||||
this_year_purchases_without_refunded.filter(date_finished__isnull=True)
|
||||
this_year_purchases_without_refunded.exclude(
|
||||
games__in=Game.objects.filter(status="f")
|
||||
)
|
||||
.filter(infinite=False)
|
||||
.filter(Q(type=Purchase.GAME) | Q(type=Purchase.DLC))
|
||||
) # do not count battle passes etc.
|
||||
)
|
||||
|
||||
# not finished
|
||||
this_year_purchases_unfinished = (
|
||||
this_year_purchases_unfinished_dropped_nondropped.filter(
|
||||
date_dropped__isnull=True
|
||||
this_year_purchases_unfinished_dropped_nondropped.exclude(
|
||||
games__status__in="ura"
|
||||
)
|
||||
)
|
||||
# abandoned
|
||||
# retired
|
||||
this_year_purchases_dropped = (
|
||||
this_year_purchases_unfinished_dropped_nondropped.filter(
|
||||
date_dropped__isnull=False
|
||||
this_year_purchases_unfinished_dropped_nondropped.exclude(
|
||||
games__in=Game.objects.filter(status="ar")
|
||||
)
|
||||
)
|
||||
|
||||
@ -341,15 +353,17 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
||||
* 100
|
||||
)
|
||||
|
||||
purchases_finished_this_year = Purchase.objects.filter(date_finished__year=year)
|
||||
purchases_finished_this_year = Purchase.objects.filter(
|
||||
games__playevents__ended__year=2025
|
||||
).annotate(game_name=F("games__name"), date_finished=F("games__playevents__ended"))
|
||||
purchases_finished_this_year_released_this_year = (
|
||||
purchases_finished_this_year.filter(games__year_released=year).order_by(
|
||||
"date_finished"
|
||||
"games__playevents__ended"
|
||||
)
|
||||
)
|
||||
purchased_this_year_finished_this_year = (
|
||||
this_year_purchases_without_refunded.filter(date_finished__year=year)
|
||||
).order_by("date_finished")
|
||||
this_year_purchases_without_refunded.filter(games__playevents__ended__year=year)
|
||||
).order_by("games__playevents__ended")
|
||||
|
||||
this_year_spendings = this_year_purchases_without_refunded.aggregate(
|
||||
total_spent=Sum(F("converted_price"))
|
||||
@ -395,8 +409,9 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
||||
item["formatted_playtime"] = format_duration(item["total_playtime"], "%2.0H")
|
||||
|
||||
backlog_decrease_count = (
|
||||
Purchase.objects.filter(date_purchased__year__lt=year)
|
||||
.intersection(purchases_finished_this_year)
|
||||
Purchase.objects.filter(date_purchased__year__lt=2025)
|
||||
.filter(games__status="f")
|
||||
.filter(games__playevents__ended__year=2025)
|
||||
.count()
|
||||
)
|
||||
|
||||
@ -412,7 +427,10 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
||||
last_play_game = last_session.game
|
||||
last_play_date = last_session.timestamp_start.strftime(dateformat)
|
||||
|
||||
all_purchased_this_year_count = this_year_purchases_with_currency.count()
|
||||
all_purchased_this_year_count = this_year_purchases.count()
|
||||
this_year_purchases_refunded = Purchase.objects.exclude(date_refunded=None).filter(
|
||||
date_purchased__year=year
|
||||
)
|
||||
all_purchased_refunded_this_year_count = this_year_purchases_refunded.count()
|
||||
|
||||
this_year_purchases_dropped_count = this_year_purchases_dropped.count()
|
||||
@ -439,15 +457,15 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
||||
),
|
||||
"all_finished_this_year": purchases_finished_this_year.prefetch_related(
|
||||
"games"
|
||||
).order_by("date_finished"),
|
||||
).order_by("games__playevents__ended"),
|
||||
"all_finished_this_year_count": purchases_finished_this_year.count(),
|
||||
"this_year_finished_this_year": purchases_finished_this_year_released_this_year.prefetch_related(
|
||||
"games"
|
||||
).order_by("date_finished"),
|
||||
).order_by("games__playevents__ended"),
|
||||
"this_year_finished_this_year_count": purchases_finished_this_year_released_this_year.count(),
|
||||
"purchased_this_year_finished_this_year": purchased_this_year_finished_this_year.prefetch_related(
|
||||
"games"
|
||||
).order_by("date_finished"),
|
||||
).order_by("games__playevents__ended"),
|
||||
"total_sessions": this_year_sessions.count(),
|
||||
"unique_days": unique_days["dates"],
|
||||
"unique_days_percent": int(unique_days["dates"] / 365 * 100),
|
||||
@ -465,9 +483,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
||||
),
|
||||
"all_purchased_refunded_this_year": this_year_purchases_refunded,
|
||||
"all_purchased_refunded_this_year_count": all_purchased_refunded_this_year_count,
|
||||
"all_purchased_this_year": this_year_purchases_with_currency.order_by(
|
||||
"date_purchased"
|
||||
),
|
||||
"all_purchased_this_year": this_year_purchases.order_by("date_purchased"),
|
||||
"all_purchased_this_year_count": all_purchased_this_year_count,
|
||||
"backlog_decrease_count": backlog_decrease_count,
|
||||
"longest_session_time": (
|
||||
|
147
games/views/playevent.py
Normal file
147
games/views/playevent.py
Normal file
@ -0,0 +1,147 @@
|
||||
import logging
|
||||
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
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
|
||||
from common.components import A, Button, Icon
|
||||
from common.time import dateformat, local_strftime
|
||||
from games.forms import PlayEventForm
|
||||
from games.models import Game, PlayEvent
|
||||
|
||||
logger = logging.getLogger("games")
|
||||
|
||||
|
||||
class TableData(TypedDict):
|
||||
header_action: Callable[..., Any]
|
||||
columns: list[str]
|
||||
rows: list[list[Any]]
|
||||
|
||||
|
||||
def create_playevent_tabledata(
|
||||
playevents: list[PlayEvent] | BaseManager[PlayEvent] | QuerySet[PlayEvent],
|
||||
exclude_columns: list[str] = [],
|
||||
request: HttpRequest | None = None,
|
||||
) -> TableData:
|
||||
column_list = [
|
||||
"Game",
|
||||
"Started",
|
||||
"Ended",
|
||||
"Days to finish",
|
||||
"Note",
|
||||
"Created",
|
||||
"Actions",
|
||||
]
|
||||
filtered_column_list = filter(
|
||||
lambda x: x not in exclude_columns,
|
||||
column_list,
|
||||
)
|
||||
excluded_column_indexes = [column_list.index(column) for column in exclude_columns]
|
||||
|
||||
row_list = [
|
||||
[
|
||||
playevent.game,
|
||||
playevent.started.strftime(dateformat) if playevent.started else "-",
|
||||
playevent.ended.strftime(dateformat) if playevent.ended else "-",
|
||||
playevent.days_to_finish if playevent.days_to_finish else "-",
|
||||
playevent.note,
|
||||
local_strftime(playevent.created_at, dateformat),
|
||||
render_to_string(
|
||||
"cotton/button_group.html",
|
||||
{
|
||||
"buttons": [
|
||||
{
|
||||
"href": reverse("edit_playevent", args=[playevent.pk]),
|
||||
"slot": Icon("edit"),
|
||||
"color": "gray",
|
||||
},
|
||||
{
|
||||
"href": reverse("delete_playevent", args=[playevent.pk]),
|
||||
"slot": Icon("delete"),
|
||||
"color": "red",
|
||||
},
|
||||
]
|
||||
},
|
||||
),
|
||||
]
|
||||
for playevent in playevents
|
||||
]
|
||||
filtered_row_list = [
|
||||
[column for idx, column in enumerate(row) if idx not in excluded_column_indexes]
|
||||
for row in row_list
|
||||
]
|
||||
return {
|
||||
"header_action": A([], Button([], "Add play event"), url="add_playevent"),
|
||||
"columns": list(filtered_column_list),
|
||||
"rows": filtered_row_list,
|
||||
}
|
||||
|
||||
|
||||
@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
|
||||
context: dict[str, Any] = {
|
||||
"title": "Manage play events",
|
||||
"page_obj": page_obj or None,
|
||||
"elided_page_range": (
|
||||
page_obj.paginator.get_elided_page_range(
|
||||
page_number, on_each_side=1, on_ends=1
|
||||
)
|
||||
if page_obj
|
||||
else None
|
||||
),
|
||||
"data": create_playevent_tabledata(playevents, request=request),
|
||||
}
|
||||
return render(request, "list_playevents.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def add_playevent(request: HttpRequest, game_id: int = 0) -> HttpResponse:
|
||||
initial: dict[str, Any] = {}
|
||||
if game_id:
|
||||
# coming from add_playevent_for_game url path
|
||||
game = get_object_or_404(Game, id=game_id)
|
||||
initial["game"] = game
|
||||
form = PlayEventForm(request.POST or None, initial=initial)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
if not game_id:
|
||||
# coming from add_playevent url path
|
||||
game_id = form.instance.game.id
|
||||
return HttpResponseRedirect(reverse("view_game", args=[game_id]))
|
||||
|
||||
return render(request, "add.html", {"form": form, "title": "Add new playthrough"})
|
||||
|
||||
|
||||
def edit_playevent(request: HttpRequest, playevent_id: int) -> HttpResponse:
|
||||
context: dict[str, Any] = {}
|
||||
playevent = get_object_or_404(PlayEvent, id=playevent_id)
|
||||
form = PlayEventForm(request.POST or None, instance=playevent)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return HttpResponseRedirect(reverse("view_game", args=[playevent.game.id]))
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"title": "Edit Play Event",
|
||||
}
|
||||
return render(request, "add.html", context)
|
||||
|
||||
|
||||
def delete_playevent(request: HttpRequest, playevent_id: int) -> HttpResponse:
|
||||
playevent = get_object_or_404(PlayEvent, id=playevent_id)
|
||||
playevent.delete()
|
||||
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
|
@ -51,8 +51,6 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
|
||||
"Infinite",
|
||||
"Purchased",
|
||||
"Refunded",
|
||||
"Finished",
|
||||
"Dropped",
|
||||
"Created",
|
||||
"Actions",
|
||||
],
|
||||
@ -68,39 +66,11 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
|
||||
if purchase.date_refunded
|
||||
else "-"
|
||||
),
|
||||
(
|
||||
purchase.date_finished.strftime(dateformat)
|
||||
if purchase.date_finished
|
||||
else "-"
|
||||
),
|
||||
(
|
||||
purchase.date_dropped.strftime(dateformat)
|
||||
if purchase.date_dropped
|
||||
else "-"
|
||||
),
|
||||
purchase.created_at.strftime(dateformat),
|
||||
render_to_string(
|
||||
"cotton/button_group.html",
|
||||
{
|
||||
"buttons": [
|
||||
{
|
||||
"href": reverse(
|
||||
"finish_purchase", args=[purchase.pk]
|
||||
),
|
||||
"slot": Icon("checkmark"),
|
||||
"title": "Mark as finished",
|
||||
}
|
||||
if not purchase.date_finished
|
||||
else {},
|
||||
{
|
||||
"href": reverse(
|
||||
"drop_purchase", args=[purchase.pk]
|
||||
),
|
||||
"slot": Icon("eject"),
|
||||
"title": "Mark as dropped",
|
||||
}
|
||||
if not purchase.date_dropped
|
||||
else {},
|
||||
{
|
||||
"href": reverse(
|
||||
"refund_purchase", args=[purchase.pk]
|
||||
|
Reference in New Issue
Block a user