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 |     - python -m pip install poetry | ||||||
|     - poetry install |     - poetry install | ||||||
|     - poetry env info |     - poetry env info | ||||||
|  |     - poetry run python manage.py migrate | ||||||
|     - poetry run pytest |     - poetry run pytest | ||||||
|  |  | ||||||
| - name: build-prod | - 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 | ## 1.4.0 / 2023-11-09 21:01+01:00 | ||||||
|  |  | ||||||
| ### New | ### New | ||||||
|  | |||||||
| @ -44,7 +44,7 @@ def format_duration( | |||||||
|     # timestamps where end is before start |     # timestamps where end is before start | ||||||
|     if seconds_total < 0: |     if seconds_total < 0: | ||||||
|         seconds_total = 0 |         seconds_total = 0 | ||||||
|     days = hours = minutes = seconds = 0 |     days = hours = hours_float = minutes = seconds = 0 | ||||||
|     remainder = seconds = seconds_total |     remainder = seconds = seconds_total | ||||||
|     if "%d" in format_string: |     if "%d" in format_string: | ||||||
|         days, remainder = divmod(seconds_total, day_seconds) |         days, remainder = divmod(seconds_total, day_seconds) | ||||||
| @ -55,7 +55,7 @@ def format_duration( | |||||||
|         minutes, seconds = divmod(remainder, minute_seconds) |         minutes, seconds = divmod(remainder, minute_seconds) | ||||||
|     literals = { |     literals = { | ||||||
|         "d": str(days), |         "d": str(days), | ||||||
|         "H": str(hours), |         "H": str(hours) if "m" not in format_string else str(hours_float), | ||||||
|         "m": str(minutes), |         "m": str(minutes), | ||||||
|         "s": str(seconds), |         "s": str(seconds), | ||||||
|         "r": str(seconds_total), |         "r": str(seconds_total), | ||||||
|  | |||||||
| @ -151,7 +151,7 @@ class Session(models.Model): | |||||||
|     objects = SessionQuerySet.as_manager() |     objects = SessionQuerySet.as_manager() | ||||||
|  |  | ||||||
|     def __str__(self): |     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})" |         return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.duration_formatted()}{mark})" | ||||||
|  |  | ||||||
|     def finish_now(self): |     def finish_now(self): | ||||||
| @ -163,7 +163,7 @@ class Session(models.Model): | |||||||
|     def duration_seconds(self) -> timedelta: |     def duration_seconds(self) -> timedelta: | ||||||
|         manual = timedelta(0) |         manual = timedelta(0) | ||||||
|         calculated = timedelta(0) |         calculated = timedelta(0) | ||||||
|         if not self.duration_manual in (None, 0, timedelta(0)): |         if self.is_manual(): | ||||||
|             manual = self.duration_manual |             manual = self.duration_manual | ||||||
|         if self.timestamp_end != None and self.timestamp_start != None: |         if self.timestamp_end != None and self.timestamp_start != None: | ||||||
|             calculated = self.timestamp_end - self.timestamp_start |             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") |         result = format_duration(self.duration_seconds(), "%02.0H:%02.0m") | ||||||
|         return result |         return result | ||||||
|  |  | ||||||
|  |     def is_manual(self) -> bool: | ||||||
|  |         return not self.duration_manual == timedelta(0) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def duration_sum(self) -> str: |     def duration_sum(self) -> str: | ||||||
|         return Session.objects.all().total_duration_formatted() |         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">Finished</td> | ||||||
|                             <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ all_finished_this_year.count }}</td> |                             <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ all_finished_this_year.count }}</td> | ||||||
|                         </tr> |                         </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> |                     </tbody> | ||||||
|                 </table> |                 </table> | ||||||
|             </div> |             </div> | ||||||
|  | |||||||
| @ -11,7 +11,6 @@ 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( |     path( | ||||||
|  | |||||||
| @ -32,7 +32,12 @@ def model_counts(request): | |||||||
|  |  | ||||||
|  |  | ||||||
| def stats_dropdown_year_range(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): | 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( |     this_year_spendings = this_year_purchases_without_refunded.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"] or 0 | ||||||
|  |  | ||||||
|     games_with_playtime = ( |     games_with_playtime = ( | ||||||
|         Game.objects.filter(edition__purchase__session__in=this_year_sessions) |         Game.objects.filter(edition__purchase__session__in=this_year_sessions) | ||||||
| @ -380,9 +385,15 @@ def stats(request, year: int = 0): | |||||||
|         "spent_per_game": int( |         "spent_per_game": int( | ||||||
|             safe_division(total_spent, this_year_purchases_without_refunded.count()) |             safe_division(total_spent, this_year_purchases_without_refunded.count()) | ||||||
|         ), |         ), | ||||||
|         "all_finished_this_year": purchases_finished_this_year, |         "all_finished_this_year": purchases_finished_this_year.order_by( | ||||||
|         "this_year_finished_this_year": purchases_finished_this_year_released_this_year, |             "date_finished" | ||||||
|         "purchased_this_year_finished_this_year": purchased_this_year_finished_this_year, |         ), | ||||||
|  |         "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(), |         "total_sessions": this_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), | ||||||
| @ -477,7 +488,12 @@ def add_edition(request, game_id=None): | |||||||
|         if game_id: |         if game_id: | ||||||
|             game = Game.objects.get(id=game_id) |             game = Game.objects.get(id=game_id) | ||||||
|             form = EditionForm( |             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: |         else: | ||||||
|             form = EditionForm() |             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") |         result = format_duration(delta, "%r seconds") | ||||||
|         self.assertEqual(result, "0 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): |     def test_all_at_once(self): | ||||||
|         delta = timedelta(days=50, hours=10, minutes=34, seconds=24) |         delta = timedelta(days=50, hours=10, minutes=34, seconds=24) | ||||||
|         result = format_duration( |         result = format_duration( | ||||||
|  | |||||||
| @ -150,5 +150,3 @@ if _csrf_trusted_origins: | |||||||
|     CSRF_TRUSTED_ORIGINS = _csrf_trusted_origins.split(",") |     CSRF_TRUSTED_ORIGINS = _csrf_trusted_origins.split(",") | ||||||
| else: | else: | ||||||
|     CSRF_TRUSTED_ORIGINS = [] |     CSRF_TRUSTED_ORIGINS = [] | ||||||
|  |  | ||||||
| USE_L10N = False |  | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user