Compare commits

..

No commits in common. "394dd4f9f87f3f4862eb900a9c614468eb2b55b0" and "b0be7b5887a875726e188fbfc9b852a377cf79c2" have entirely different histories.

9 changed files with 75 additions and 236 deletions

View File

@ -1,4 +1,4 @@
## 1.4.0 / 2023-11-09 21:01+01:00 ## Unreleased
### New ### New
* More fields are now optional. This is to make it easier to add new items in bulk. * More fields are now optional. This is to make it easier to add new items in bulk.
@ -22,9 +22,6 @@
* Finished (count) * Finished (count)
* Unfinished (count) * Unfinished (count)
* Refunded (count) * Refunded (count)
* Backlog Decrease (count)
* New workflow:
* Adding Game, Edition, Purchase, and Session in a row is now much faster
### Improved ### Improved
* game overview: simplify playtime range display * game overview: simplify playtime range display

View File

@ -6,7 +6,7 @@ RUN npm install && \
FROM python:3.10.9-slim-bullseye FROM python:3.10.9-slim-bullseye
ENV VERSION_NUMBER 1.4.0 ENV VERSION_NUMBER 1.3.0
ENV PROD 1 ENV PROD 1
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1

View File

@ -1,29 +0,0 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock title %}
{% block content %}
<form method="post" enctype="multipart/form-data">
<table class="mx-auto">
{% csrf_token %}
{{ form.as_table }}
<tr>
<td></td>
<td><input type="submit" name="submit" value="Submit"/></td>
</tr>
<tr>
<td></td>
<td><input type="submit" name="submit_and_redirect" value="Submit & Create Purchase"/></td>
</tr>
</table>
</form>
{% endblock content %}
{% block scripts %}
{% if script_name %}
<script type="module" src="{% static 'js/'|add:script_name %}"></script>
{% endif %}
{% endblock scripts %}

View File

@ -1,29 +0,0 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock title %}
{% block content %}
<form method="post" enctype="multipart/form-data">
<table class="mx-auto">
{% csrf_token %}
{{ form.as_table }}
<tr>
<td></td>
<td><input type="submit" name="submit" value="Submit"/></td>
</tr>
<tr>
<td></td>
<td><input type="submit" name="submit_and_redirect" value="Submit & Create Edition"/></td>
</tr>
</table>
</form>
{% endblock content %}
{% block scripts %}
{% if script_name %}
<script type="module" src="{% static 'js/'|add:script_name %}"></script>
{% endif %}
{% endblock scripts %}

View File

@ -1,29 +0,0 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock title %}
{% block content %}
<form method="post" enctype="multipart/form-data">
<table class="mx-auto">
{% csrf_token %}
{{ form.as_table }}
<tr>
<td></td>
<td><input type="submit" name="submit" value="Submit"/></td>
</tr>
<tr>
<td></td>
<td><input type="submit" name="submit_and_redirect" value="Submit & Create Session"/></td>
</tr>
</table>
</form>
{% endblock content %}
{% block scripts %}
{% if script_name %}
<script type="module" src="{% static 'js/'|add:script_name %}"></script>
{% endif %}
{% endblock scripts %}

View File

@ -64,10 +64,6 @@
<td class="px-2 sm:px-4 md:px-6 md:py-2">Unfinished</td> <td class="px-2 sm:px-4 md:px-6 md:py-2">Unfinished</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchased_unfinished.count }} ({{ unfinished_purchases_percent }}%)</td> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchased_unfinished.count }} ({{ unfinished_purchases_percent }}%)</td>
</tr> </tr>
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Backlog Decrease</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ backlog_decrease_count }}</td>
</tr>
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Spendings ({{ total_spent_currency }})</td> <td class="px-2 sm:px-4 md:px-6 md:py-2">Spendings ({{ total_spent_currency }})</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_spent }} ({{ spent_per_game }}/game)</td> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_spent }} ({{ spent_per_game }}/game)</td>

View File

