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,4 +1,9 @@ | ||||
| ## Unreleased | ||||
| ### Unreleased | ||||
|  | ||||
| ## New | ||||
| * Add stat for finished this year's games | ||||
|  | ||||
| ## 1.4.0 / 2023-11-09 21:01+01:00 | ||||
|  | ||||
| ### New | ||||
| * More fields are now optional. This is to make it easier to add new items in bulk. | ||||
| @ -23,6 +28,8 @@ | ||||
|   * Unfinished (count) | ||||
|   * Refunded (count) | ||||
|   * Backlog Decrease (count) | ||||
| * New workflow: | ||||
|   * Adding Game, Edition, Purchase, and Session in a row is now much faster | ||||
|  | ||||
| ### Improved | ||||
| * game overview: simplify playtime range display | ||||
|  | ||||
| @ -6,7 +6,7 @@ RUN npm install && \ | ||||
|  | ||||
| FROM python:3.10.9-slim-bullseye | ||||
|  | ||||
| ENV VERSION_NUMBER 1.3.0 | ||||
| ENV VERSION_NUMBER 1.4.0 | ||||
| ENV PROD 1 | ||||
| ENV PYTHONUNBUFFERED=1 | ||||
|  | ||||
|  | ||||
| @ -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() | ||||
|  | ||||
							
								
								
									
										29
									
								
								games/templates/add_edition.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								games/templates/add_edition.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | ||||
| {% 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 %} | ||||
|          | ||||
							
								
								
									
										29
									
								
								games/templates/add_game.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								games/templates/add_game.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | ||||
| {% 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 %} | ||||
|          | ||||
							
								
								
									
										29
									
								
								games/templates/add_purchase.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								games/templates/add_purchase.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | ||||
| {% 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 %} | ||||
|          | ||||
| @ -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> | ||||
|  | ||||
| @ -13,6 +13,11 @@ urlpatterns = [ | ||||
|     path("add-game/", views.add_game, name="add_game"), | ||||
|     path("add-platform/", views.add_platform, name="add_platform"), | ||||
|     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( | ||||
|         "update-session/by-session/<int:session_id>", | ||||
|         views.update_session, | ||||
| @ -34,7 +39,17 @@ urlpatterns = [ | ||||
|     #     name="delete_session", | ||||
|     # ), | ||||
|     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-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("game/<int:game_id>/view", views.view_game, name="view_game"), | ||||
|     path("game/<int:game_id>/edit", views.edit_game, name="edit_game"), | ||||
|  | ||||
							
								
								
									
										128
									
								
								games/views.py
									
									
									
									
									
								
							
							
						
						
									
										128
									
								
								games/views.py
									
									
									
									
									
								
							| @ -32,24 +32,38 @@ 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): | ||||
| def add_session(request, purchase_id=None): | ||||
|     context = {} | ||||
|     initial = {} | ||||
|  | ||||
|     now = now_with_tz() | ||||
|     initial["timestamp_start"] = now | ||||
|     initial = {"timestamp_start": now_with_tz()} | ||||
|  | ||||
|     last = Session.objects.all().last() | ||||
|     if last != None: | ||||
|         initial["purchase"] = last.purchase | ||||
|  | ||||
|     form = SessionForm(request.POST or None, initial=initial) | ||||
|     if form.is_valid(): | ||||
|         form.save() | ||||
|         return redirect("list_sessions") | ||||
|     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) | ||||
|  | ||||
|     context["title"] = "Add New Session" | ||||
|     context["form"] = form | ||||
| @ -322,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) | ||||
| @ -371,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), | ||||
| @ -397,45 +417,91 @@ def stats(request, year: int = 0): | ||||
|     return render(request, "stats.html", context) | ||||
|  | ||||
|  | ||||
| def add_purchase(request): | ||||
| def add_purchase(request, edition_id=None): | ||||
|     context = {} | ||||
|     now = datetime.now() | ||||
|     initial = {"date_purchased": now} | ||||
|     form = PurchaseForm(request.POST or None, initial=initial) | ||||
|     if form.is_valid(): | ||||
|         form.save() | ||||
|         return redirect("index") | ||||
|     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) | ||||
|  | ||||
|     context["form"] = form | ||||
|     context["title"] = "Add New Purchase" | ||||
|     context["script_name"] = "add_purchase.js" | ||||
|     return render(request, "add.html", context) | ||||
|     return render(request, "add_purchase.html", context) | ||||
|  | ||||
|  | ||||
| def add_game(request): | ||||
|     context = {} | ||||
|     form = GameForm(request.POST or None) | ||||
|     if form.is_valid(): | ||||
|         form.save() | ||||
|         return redirect("index") | ||||
|         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") | ||||
|  | ||||
|     context["form"] = form | ||||
|     context["title"] = "Add New Game" | ||||
|     context["script_name"] = "add_game.js" | ||||
|     return render(request, "add.html", context) | ||||
|     return render(request, "add_game.html", context) | ||||
|  | ||||
|  | ||||
| def add_edition(request): | ||||
| def add_edition(request, game_id=None): | ||||
|     context = {} | ||||
|     form = EditionForm(request.POST or None) | ||||
|     if form.is_valid(): | ||||
|         form.save() | ||||
|         return redirect("index") | ||||
|     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, | ||||
|                 } | ||||
|             ) | ||||
|         else: | ||||
|             form = EditionForm() | ||||
|  | ||||
|     context["form"] = form | ||||
|     context["title"] = "Add New Edition" | ||||
|     context["script_name"] = "add_edition.js" | ||||
|     return render(request, "add.html", context) | ||||
|     return render(request, "add_edition.html", context) | ||||
|  | ||||
|  | ||||
| def add_platform(request): | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| [tool.poetry] | ||||
| name = "timetracker" | ||||
| version = "1.3.0" | ||||
| version = "1.4.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