Compare commits
	
		
			12 Commits
		
	
	
		
			1.4.0
			...
			c8a3212b77
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | ||||
|  | ||||
| @ -1,3 +1,8 @@ | ||||
| ### Unreleased | ||||
|  | ||||
| ## New | ||||
| * Add stat for finished this year's games | ||||
|  | ||||
| ## 1.4.0 / 2023-11-09 21:01+01:00 | ||||
|  | ||||
| ### New | ||||
|  | ||||
| @ -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), | ||||
|  | ||||
| @ -151,7 +151,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 +163,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 +173,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() | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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): | ||||
| @ -331,7 +336,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 +385,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 +488,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() | ||||
|  | ||||
							
								
								
									
										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