Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
f31280c682 | |||
a745d16ec3 | |||
ae079e36ec | |||
c8a3212b77 | |||
d211326c3f | |||
270a291f05 | |||
13b750ca92 | |||
015b6db2f7 | |||
667b161fff | |||
5958cbf4a6 | |||
3b37f2c3f0 | |||
4517ff2b5a | |||
884ce13e26 | |||
dd219bae9d | |||
60d29090a1 |
@ -10,6 +10,7 @@ steps:
|
||||
- python -m pip install poetry
|
||||
- poetry install
|
||||
- poetry env info
|
||||
- poetry run python manage.py migrate
|
||||
- poetry run pytest
|
||||
|
||||
- name: build-prod
|
||||
|
13
CHANGELOG.md
13
CHANGELOG.md
@ -1,3 +1,16 @@
|
||||
## 1.5.0 / 2023-11-14 19:27+01:00
|
||||
|
||||
## New
|
||||
* Add stat for finished this year's games
|
||||
* Add purchase types:
|
||||
* Game (previously all of them were this type)
|
||||
* DLC
|
||||
* Season Pass
|
||||
* Battle Pass
|
||||
|
||||
## Fixed
|
||||
* Order purchases by date on game view
|
||||
|
||||
## 1.4.0 / 2023-11-09 21:01+01:00
|
||||
|
||||
### New
|
||||
|
@ -6,7 +6,7 @@ RUN npm install && \
|
||||
|
||||
FROM python:3.10.9-slim-bullseye
|
||||
|
||||
ENV VERSION_NUMBER 1.4.0
|
||||
ENV VERSION_NUMBER 1.5.0
|
||||
ENV PROD 1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
|
@ -66,6 +66,12 @@ textarea {
|
||||
@apply dark:border dark:border-slate-900 dark:bg-slate-500 dark:text-slate-100;
|
||||
}
|
||||
|
||||
form input:disabled,
|
||||
select:disabled,
|
||||
textarea:disabled {
|
||||
@apply dark:bg-slate-700 dark:text-slate-400;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
form input,
|
||||
select,
|
||||
|
@ -44,7 +44,7 @@ def format_duration(
|
||||
# timestamps where end is before start
|
||||
if seconds_total < 0:
|
||||
seconds_total = 0
|
||||
days = hours = minutes = seconds = 0
|
||||
days = hours = hours_float = minutes = seconds = 0
|
||||
remainder = seconds = seconds_total
|
||||
if "%d" in format_string:
|
||||
days, remainder = divmod(seconds_total, day_seconds)
|
||||
@ -55,7 +55,7 @@ def format_duration(
|
||||
minutes, seconds = divmod(remainder, minute_seconds)
|
||||
literals = {
|
||||
"d": str(days),
|
||||
"H": str(hours),
|
||||
"H": str(hours) if "m" not in format_string else str(hours_float),
|
||||
"m": str(minutes),
|
||||
"s": str(seconds),
|
||||
"r": str(seconds_total),
|
||||
|
@ -55,6 +55,9 @@ class PurchaseForm(forms.ModelForm):
|
||||
widget=IncludePlatformSelect(attrs={"autoselect": "autoselect"}),
|
||||
)
|
||||
platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name"))
|
||||
related_purchase = forms.ModelChoiceField(
|
||||
queryset=Purchase.objects.order_by("edition__sort_name")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
widgets = {
|
||||
@ -72,6 +75,9 @@ class PurchaseForm(forms.ModelForm):
|
||||
"price",
|
||||
"price_currency",
|
||||
"ownership_type",
|
||||
"type",
|
||||
"related_purchase",
|
||||
"name",
|
||||
]
|
||||
|
||||
|
||||
|
27
games/migrations/0026_purchase_type.py
Normal file
27
games/migrations/0026_purchase_type.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Generated by Django 4.1.5 on 2023-11-14 08:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0025_game_sort_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="purchase",
|
||||
name="type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("game", "Game"),
|
||||
("dlc", "DLC"),
|
||||
("season_pass", "Season Pass"),
|
||||
("battle_pass", "Battle Pass"),
|
||||
],
|
||||
default="game",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
]
|
25
games/migrations/0027_purchase_related_purchase.py
Normal file
25
games/migrations/0027_purchase_related_purchase.py
Normal file
@ -0,0 +1,25 @@
|
||||
# Generated by Django 4.1.5 on 2023-11-14 08:41
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0026_purchase_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="purchase",
|
||||
name="related_purchase",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="games.purchase",
|
||||
),
|
||||
),
|
||||
]
|
25
games/migrations/0028_purchase_name.py
Normal file
25
games/migrations/0028_purchase_name.py
Normal file
@ -0,0 +1,25 @@
|
||||
# Generated by Django 4.1.5 on 2023-11-14 11:05
|
||||
|
||||
from django.db import migrations, models
|
||||
from games.models import Purchase
|
||||
|
||||
|
||||
def null_game_name(apps, schema_editor):
|
||||
Purchase.objects.filter(type=Purchase.GAME).update(name=None)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("games", "0027_purchase_related_purchase"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="purchase",
|
||||
name="name",
|
||||
field=models.CharField(
|
||||
blank=True, default="Unknown Name", max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.RunPython(null_game_name),
|
||||
]
|
@ -71,6 +71,9 @@ class PurchaseQueryset(models.QuerySet):
|
||||
def finished(self):
|
||||
return self.filter(date_finished__isnull=False)
|
||||
|
||||
def games_only(self):
|
||||
return self.filter(type=Purchase.GAME)
|
||||
|
||||
|
||||
class Purchase(models.Model):
|
||||
PHYSICAL = "ph"
|
||||
@ -91,6 +94,16 @@ class Purchase(models.Model):
|
||||
(DEMO, "Demo"),
|
||||
(PIRATED, "Pirated"),
|
||||
]
|
||||
GAME = "game"
|
||||
DLC = "dlc"
|
||||
SEASONPASS = "season_pass"
|
||||
BATTLEPASS = "battle_pass"
|
||||
TYPES = [
|
||||
(GAME, "Game"),
|
||||
(DLC, "DLC"),
|
||||
(SEASONPASS, "Season Pass"),
|
||||
(BATTLEPASS, "Battle Pass"),
|
||||
]
|
||||
|
||||
objects = PurchaseQueryset().as_manager()
|
||||
|
||||
@ -106,6 +119,13 @@ class Purchase(models.Model):
|
||||
ownership_type = models.CharField(
|
||||
max_length=2, choices=OWNERSHIP_TYPES, default=DIGITAL
|
||||
)
|
||||
type = models.CharField(max_length=255, choices=TYPES, default=GAME)
|
||||
name = models.CharField(
|
||||
max_length=255, default="Unknown Name", null=True, blank=True
|
||||
)
|
||||
related_purchase = models.ForeignKey(
|
||||
"Purchase", on_delete=models.SET_NULL, default=None, null=True, blank=True
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
platform_info = self.platform
|
||||
@ -113,6 +133,14 @@ class Purchase(models.Model):
|
||||
platform_info = f"{self.edition.platform} version on {self.platform}"
|
||||
return f"{self.edition} ({platform_info}, {self.edition.year_released}, {self.get_ownership_type_display()})"
|
||||
|
||||
def is_game(self):
|
||||
return self.type == self.GAME
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.type == Purchase.GAME:
|
||||
self.name = ""
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Platform(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
@ -151,7 +179,7 @@ class Session(models.Model):
|
||||
objects = SessionQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
mark = ", manual" if self.duration_manual != None else ""
|
||||
mark = ", manual" if self.is_manual() else ""
|
||||
return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.duration_formatted()}{mark})"
|
||||
|
||||
def finish_now(self):
|
||||
@ -163,7 +191,7 @@ class Session(models.Model):
|
||||
def duration_seconds(self) -> timedelta:
|
||||
manual = timedelta(0)
|
||||
calculated = timedelta(0)
|
||||
if not self.duration_manual in (None, 0, timedelta(0)):
|
||||
if self.is_manual():
|
||||
manual = self.duration_manual
|
||||
if self.timestamp_end != None and self.timestamp_start != None:
|
||||
calculated = self.timestamp_end - self.timestamp_start
|
||||
@ -173,6 +201,9 @@ class Session(models.Model):
|
||||
result = format_duration(self.duration_seconds(), "%02.0H:%02.0m")
|
||||
return result
|
||||
|
||||
def is_manual(self) -> bool:
|
||||
return not self.duration_manual == timedelta(0)
|
||||
|
||||
@property
|
||||
def duration_sum(self) -> str:
|
||||
return Session.objects.all().total_duration_formatted()
|
||||
|
@ -1222,6 +1222,15 @@ textarea) {
|
||||
color: rgb(241 245 249 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
:is(.dark form input:disabled),:is(.dark
|
||||
select:disabled),:is(.dark
|
||||
textarea:disabled) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(51 65 85 / var(--tw-bg-opacity));
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(148 163 184 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
form input,
|
||||
select,
|
||||
@ -1428,6 +1437,10 @@ th label {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.sm\:pl-6 {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.sm\:decoration-2 {
|
||||
text-decoration-thickness: 2px;
|
||||
}
|
||||
|
@ -1,12 +1,31 @@
|
||||
import { syncSelectInputUntilChanged } from './utils.js'
|
||||
import { syncSelectInputUntilChanged, getEl, conditionalElementHandler } from "./utils.js";
|
||||
|
||||
let syncData = [
|
||||
{
|
||||
"source": "#id_edition",
|
||||
"source_value": "dataset.platform",
|
||||
"target": "#id_platform",
|
||||
"target_value": "value"
|
||||
source: "#id_edition",
|
||||
source_value: "dataset.platform",
|
||||
target: "#id_platform",
|
||||
target_value: "value",
|
||||
},
|
||||
];
|
||||
|
||||
syncSelectInputUntilChanged(syncData, "form");
|
||||
|
||||
|
||||
let myConfig = [
|
||||
() => {
|
||||
return getEl("#id_type").value == "game";
|
||||
},
|
||||
["#id_name", "#id_related_purchase"],
|
||||
(el) => {
|
||||
el.disabled = "disabled";
|
||||
},
|
||||
(el) => {
|
||||
el.disabled = "";
|
||||
}
|
||||
]
|
||||
|
||||
syncSelectInputUntilChanged(syncData, "form")
|
||||
document.DOMContentLoaded = conditionalElementHandler(...myConfig)
|
||||
getEl("#id_type").onchange = () => {
|
||||
conditionalElementHandler(...myConfig)
|
||||
}
|
||||
|
@ -87,4 +87,49 @@ function getValueFromProperty(sourceElement, property) {
|
||||
}
|
||||
}
|
||||
|
||||
export { toISOUTCString, syncSelectInputUntilChanged };
|
||||
/**
|
||||
* @description Returns a single element by name.
|
||||
* @param {string} selector The selector to look for.
|
||||
*/
|
||||
function getEl(selector) {
|
||||
if (selector.startsWith("#")) {
|
||||
return document.getElementById(selector.slice(1))
|
||||
}
|
||||
else if (selector.startsWith(".")) {
|
||||
return document.getElementsByClassName(selector)
|
||||
}
|
||||
else {
|
||||
return document.getElementsByName(selector)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Does something to elements when something happens.
|
||||
* @param {() => boolean} condition The condition that is being tested.
|
||||
* @param {string[]} targetElements
|
||||
* @param {(elementName: HTMLElement) => void} callbackfn1 Called when the condition matches.
|
||||
* @param {(elementName: HTMLElement) => void} callbackfn2 Called when the condition doesn't match.
|
||||
*/
|
||||
function conditionalElementHandler(condition, targetElements, callbackfn1, callbackfn2) {
|
||||
if (condition()) {
|
||||
targetElements.forEach((elementName) => {
|
||||
let el = getEl(elementName);
|
||||
if (el === null) {
|
||||
console.error("Element ${elementName} doesn't exist.");
|
||||
} else {
|
||||
callbackfn1(el);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
targetElements.forEach((elementName) => {
|
||||
let el = getEl(elementName);
|
||||
if (el === null) {
|
||||
console.error("Element ${elementName} doesn't exist.");
|
||||
} else {
|
||||
callbackfn2(el);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { toISOUTCString, syncSelectInputUntilChanged, getEl, conditionalElementHandler };
|
||||
|
@ -45,6 +45,10 @@
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2">Finished</td>
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ all_finished_this_year.count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2">Finished ({{ year }})</td>
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ this_year_finished_this_year.count }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -177,7 +181,14 @@
|
||||
<tbody>
|
||||
{% for purchase in all_purchased_this_year %}
|
||||
<tr>
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono"><a class="underline decoration-slate-500 sm:decoration-2" href="{% url 'edit_purchase' purchase.id %}">{{ purchase.edition.name }}</a></td>
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
||||
<a class="underline decoration-slate-500 sm:decoration-2" href="{% url 'edit_purchase' purchase.id %}">
|
||||
{{ purchase.edition.name }}
|
||||
{% if purchase.type != "game" %}
|
||||
({{ purchase.name }}, {{ purchase.get_type_display }})
|
||||
{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.price }}</td>
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_purchased | date:"d/m/Y" }}</td>
|
||||
</tr>
|
||||
|
@ -42,6 +42,19 @@
|
||||
({{ purchase.get_ownership_type_display }}, {{ purchase.date_purchased | date:"Y" }}, {{ purchase.price }} {{ purchase.price_currency}})
|
||||
{% url 'edit_purchase' purchase.id as edit_url %}
|
||||
{% include 'components/edit_button.html' with edit_url=edit_url %}
|
||||
{% if purchase.related_purchases %}
|
||||
<li>
|
||||
<ul>
|
||||
{% for related_purchase in purchase.related_purchases %}
|
||||
<li class="sm:pl-6 flex items-center">
|
||||
{{ related_purchase.name}} ({{ related_purchase.get_type_display }}, {{ related_purchase.date_purchased | date:"Y" }}, {{ related_purchase.price }} {{ related_purchase.price_currency}})
|
||||
{% url 'edit_purchase' related_purchase.id as edit_url %}
|
||||
{% include 'components/edit_button.html' with edit_url=edit_url %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
@ -11,7 +11,6 @@ urlpatterns = [
|
||||
name="list_sessions_recent",
|
||||
),
|
||||
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-session/", views.add_session, name="add_session"),
|
||||
path(
|
||||
|
@ -32,7 +32,12 @@ def model_counts(request):
|
||||
|
||||
|
||||
def stats_dropdown_year_range(request):
|
||||
return {"stats_dropdown_year_range": range(2018, 2024)}
|
||||
result = {
|
||||
"stats_dropdown_year_range": range(
|
||||
datetime.now(ZoneInfo(settings.TIME_ZONE)).year, 1999, -1
|
||||
)
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def add_session(request, purchase_id=None):
|
||||
@ -113,7 +118,8 @@ def edit_purchase(request, purchase_id=None):
|
||||
return redirect("list_sessions")
|
||||
context["title"] = "Edit Purchase"
|
||||
context["form"] = form
|
||||
return render(request, "add.html", context)
|
||||
context["script_name"] = "add_purchase.js"
|
||||
return render(request, "add_purchase.html", context)
|
||||
|
||||
|
||||
@use_custom_redirect
|
||||
@ -135,7 +141,17 @@ def view_game(request, game_id=None):
|
||||
context["title"] = "View Game"
|
||||
context["game"] = game
|
||||
context["editions"] = Edition.objects.filter(game_id=game_id)
|
||||
context["purchases"] = Purchase.objects.filter(edition__game_id=game_id)
|
||||
game_purchases = (
|
||||
Purchase.objects.filter(edition__game_id=game_id)
|
||||
.filter(type=Purchase.GAME)
|
||||
.order_by("date_purchased")
|
||||
)
|
||||
for purchase in game_purchases:
|
||||
purchase.related_purchases = Purchase.objects.exclude(
|
||||
type=Purchase.GAME
|
||||
).filter(related_purchase=purchase.id)
|
||||
|
||||
context["purchases"] = game_purchases
|
||||
context["sessions"] = Session.objects.filter(
|
||||
purchase__edition__game_id=game_id
|
||||
).order_by("-timestamp_start")
|
||||
@ -307,7 +323,9 @@ def stats(request, year: int = 0):
|
||||
|
||||
this_year_purchases_unfinished = this_year_purchases_without_refunded.filter(
|
||||
date_finished__isnull=True
|
||||
)
|
||||
).filter(
|
||||
type=Purchase.GAME
|
||||
) # do not count DLC etc.
|
||||
|
||||
this_year_purchases_unfinished_percent = int(
|
||||
safe_division(
|
||||
@ -331,7 +349,7 @@ def stats(request, year: int = 0):
|
||||
this_year_spendings = this_year_purchases_without_refunded.aggregate(
|
||||
total_spent=Sum(F("price"))
|
||||
)
|
||||
total_spent = this_year_spendings["total_spent"]
|
||||
total_spent = this_year_spendings["total_spent"] or 0
|
||||
|
||||
games_with_playtime = (
|
||||
Game.objects.filter(edition__purchase__session__in=this_year_sessions)
|
||||
@ -380,9 +398,15 @@ def stats(request, year: int = 0):
|
||||
"spent_per_game": int(
|
||||
safe_division(total_spent, this_year_purchases_without_refunded.count())
|
||||
),
|
||||
"all_finished_this_year": purchases_finished_this_year,
|
||||
"this_year_finished_this_year": purchases_finished_this_year_released_this_year,
|
||||
"purchased_this_year_finished_this_year": purchased_this_year_finished_this_year,
|
||||
"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"
|
||||
),
|
||||
"total_sessions": this_year_sessions.count(),
|
||||
"unique_days": unique_days["dates"],
|
||||
"unique_days_percent": int(unique_days["dates"] / 365 * 100),
|
||||
@ -477,7 +501,12 @@ def add_edition(request, game_id=None):
|
||||
if game_id:
|
||||
game = Game.objects.get(id=game_id)
|
||||
form = EditionForm(
|
||||
initial={"game": game, "name": game.name, "sort_name": game.sort_name}
|
||||
initial={
|
||||
"game": game,
|
||||
"name": game.name,
|
||||
"sort_name": game.sort_name,
|
||||
"year_released": game.year_released,
|
||||
}
|
||||
)
|
||||
else:
|
||||
form = EditionForm()
|
||||
|
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "timetracker"
|
||||
version = "1.4.0"
|
||||
version = "1.5.0"
|
||||
description = "A simple time tracker."
|
||||
authors = ["Lukáš Kucharczyk <lukas@kucharczyk.xyz>"]
|
||||
license = "GPL"
|
||||
|
89
tests/test_paths_return_200.py
Normal file
89
tests/test_paths_return_200.py
Normal file
@ -0,0 +1,89 @@
|
||||
import django
|
||||
import os
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings")
|
||||
django.setup()
|
||||
from django.conf import settings
|
||||
|
||||
from games.models import Game, Edition, Purchase, Session, Platform
|
||||
|
||||
ZONEINFO = ZoneInfo(settings.TIME_ZONE)
|
||||
|
||||
|
||||
class PathWorksTest(TestCase):
|
||||
def setUp(self) -> None:
|
||||
pl = Platform(name="Test Platform")
|
||||
pl.save()
|
||||
g = Game(name="The Test Game")
|
||||
g.save()
|
||||
e = Edition(game=g, name="The Test Game Edition", platform=pl)
|
||||
e.save()
|
||||
p = Purchase(
|
||||
edition=e,
|
||||
platform=pl,
|
||||
date_purchased=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO),
|
||||
)
|
||||
p.save()
|
||||
s = Session(
|
||||
purchase=p,
|
||||
timestamp_start=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO),
|
||||
timestamp_end=datetime(2022, 9, 26, 17, 38, tzinfo=ZONEINFO),
|
||||
)
|
||||
s.save()
|
||||
self.testSession = s
|
||||
return super().setUp()
|
||||
|
||||
def test_add_device_returns_200(self):
|
||||
url = reverse("add_device")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_add_platform_returns_200(self):
|
||||
url = reverse("add_platform")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_add_game_returns_200(self):
|
||||
url = reverse("add_game")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_add_edition_returns_200(self):
|
||||
url = reverse("add_edition")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_add_purchase_returns_200(self):
|
||||
url = reverse("add_purchase")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_add_session_returns_200(self):
|
||||
url = reverse("add_session")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_edit_session_returns_200(self):
|
||||
id = self.testSession.id
|
||||
url = reverse("edit_session", args=[id])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_view_game_returns_200(self):
|
||||
url = reverse("view_game", args=[1])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_edit_game_returns_200(self):
|
||||
url = reverse("edit_game", args=[1])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_list_sessions_returns_200(self):
|
||||
url = reverse("list_sessions")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
38
tests/test_session_formatting.py
Normal file
38
tests/test_session_formatting.py
Normal file
@ -0,0 +1,38 @@
|
||||
import django
|
||||
import os
|
||||
from django.test import TestCase
|
||||
from django.db import models
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings")
|
||||
django.setup()
|
||||
from django.conf import settings
|
||||
from games.models import Game, Edition, Purchase, Session
|
||||
|
||||
ZONEINFO = ZoneInfo(settings.TIME_ZONE)
|
||||
|
||||
|
||||
class FormatDurationTest(TestCase):
|
||||
def setUp(self) -> None:
|
||||
return super().setUp()
|
||||
|
||||
def test_duration_format(self):
|
||||
g = Game(name="The Test Game")
|
||||
g.save()
|
||||
e = Edition(game=g, name="The Test Game Edition")
|
||||
e.save()
|
||||
p = Purchase(
|
||||
edition=e, date_purchased=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO)
|
||||
)
|
||||
p.save()
|
||||
s = Session(
|
||||
purchase=p,
|
||||
timestamp_start=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO),
|
||||
timestamp_end=datetime(2022, 9, 26, 17, 38, tzinfo=ZONEINFO),
|
||||
)
|
||||
s.save()
|
||||
self.assertEqual(
|
||||
s.duration_formatted(),
|
||||
"02:40",
|
||||
)
|
@ -83,6 +83,16 @@ class FormatDurationTest(unittest.TestCase):
|
||||
result = format_duration(delta, "%r seconds")
|
||||
self.assertEqual(result, "0 seconds")
|
||||
|
||||
def test_specific(self):
|
||||
delta = timedelta(hours=2, minutes=40)
|
||||
result = format_duration(delta, "%H:%m")
|
||||
self.assertEqual(result, "2:40")
|
||||
|
||||
def test_specific_precise_if_unncessary(self):
|
||||
delta = timedelta(hours=2, minutes=40)
|
||||
result = format_duration(delta, "%02.0H:%02.0m")
|
||||
self.assertEqual(result, "02:40")
|
||||
|
||||
def test_all_at_once(self):
|
||||
delta = timedelta(days=50, hours=10, minutes=34, seconds=24)
|
||||
result = format_duration(
|
||||
|
@ -150,5 +150,3 @@ if _csrf_trusted_origins:
|
||||
CSRF_TRUSTED_ORIGINS = _csrf_trusted_origins.split(",")
|
||||
else:
|
||||
CSRF_TRUSTED_ORIGINS = []
|
||||
|
||||
USE_L10N = False
|
||||
|
Reference in New Issue
Block a user