@ -11,14 +11,8 @@ urlpatterns = [
name="list_sessions_recent", name="list_sessions_recent",
), ),
path("add-game/", views.add_game, name="add_game"), path("add-game/", views.add_game, name="add_game"),
path("add-game-unified/", views.add_game_unified, name="add_game_unified"),
path("add-platform/", views.add_platform, name="add_platform"), path("add-platform/", views.add_platform, name="add_platform"),
path("add-session/", views.add_session, name="add_session"), path("add-session/", views.add_session, name="add_session"),
path(
"add-session-for-purchase/<int:purchase_id>",
views.add_session,
name="add_session_for_purchase",
),
path( path(
"update-session/by-session/<int:session_id>", "update-session/by-session/<int:session_id>",
views.update_session, views.update_session,
@ -40,17 +34,7 @@ urlpatterns = [
# name="delete_session", # name="delete_session",
# ), # ),
path("add-purchase/", views.add_purchase, name="add_purchase"), path("add-purchase/", views.add_purchase, name="add_purchase"),
path(
"add-purchase-for-edition/<int:edition_id>",
views.add_purchase,
name="add_purchase_for_edition",
),
path("add-edition/", views.add_edition, name="add_edition"), path("add-edition/", views.add_edition, name="add_edition"),
path(
"add-edition-for-game/<int:game_id>",
views.add_edition,
name="add_edition_for_game",
),
path("edit-edition/<int:edition_id>", views.edit_edition, name="edit_edition"), path("edit-edition/<int:edition_id>", views.edit_edition, name="edit_edition"),
path("game/<int:game_id>/view", views.view_game, name="view_game"), path("game/<int:game_id>/view", views.view_game, name="view_game"),
path("game/<int:game_id>/edit", views.edit_game, name="edit_game"), path("game/<int:game_id>/edit", views.edit_game, name="edit_game"),

View File

@ -35,30 +35,21 @@ def stats_dropdown_year_range(request):
return {"stats_dropdown_year_range": range(2018, 2024)} return {"stats_dropdown_year_range": range(2018, 2024)}
def add_session(request, purchase_id=None): def add_session(request):
context = {} context = {}
initial = {"timestamp_start": now_with_tz()} initial = {}
now = now_with_tz()
initial["timestamp_start"] = now
last = Session.objects.all().last() last = Session.objects.all().last()
if last != None: if last != None:
initial["purchase"] = last.purchase initial["purchase"] = last.purchase
if request.method == "POST": form = SessionForm(request.POST or None, initial=initial)
form = SessionForm(request.POST or None, initial=initial) if form.is_valid():
if form.is_valid(): form.save()
form.save() return redirect("list_sessions")
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)
context["title"] = "Add New Session" context["title"] = "Add New Session"
context["form"] = form context["form"] = form
@ -284,57 +275,65 @@ def stats(request, year: int = 0):
return HttpResponseRedirect(reverse("stats_by_year", args=[selected_year])) return HttpResponseRedirect(reverse("stats_by_year", args=[selected_year]))
if year == 0: if year == 0:
year = now_with_tz().year year = now_with_tz().year
this_year_sessions = Session.objects.filter(timestamp_start__year=year) first_day_of_year = datetime(year, 1, 1)
selected_currency = "CZK" last_day_of_year = datetime(year + 1, 1, 1)
year_sessions = Session.objects.filter(timestamp_start__year=year)
unique_days = ( unique_days = (
this_year_sessions.annotate(date=TruncDate("timestamp_start")) year_sessions.annotate(date=TruncDate("timestamp_start"))
.values("date") .values("date")
.distinct() .distinct()
.aggregate(dates=Count("date")) .aggregate(dates=Count("date"))
) )
this_year_played_purchases = Purchase.objects.filter( year_played_purchases = Purchase.objects.filter(
session__in=this_year_sessions session__in=year_sessions
).distinct() ).distinct()
this_year_purchases = Purchase.objects.filter(date_purchased__year=year) selected_currency = "CZK"
this_year_purchases_with_currency = this_year_purchases.filter( all_purchased_this_year = (
price_currency__exact=selected_currency Purchase.objects.filter(date_purchased__year=year)
.filter(price_currency__exact=selected_currency)
.order_by("date_purchased")
) )
this_year_purchases_without_refunded = this_year_purchases_with_currency.filter( all_purchased_without_refunded_this_year = all_purchased_this_year.not_refunded()
date_refunded=None all_purchased_refunded_this_year = (
Purchase.objects.filter(date_purchased__year=year)
.filter(price_currency__exact=selected_currency)
.refunded()
.order_by("date_purchased")
) )
this_year_purchases_refunded = this_year_purchases_with_currency.refunded()
this_year_purchases_unfinished = this_year_purchases_without_refunded.filter( purchased_unfinished = all_purchased_without_refunded_this_year.filter(
date_finished__isnull=True date_finished__isnull=True
) )
this_year_purchases_unfinished_percent = int( unfinished_purchases_percent = int(
safe_division( safe_division(
this_year_purchases_unfinished.count(), this_year_purchases_refunded.count() purchased_unfinished.count(), all_purchased_refunded_this_year.count()
) )
* 100 * 100
) )
purchases_finished_this_year = Purchase.objects.filter(date_finished__year=year) all_finished_this_year = Purchase.objects.filter(date_finished__year=year).order_by(
purchases_finished_this_year_released_this_year = ( "date_finished"
purchases_finished_this_year.filter(edition__year_released=year).order_by( )
"date_finished" this_year_finished_this_year = (
) Purchase.objects.filter(date_finished__year=year)
.filter(edition__year_released=year)
.order_by("date_finished")
) )
purchased_this_year_finished_this_year = ( purchased_this_year_finished_this_year = (
this_year_purchases_without_refunded.intersection( all_purchased_without_refunded_this_year.filter(
purchases_finished_this_year date_finished__year=year
).order_by("date_finished") ).order_by("date_finished")
) )
this_year_spendings = this_year_purchases_without_refunded.aggregate( this_year_spendings = all_purchased_without_refunded_this_year.aggregate(
total_spent=Sum(F("price")) total_spent=Sum(F("price"))
) )
total_spent = this_year_spendings["total_spent"] total_spent = this_year_spendings["total_spent"]
games_with_playtime = ( games_with_playtime = (
Game.objects.filter(edition__purchase__session__in=this_year_sessions) Game.objects.filter(edition__purchase__session__in=year_sessions)
.annotate( .annotate(
total_playtime=Sum( total_playtime=Sum(
F("edition__purchase__session__duration_calculated") F("edition__purchase__session__duration_calculated")
@ -348,7 +347,7 @@ def stats(request, year: int = 0):
game["formatted_playtime"] = format_duration(game["total_playtime"], "%2.0H") game["formatted_playtime"] = format_duration(game["total_playtime"], "%2.0H")
total_playtime_per_platform = ( total_playtime_per_platform = (
this_year_sessions.values("purchase__platform__name") year_sessions.values("purchase__platform__name")
.annotate(total_playtime=Sum(F("duration_calculated") + F("duration_manual"))) .annotate(total_playtime=Sum(F("duration_calculated") + F("duration_manual")))
.annotate(platform_name=F("purchase__platform__name")) .annotate(platform_name=F("purchase__platform__name"))
.values("platform_name", "total_playtime") .values("platform_name", "total_playtime")
@ -357,18 +356,12 @@ def stats(request, year: int = 0):
for item in total_playtime_per_platform: for item in total_playtime_per_platform:
item["formatted_playtime"] = format_duration(item["total_playtime"], "%2.0H") 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)
.count()
)
context = { context = {
"total_hours": format_duration( "total_hours": format_duration(
this_year_sessions.total_duration_unformatted(), "%2.0H" year_sessions.total_duration_unformatted(), "%2.0H"
), ),
"total_games": this_year_played_purchases.count(), "total_games": year_played_purchases.count(),
"total_2023_games": this_year_played_purchases.filter( "total_2023_games": year_played_purchases.filter(
edition__year_released=year edition__year_released=year
).count(), ).count(),
"top_10_games_by_playtime": top_10_games_by_playtime, "top_10_games_by_playtime": top_10_games_by_playtime,
@ -376,116 +369,72 @@ def stats(request, year: int = 0):
"total_playtime_per_platform": total_playtime_per_platform, "total_playtime_per_platform": total_playtime_per_platform,
"total_spent": total_spent, "total_spent": total_spent,
"total_spent_currency": selected_currency, "total_spent_currency": selected_currency,
"all_purchased_this_year": this_year_purchases_without_refunded, "all_purchased_this_year": all_purchased_without_refunded_this_year,
"spent_per_game": int( "spent_per_game": int(
safe_division(total_spent, this_year_purchases_without_refunded.count()) safe_division(total_spent, all_purchased_without_refunded_this_year.count())
), ),
"all_finished_this_year": purchases_finished_this_year, "all_finished_this_year": all_finished_this_year,
"this_year_finished_this_year": purchases_finished_this_year_released_this_year, "this_year_finished_this_year": this_year_finished_this_year,
"purchased_this_year_finished_this_year": purchased_this_year_finished_this_year, "purchased_this_year_finished_this_year": purchased_this_year_finished_this_year,
"total_sessions": this_year_sessions.count(), "total_sessions": year_sessions.count(),
"unique_days": unique_days["dates"], "unique_days": unique_days["dates"],
"unique_days_percent": int(unique_days["dates"] / 365 * 100), "unique_days_percent": int(unique_days["dates"] / 365 * 100),
"purchased_unfinished": this_year_purchases_unfinished, "purchased_unfinished": purchased_unfinished,
"unfinished_purchases_percent": this_year_purchases_unfinished_percent, "unfinished_purchases_percent": unfinished_purchases_percent,
"refunded_percent": int( "refunded_percent": int(
safe_division( safe_division(
this_year_purchases_refunded.count(), all_purchased_refunded_this_year.count(),
this_year_purchases_with_currency.count(), all_purchased_this_year.count(),
) )
* 100 * 100
), ),
"all_purchased_refunded_this_year": this_year_purchases_refunded, "all_purchased_refunded_this_year": all_purchased_refunded_this_year,
"all_purchased_this_year": this_year_purchases_with_currency.order_by( "all_purchased_this_year": all_purchased_this_year,
"date_purchased"
),
"backlog_decrease_count": backlog_decrease_count,
} }
request.session["return_path"] = request.path request.session["return_path"] = request.path
return render(request, "stats.html", context) return render(request, "stats.html", context)
def add_purchase(request, edition_id=None): def add_purchase(request):
context = {} context = {}
initial = {"date_purchased": now_with_tz()} now = datetime.now()
initial = {"date_purchased": now}
if request.method == "POST": form = PurchaseForm(request.POST or None, initial=initial)
form = PurchaseForm(request.POST or None, initial=initial) if form.is_valid():
if form.is_valid(): form.save()
purchase = form.save() return redirect("index")
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)
context["form"] = form context["form"] = form
context["title"] = "Add New Purchase" context["title"] = "Add New Purchase"
context["script_name"] = "add_purchase.js" context["script_name"] = "add_purchase.js"
return render(request, "add_purchase.html", context) return render(request, "add.html", context)
def add_game(request): def add_game(request):
context = {} context = {}
form = GameForm(request.POST or None) form = GameForm(request.POST or None)
if form.is_valid(): if form.is_valid():
game = form.save() form.save()
if "submit_and_redirect" in request.POST: return redirect("index")
return HttpResponseRedirect(
reverse("add_edition_for_game", kwargs={"game_id": game.id})
)
else:
return redirect("index")
context["form"] = form context["form"] = form
context["title"] = "Add New Game" context["title"] = "Add New Game"
context["script_name"] = "add_game.js" context["script_name"] = "add_game.js"
return render(request, "add_game.html", context) return render(request, "add.html", context)
def add_edition(request, game_id=None): def add_edition(request):
context = {} context = {}
if request.method == "POST": form = EditionForm(request.POST or None)
form = EditionForm(request.POST or None) if form.is_valid():
if form.is_valid(): form.save()
edition = form.save() return redirect("index")
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}
)
else:
form = EditionForm()
context["form"] = form context["form"] = form
context["title"] = "Add New Edition" context["title"] = "Add New Edition"
context["script_name"] = "add_edition.js" context["script_name"] = "add_edition.js"
return render(request, "add_edition.html", context) return render(request, "add.html", context)
def add_platform(request): def add_platform(request):

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "timetracker" name = "timetracker"
version = "1.4.0" version = "1.3.0"
description = "A simple time tracker." description = "A simple time tracker."
authors = ["Lukáš Kucharczyk <lukas@kucharczyk.xyz>"] authors = ["Lukáš Kucharczyk <lukas@kucharczyk.xyz>"]
license = "GPL" license = "GPL"