timetracker/games/views.py

554 lines
18 KiB
Python
Raw Normal View History

from common.time import format_duration, now as now_with_tz
2023-11-09 09:06:14 +00:00
from common.utils import safe_division
2023-11-02 08:53:28 +00:00
from datetime import datetime, timedelta
2023-01-15 22:39:52 +00:00
from django.conf import settings
from django.db.models import Sum, F, Count, Prefetch
from django.db.models.functions import TruncDate
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
2023-11-02 08:53:28 +00:00
from django.shortcuts import redirect, render
2023-11-02 08:20:09 +00:00
from django.urls import reverse
from typing import Callable, Any
2023-11-02 08:53:28 +00:00
from zoneinfo import ZoneInfo
2023-01-15 22:39:52 +00:00
2023-02-18 20:12:18 +00:00
from .forms import (
GameForm,
PlatformForm,
PurchaseForm,
SessionForm,
EditionForm,
DeviceForm,
)
2023-02-18 19:49:46 +00:00
from .models import Game, Platform, Purchase, Session, Edition
2023-01-04 16:27:54 +00:00
def model_counts(request):
return {
"game_available": Game.objects.count() != 0,
2023-02-18 19:49:46 +00:00
"edition_available": Edition.objects.count() != 0,
"platform_available": Platform.objects.count() != 0,
"purchase_available": Purchase.objects.count() != 0,
"session_count": Session.objects.count(),
}
2022-12-31 13:18:27 +00:00
2023-11-02 08:52:42 +00:00
def stats_dropdown_year_range(request):
result = {
"stats_dropdown_year_range": range(
datetime.now(ZoneInfo(settings.TIME_ZONE)).year, 1999, -1
)
}
return result
2023-11-02 08:52:42 +00:00
2023-11-09 20:01:01 +00:00
def add_session(request, purchase_id=None):
2022-12-31 13:18:27 +00:00
context = {}
2023-11-09 20:01:01 +00:00
initial = {"timestamp_start": now_with_tz()}
last = Session.objects.all().last()
if last != None:
initial["purchase"] = last.purchase
2023-11-09 20:01:01 +00:00
if request.method == "POST":
form = SessionForm(request.POST or None, initial=initial)
if form.is_valid():
form.save()
return redirect("list_sessions")
else:
if purchase_id:
purchase = Purchase.objects.get(id=purchase_id)
form = SessionForm(
initial={
**initial,
"purchase": purchase,
}
)
else:
form = SessionForm(initial=initial)
2022-12-31 13:18:27 +00:00
2023-01-05 21:09:21 +00:00
context["title"] = "Add New Session"
2022-12-31 13:18:27 +00:00
context["form"] = form
return render(request, "add_session.html", context)
2022-12-31 13:18:27 +00:00
def update_session(request, session_id=None):
session = Session.objects.get(id=session_id)
session.finish_now()
session.save()
return redirect("list_sessions")
def use_custom_redirect(
func: Callable[..., HttpResponse]
) -> Callable[..., HttpResponse]:
"""
Will redirect to "return_path" session variable if set.
"""
def wrapper(request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
response = func(request, *args, **kwargs)
if isinstance(response, HttpResponseRedirect) and (
next_url := request.session.get("return_path")
):
return HttpResponseRedirect(next_url)
return response
return wrapper
@use_custom_redirect
def edit_session(request, session_id=None):
context = {}
session = Session.objects.get(id=session_id)
form = SessionForm(request.POST or None, instance=session)
if form.is_valid():
form.save()
return redirect("list_sessions")
context["title"] = "Edit Session"
context["form"] = form
return render(request, "add_session.html", context)
@use_custom_redirect
2023-02-18 20:44:19 +00:00
def edit_purchase(request, purchase_id=None):
context = {}
purchase = Purchase.objects.get(id=purchase_id)
form = PurchaseForm(request.POST or None, instance=purchase)
if form.is_valid():
form.save()
return redirect("list_sessions")
context["title"] = "Edit Purchase"
context["form"] = form
2023-11-14 18:27:00 +00:00
context["script_name"] = "add_purchase.js"
return render(request, "add_purchase.html", context)
2023-02-18 20:44:19 +00:00
@use_custom_redirect
def edit_game(request, game_id=None):
context = {}
purchase = Game.objects.get(id=game_id)
form = GameForm(request.POST or None, instance=purchase)
if form.is_valid():
form.save()
return redirect("list_sessions")
context["title"] = "Edit Game"
context["form"] = form
return render(request, "add.html", context)
2023-10-01 19:28:02 +00:00
def view_game(request, game_id=None):
game = Game.objects.get(id=game_id)
nongame_related_purchases_prefetch = Prefetch(
"related_purchases",
queryset=Purchase.objects.exclude(type=Purchase.GAME),
to_attr="nongame_related_purchases_prefetch",
2023-11-14 18:27:00 +00:00
)
game_purchases_prefetch = Prefetch(
"purchase_set",
queryset=Purchase.objects.filter(type=Purchase.GAME).prefetch_related(
nongame_related_purchases_prefetch
),
to_attr="game_purchases",
)
editions = (
Edition.objects.filter(game=game)
.prefetch_related(game_purchases_prefetch)
.order_by("year_released")
)
2023-11-06 11:05:39 +00:00
sessions = Session.objects.filter(purchase__edition__game=game)
session_count = sessions.count()
playrange_start = sessions.first().timestamp_start.strftime("%b %Y")
playrange_end = sessions.last().timestamp_start.strftime("%b %Y")
playrange = (
2023-11-06 11:05:39 +00:00
playrange_start
if playrange_start == playrange_end
else f"{playrange_start}{playrange_end}"
)
total_hours = float(format_duration(sessions.total_duration_unformatted(), "%2.1H"))
context = {
"edition_count": editions.count(),
"editions": editions,
"game": game,
"playrange": playrange,
"purchase_count": Purchase.objects.filter(edition__game=game).count(),
"session_average": round(total_hours / int(session_count), 1),
"session_count": session_count,
"sessions_with_notes": sessions.exclude(note=""),
"sessions": sessions.order_by("-timestamp_start"),
"title": f"Game Overview - {game.name}",
"hours_sum": total_hours,
}
2023-11-06 11:05:39 +00:00
request.session["return_path"] = request.path
2023-10-01 19:28:02 +00:00
return render(request, "view_game.html", context)
@use_custom_redirect
def edit_platform(request, platform_id=None):
context = {}
purchase = Platform.objects.get(id=platform_id)
form = PlatformForm(request.POST or None, instance=purchase)
if form.is_valid():
form.save()
return redirect("list_sessions")
context["title"] = "Edit Platform"
context["form"] = form
return render(request, "add.html", context)
@use_custom_redirect
2023-02-18 20:47:25 +00:00
def edit_edition(request, edition_id=None):
context = {}
edition = Edition.objects.get(id=edition_id)
form = EditionForm(request.POST or None, instance=edition)
if form.is_valid():
form.save()
return redirect("list_sessions")
context["title"] = "Edit Edition"
context["form"] = form
return render(request, "add.html", context)
@use_custom_redirect
def start_game_session(request, game_id: int):
last_session = (
Session.objects.filter(purchase__edition__game_id=game_id)
.order_by("-timestamp_start")
.first()
)
session = SessionForm(
{
"purchase": last_session.purchase.id,
"timestamp_start": now_with_tz(),
"device": last_session.device,
}
)
session.save()
return redirect("list_sessions")
def start_session_same_as_last(request, last_session_id: int):
2023-02-18 20:43:51 +00:00
last_session = Session.objects.get(id=last_session_id)
session = SessionForm(
{
"purchase": last_session.purchase.id,
"timestamp_start": now_with_tz(),
"device": last_session.device,
}
)
session.save()
return redirect("list_sessions")
2023-09-17 15:17:22 +00:00
# def delete_session(request, session_id=None):
# session = Session.objects.get(id=session_id)
# session.delete()
# return redirect("list_sessions")
2023-01-04 19:28:07 +00:00
def list_sessions(
request,
filter="",
purchase_id="",
platform_id="",
game_id="",
edition_id="",
ownership_type: str = "",
):
2022-12-31 13:18:27 +00:00
context = {}
context["title"] = "Sessions"
2023-01-03 18:03:30 +00:00
if filter == "purchase":
2023-01-03 18:03:30 +00:00
dataset = Session.objects.filter(purchase=purchase_id)
context["purchase"] = Purchase.objects.get(id=purchase_id)
elif filter == "platform":
dataset = Session.objects.filter(purchase__platform=platform_id)
context["platform"] = Platform.objects.get(id=platform_id)
2023-02-18 19:49:46 +00:00
elif filter == "edition":
dataset = Session.objects.filter(purchase__edition=edition_id)
context["edition"] = Edition.objects.get(id=edition_id)
elif filter == "game":
dataset = Session.objects.filter(purchase__edition__game=game_id)
context["game"] = Game.objects.get(id=game_id)
elif filter == "ownership_type":
dataset = Session.objects.filter(purchase__ownership_type=ownership_type)
context["ownership_type"] = dict(Purchase.OWNERSHIP_TYPES)[ownership_type]
elif filter == "recent":
current_year = datetime.now().year
first_day_of_year = datetime(current_year, 1, 1)
dataset = Session.objects.filter(
timestamp_start__gte=first_day_of_year
2023-01-31 15:37:44 +00:00
).order_by("-timestamp_start")
context["title"] = "This year"
2023-01-03 18:03:30 +00:00
else:
# by default, sort from newest to oldest
dataset = Session.objects.all().order_by("-timestamp_start")
2023-01-03 18:03:30 +00:00
2023-01-04 16:27:54 +00:00
for session in dataset:
if session.timestamp_end == None and session.duration_manual == timedelta(
seconds=0
):
2023-01-04 16:27:54 +00:00
session.timestamp_end = datetime.now(ZoneInfo(settings.TIME_ZONE))
session.unfinished = True
context["total_duration"] = dataset.total_duration_formatted()
2022-12-31 13:18:27 +00:00
context["dataset"] = dataset
# cannot use dataset[0] here because that might be only partial QuerySet
context["last"] = Session.objects.all().order_by("timestamp_start").last()
2022-12-31 13:18:27 +00:00
return render(request, "list_sessions.html", context)
2023-11-02 08:20:09 +00:00
def stats(request, year: int = 0):
selected_year = request.GET.get("year")
if selected_year:
return HttpResponseRedirect(reverse("stats_by_year", args=[selected_year]))
if year == 0:
year = now_with_tz().year
2023-11-09 18:35:57 +00:00
this_year_sessions = Session.objects.filter(timestamp_start__year=year)
selected_currency = "CZK"
unique_days = (
2023-11-09 18:35:57 +00:00
this_year_sessions.annotate(date=TruncDate("timestamp_start"))
.values("date")
.distinct()
.aggregate(dates=Count("date"))
)
2023-11-09 18:35:57 +00:00
this_year_played_purchases = Purchase.objects.filter(
session__in=this_year_sessions
2023-11-02 19:12:32 +00:00
).distinct()
2023-11-09 18:35:57 +00:00
this_year_purchases = Purchase.objects.filter(date_purchased__year=year)
this_year_purchases_with_currency = this_year_purchases.filter(
price_currency__exact=selected_currency
2023-11-02 19:12:32 +00:00
)
2023-11-09 18:35:57 +00:00
this_year_purchases_without_refunded = this_year_purchases_with_currency.filter(
date_refunded=None
)
2023-11-09 18:35:57 +00:00
this_year_purchases_refunded = this_year_purchases_with_currency.refunded()
2023-11-09 18:35:57 +00:00
this_year_purchases_unfinished = this_year_purchases_without_refunded.filter(
date_finished__isnull=True
2023-11-14 18:27:00 +00:00
).filter(
type=Purchase.GAME
) # do not count DLC etc.
2023-11-09 09:06:14 +00:00
2023-11-09 18:35:57 +00:00
this_year_purchases_unfinished_percent = int(
2023-11-09 09:06:14 +00:00
safe_division(
2023-11-09 18:35:57 +00:00
this_year_purchases_unfinished.count(), this_year_purchases_refunded.count()
2023-11-09 08:18:49 +00:00
)
2023-11-09 09:06:14 +00:00
* 100
)
2023-11-02 19:12:32 +00:00
2023-11-09 18:35:57 +00:00
purchases_finished_this_year = Purchase.objects.filter(date_finished__year=year)
purchases_finished_this_year_released_this_year = (
purchases_finished_this_year.filter(edition__year_released=year).order_by(
"date_finished"
)
)
purchased_this_year_finished_this_year = (
2023-11-09 18:35:57 +00:00
this_year_purchases_without_refunded.intersection(
purchases_finished_this_year
).order_by("date_finished")
)
2023-11-09 18:35:57 +00:00
this_year_spendings = this_year_purchases_without_refunded.aggregate(
total_spent=Sum(F("price"))
)
2023-11-12 07:01:12 +00:00
total_spent = this_year_spendings["total_spent"] or 0
2023-11-02 14:08:11 +00:00
games_with_playtime = (
2023-11-09 18:35:57 +00:00
Game.objects.filter(edition__purchase__session__in=this_year_sessions)
2023-11-02 14:08:11 +00:00
.annotate(
total_playtime=Sum(
F("edition__purchase__session__duration_calculated")
+ F("edition__purchase__session__duration_manual")
)
2023-11-01 19:18:39 +00:00
)
2023-11-02 14:08:11 +00:00
.values("id", "name", "total_playtime")
2023-11-01 19:18:39 +00:00
)
2023-11-02 14:08:11 +00:00
top_10_games_by_playtime = games_with_playtime.order_by("-total_playtime")[:10]
for game in top_10_games_by_playtime:
game["formatted_playtime"] = format_duration(game["total_playtime"], "%2.0H")
2023-11-01 19:18:39 +00:00
total_playtime_per_platform = (
2023-11-09 18:35:57 +00:00
this_year_sessions.values("purchase__platform__name")
2023-11-02 14:09:31 +00:00
.annotate(total_playtime=Sum(F("duration_calculated") + F("duration_manual")))
.annotate(platform_name=F("purchase__platform__name"))
.values("platform_name", "total_playtime")
.order_by("-total_playtime")
2023-11-01 19:18:39 +00:00
)
for item in total_playtime_per_platform:
item["formatted_playtime"] = format_duration(item["total_playtime"], "%2.0H")
2023-11-09 18:15:49 +00:00
backlog_decrease_count = (
Purchase.objects.filter(date_purchased__year__lt=year)
2023-11-09 18:35:57 +00:00
.intersection(purchases_finished_this_year)
2023-11-09 18:15:49 +00:00
.count()
)
2023-11-01 19:18:39 +00:00
context = {
"total_hours": format_duration(
2023-11-09 18:35:57 +00:00
this_year_sessions.total_duration_unformatted(), "%2.0H"
2023-11-01 19:18:39 +00:00
),
2023-11-09 18:35:57 +00:00
"total_games": this_year_played_purchases.count(),
"total_2023_games": this_year_played_purchases.filter(
2023-11-02 19:12:32 +00:00
edition__year_released=year
).count(),
2023-11-02 14:08:11 +00:00
"top_10_games_by_playtime": top_10_games_by_playtime,
2023-11-01 19:18:39 +00:00
"year": year,
"total_playtime_per_platform": total_playtime_per_platform,
2023-11-02 19:12:32 +00:00
"total_spent": total_spent,
"total_spent_currency": selected_currency,
2023-11-09 18:35:57 +00:00
"all_purchased_this_year": this_year_purchases_without_refunded,
"spent_per_game": int(
2023-11-09 18:35:57 +00:00
safe_division(total_spent, this_year_purchases_without_refunded.count())
),
2023-11-09 20:43:17 +00:00
"all_finished_this_year": purchases_finished_this_year.order_by(
"date_finished"
),
"this_year_finished_this_year": purchases_finished_this_year_released_this_year.order_by(
"date_finished"
),
"purchased_this_year_finished_this_year": purchased_this_year_finished_this_year.order_by(
"date_finished"
),
2023-11-09 18:35:57 +00:00
"total_sessions": this_year_sessions.count(),
"unique_days": unique_days["dates"],
"unique_days_percent": int(unique_days["dates"] / 365 * 100),
2023-11-09 18:35:57 +00:00
"purchased_unfinished": this_year_purchases_unfinished,
"unfinished_purchases_percent": this_year_purchases_unfinished_percent,
"refunded_percent": int(
2023-11-09 09:06:14 +00:00
safe_division(
2023-11-09 18:35:57 +00:00
this_year_purchases_refunded.count(),
this_year_purchases_with_currency.count(),
2023-11-09 09:06:14 +00:00
)
* 100
),
2023-11-09 18:35:57 +00:00
"all_purchased_refunded_this_year": this_year_purchases_refunded,
"all_purchased_this_year": this_year_purchases_with_currency.order_by(
"date_purchased"
),
2023-11-09 18:15:49 +00:00
"backlog_decrease_count": backlog_decrease_count,
2023-11-01 19:18:39 +00:00
}
request.session["return_path"] = request.path
2023-11-01 19:18:39 +00:00
return render(request, "stats.html", context)
2023-11-09 20:01:01 +00:00
def add_purchase(request, edition_id=None):
2022-12-31 13:18:27 +00:00
context = {}
2023-11-09 20:01:01 +00:00
initial = {"date_purchased": now_with_tz()}
if request.method == "POST":
form = PurchaseForm(request.POST or None, initial=initial)
if form.is_valid():
purchase = form.save()
if "submit_and_redirect" in request.POST:
return HttpResponseRedirect(
reverse(
"add_session_for_purchase", kwargs={"purchase_id": purchase.id}
)
)
else:
return redirect("index")
else:
if edition_id:
edition = Edition.objects.get(id=edition_id)
form = PurchaseForm(
initial={
**initial,
"edition": edition,
"platform": edition.platform,
}
)
else:
form = PurchaseForm(initial=initial)
2022-12-31 13:18:27 +00:00
context["form"] = form
2023-01-03 21:04:36 +00:00
context["title"] = "Add New Purchase"
context["script_name"] = "add_purchase.js"
2023-11-09 20:01:01 +00:00
return render(request, "add_purchase.html", context)
2022-12-31 13:18:27 +00:00
def add_game(request):
context = {}
form = GameForm(request.POST or None)
if form.is_valid():
2023-11-09 20:01:01 +00:00
game = form.save()
if "submit_and_redirect" in request.POST:
return HttpResponseRedirect(
reverse("add_edition_for_game", kwargs={"game_id": game.id})
)
else:
return redirect("index")
2022-12-31 13:18:27 +00:00
context["form"] = form
context["title"] = "Add New Game"
context["script_name"] = "add_game.js"
2023-11-09 20:01:01 +00:00
return render(request, "add_game.html", context)
2023-01-04 16:22:36 +00:00
2023-01-04 16:23:34 +00:00
2023-11-09 20:01:01 +00:00
def add_edition(request, game_id=None):
2023-02-18 19:49:46 +00:00
context = {}
2023-11-09 20:01:01 +00:00
if request.method == "POST":
form = EditionForm(request.POST or None)
if form.is_valid():
edition = form.save()
if "submit_and_redirect" in request.POST:
return HttpResponseRedirect(
reverse(
"add_purchase_for_edition", kwargs={"edition_id": edition.id}
)
)
else:
return redirect("index")
else:
if game_id:
game = Game.objects.get(id=game_id)
form = EditionForm(
initial={
"game": game,
"name": game.name,
"sort_name": game.sort_name,
"year_released": game.year_released,
}
2023-11-09 20:01:01 +00:00
)
else:
form = EditionForm()
2023-02-18 19:49:46 +00:00
context["form"] = form
context["title"] = "Add New Edition"
context["script_name"] = "add_edition.js"
2023-11-09 20:01:01 +00:00
return render(request, "add_edition.html", context)
2023-02-18 19:49:46 +00:00
2023-01-04 16:23:34 +00:00
def add_platform(request):
context = {}
form = PlatformForm(request.POST or None)
if form.is_valid():
form.save()
2023-02-18 19:50:36 +00:00
return redirect("index")
2023-01-04 16:23:34 +00:00
context["form"] = form
context["title"] = "Add New Platform"
return render(request, "add.html", context)
2023-02-18 19:50:36 +00:00
2023-02-18 20:12:18 +00:00
def add_device(request):
context = {}
form = DeviceForm(request.POST or None)
if form.is_valid():
form.save()
return redirect("index")
context["form"] = form
context["title"] = "Add New Device"
return render(request, "add.html", context)
2023-02-18 19:50:36 +00:00
def index(request):
return redirect("list_sessions_recent")