Compare commits
	
		
			5 Commits
		
	
	
		
			61d2e65d83
			...
			4b45127335
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4b45127335 | |||
| b8a15e43db | |||
| a1309c3738 | |||
| 12cc9025a0 | |||
| 6fe960bc04 | 
| @ -1,4 +1,9 @@ | |||||||
| ## Unreleased | ## Unreleased | ||||||
|  | * Allow deleting sessions | ||||||
|  | * Redirect after adding game/platform/purchase/session | ||||||
|  | * Fix display of duration_manual | ||||||
|  | * Fix display of duration_calculated, display durations less than a minute | ||||||
|  | * Make the "Finish now?" button on session list work | ||||||
| * Hide navigation bar items if there are no games/purchases/sessions | * Hide navigation bar items if there are no games/purchases/sessions | ||||||
| * Set default version to "git-main" to indicate development environment | * Set default version to "git-main" to indicate development environment | ||||||
| * Add homepage, link to it from the logo | * Add homepage, link to it from the logo | ||||||
|  | |||||||
| @ -12,5 +12,5 @@ COPY entrypoint.sh / | |||||||
| RUN chmod +x /entrypoint.sh | RUN chmod +x /entrypoint.sh | ||||||
| USER timetracker | USER timetracker | ||||||
| EXPOSE 8000 | EXPOSE 8000 | ||||||
| ENV VERSION_NUMBER 0.1.0-4-g166dd71 | ENV VERSION_NUMBER 0.1.0-18-gb8a15e4 | ||||||
| ENTRYPOINT [ "/entrypoint.sh" ] | ENTRYPOINT [ "/entrypoint.sh" ] | ||||||
							
								
								
									
										0
									
								
								src/web/common/util/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/web/common/util/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										7
									
								
								src/web/common/util/time.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/web/common/util/time.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | from datetime import datetime | ||||||
|  | from django.conf import settings | ||||||
|  | from zoneinfo import ZoneInfo | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def now(): | ||||||
|  |     return datetime.now(ZoneInfo(settings.TIME_ZONE)) | ||||||
| @ -1,5 +1,7 @@ | |||||||
| from django.db import models | from django.db import models | ||||||
| from datetime import timedelta | from datetime import datetime | ||||||
|  | from django.conf import settings | ||||||
|  | from zoneinfo import ZoneInfo | ||||||
|  |  | ||||||
|  |  | ||||||
| class Game(models.Model): | class Game(models.Model): | ||||||
| @ -40,14 +42,17 @@ class Session(models.Model): | |||||||
|         mark = ", manual" if self.duration_manual != None else "" |         mark = ", manual" if self.duration_manual != None else "" | ||||||
|         return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.duration_any()}{mark})" |         return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.duration_any()}{mark})" | ||||||
|  |  | ||||||
|  |     def finish_now(self): | ||||||
|  |         self.timestamp_end = datetime.now(ZoneInfo(settings.TIME_ZONE)) | ||||||
|  |  | ||||||
|     def duration_seconds(self): |     def duration_seconds(self): | ||||||
|         if self.timestamp_end == None or self.timestamp_start == None: |  | ||||||
|         if self.duration_manual == None: |         if self.duration_manual == None: | ||||||
|  |             if self.timestamp_end == None or self.timestamp_start == None: | ||||||
|                 return 0 |                 return 0 | ||||||
|             else: |  | ||||||
|                 value = self.duration_manual |  | ||||||
|             else: |             else: | ||||||
|                 value = self.timestamp_end - self.timestamp_start |                 value = self.timestamp_end - self.timestamp_start | ||||||
|  |         else: | ||||||
|  |             value = self.duration_manual | ||||||
|         return value.total_seconds() |         return value.total_seconds() | ||||||
|  |  | ||||||
|     def duration_formatted(self): |     def duration_formatted(self): | ||||||
| @ -55,7 +60,10 @@ class Session(models.Model): | |||||||
|         if seconds == 0: |         if seconds == 0: | ||||||
|             return seconds |             return seconds | ||||||
|         hours, remainder = divmod(seconds, 3600) |         hours, remainder = divmod(seconds, 3600) | ||||||
|         minutes = remainder % 60 |         minutes = remainder // 60 | ||||||
|  |         if hours == 0 and minutes == 0: | ||||||
|  |             return "less than a minute" | ||||||
|  |         else: | ||||||
|             hour_string = f"{int(hours)}h" if hours != 0 else "" |             hour_string = f"{int(hours)}h" if hours != 0 else "" | ||||||
|             minute_string = f"{int(minutes)}m" if minutes != 0 else "" |             minute_string = f"{int(minutes)}m" if minutes != 0 else "" | ||||||
|             return f"{hour_string}{minute_string}" |             return f"{hour_string}{minute_string}" | ||||||
|  | |||||||
| @ -766,8 +766,8 @@ select { | |||||||
|   max-width: 1024px; |   max-width: 1024px; | ||||||
| } | } | ||||||
|  |  | ||||||
| .grid-cols-4 { | .grid-cols-5 { | ||||||
|   grid-template-columns: repeat(4, minmax(0, 1fr)); |   grid-template-columns: repeat(5, minmax(0, 1fr)); | ||||||
| } | } | ||||||
|  |  | ||||||
| .flex-col { | .flex-col { | ||||||
| @ -819,11 +819,6 @@ select { | |||||||
|   border-color: rgb(229 231 235 / var(--tw-border-opacity)); |   border-color: rgb(229 231 235 / var(--tw-border-opacity)); | ||||||
| } | } | ||||||
|  |  | ||||||
| .border-red-800 { |  | ||||||
|   --tw-border-opacity: 1; |  | ||||||
|   border-color: rgb(153 27 27 / var(--tw-border-opacity)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .border-red-900 { | .border-red-900 { | ||||||
|   --tw-border-opacity: 1; |   --tw-border-opacity: 1; | ||||||
|   border-color: rgb(127 29 29 / var(--tw-border-opacity)); |   border-color: rgb(127 29 29 / var(--tw-border-opacity)); | ||||||
| @ -943,21 +938,6 @@ form input[type=submit] { | |||||||
|   border-color: rgb(255 255 255 / var(--tw-border-opacity)); |   border-color: rgb(255 255 255 / var(--tw-border-opacity)); | ||||||
| } | } | ||||||
|  |  | ||||||
| .hover\:bg-red-600:hover { |  | ||||||
|   --tw-bg-opacity: 1; |  | ||||||
|   background-color: rgb(220 38 38 / var(--tw-bg-opacity)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hover\:bg-red-500:hover { |  | ||||||
|   --tw-bg-opacity: 1; |  | ||||||
|   background-color: rgb(239 68 68 / var(--tw-bg-opacity)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hover\:bg-yellow-700:hover { |  | ||||||
|   --tw-bg-opacity: 1; |  | ||||||
|   background-color: rgb(161 98 7 / var(--tw-bg-opacity)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hover\:bg-orange-700:hover { | .hover\:bg-orange-700:hover { | ||||||
|   --tw-bg-opacity: 1; |   --tw-bg-opacity: 1; | ||||||
|   background-color: rgb(194 65 12 / var(--tw-bg-opacity)); |   background-color: rgb(194 65 12 / var(--tw-bg-opacity)); | ||||||
|  | |||||||
| @ -9,17 +9,18 @@ | |||||||
|     <a class="dark:text-white hover:underline" href="{% url 'list_sessions' %}">View all sessions</a> |     <a class="dark:text-white hover:underline" href="{% url 'list_sessions' %}">View all sessions</a> | ||||||
| </div> | </div> | ||||||
| {% endif %} | {% endif %} | ||||||
| <div class="grid grid-cols-4 gap-4 shadow rounded-xl max-w-screen-lg mx-auto dark:bg-slate-700 p-2 justify-center"> | <div class="grid grid-cols-5 gap-4 shadow rounded-xl max-w-screen-lg mx-auto dark:bg-slate-700 p-2 justify-center"> | ||||||
|     <div class="dark:border-white dark:text-slate-300 text-lg">Name</div> |     <div class="dark:border-white dark:text-slate-300 text-lg">Name</div> | ||||||
|     <div class="dark:border-white dark:text-slate-300 text-lg">Start</div> |     <div class="dark:border-white dark:text-slate-300 text-lg">Start</div> | ||||||
|     <div class="dark:border-white dark:text-slate-300 text-lg">End</div> |     <div class="dark:border-white dark:text-slate-300 text-lg">End</div> | ||||||
|     <div class="dark:border-white dark:text-slate-300 text-lg">Duration</div> |     <div class="dark:border-white dark:text-slate-300 text-lg">Duration</div> | ||||||
|  |     <div></div> | ||||||
|     {% for data in dataset %} |     {% for data in dataset %} | ||||||
|     <div class=""><a class="dark:text-white hover:underline" href="{% url 'list_sessions' data.purchase.id %}">{{ data.purchase }}</a></div> |     <div class=""><a class="dark:text-white hover:underline" href="{% url 'list_sessions' data.purchase.id %}">{{ data.purchase }}</a></div> | ||||||
|     <div class="dark:text-slate-400">{{ data.timestamp_start | date:"d/m/Y H:i" }}</div> |     <div class="dark:text-slate-400">{{ data.timestamp_start | date:"d/m/Y H:i" }}</div> | ||||||
|     <div class="dark:text-slate-400"> |     <div class="dark:text-slate-400"> | ||||||
|         {% if data.unfinished %} |         {% if data.unfinished %} | ||||||
|         Not finished yet. <button class="bg-red-700 hover:bg-orange-700 border border-red-900 hover:border-dotted hover:border-white rounded p-1 text-white text-sm">Finish now?</button> |         Not finished yet. <a href="{% url 'update_session' data.id %}"><button class="bg-red-700 hover:bg-orange-700 border border-red-900 hover:border-dotted hover:border-white rounded p-1 text-white text-sm">Finish now?</button></a> | ||||||
|         {% elif data.duration_manual %} |         {% elif data.duration_manual %} | ||||||
|         MANUAL |         MANUAL | ||||||
|         {% else %} |         {% else %} | ||||||
| @ -27,6 +28,9 @@ | |||||||
|         {% endif %} |         {% endif %} | ||||||
|     </div> |     </div> | ||||||
|     <div class="dark:text-slate-400">{{ data.duration_formatted }}{% if data.duration_manual %} (M){% endif %}</div> |     <div class="dark:text-slate-400">{{ data.duration_formatted }}{% if data.duration_manual %} (M){% endif %}</div> | ||||||
|  |     <div> | ||||||
|  |         <a href="{% url 'delete_session' data.id %}"><button>❌</button></a> | ||||||
|  |     </div> | ||||||
|     {% endfor %} |     {% endfor %} | ||||||
| </div> | </div> | ||||||
| {% endblock content %} | {% endblock content %} | ||||||
| @ -7,6 +7,16 @@ urlpatterns = [ | |||||||
|     path("add-game/", views.add_game, name="add_game"), |     path("add-game/", views.add_game, name="add_game"), | ||||||
|     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( | ||||||
|  |         "update-session/by-session/<int:session_id>", | ||||||
|  |         views.update_session, | ||||||
|  |         name="update_session", | ||||||
|  |     ), | ||||||
|  |     path( | ||||||
|  |         "delete_session/by-id/<int:session_id>", | ||||||
|  |         views.delete_session, | ||||||
|  |         name="delete_session", | ||||||
|  |     ), | ||||||
|     path("add-purchase/", views.add_purchase, name="add_purchase"), |     path("add-purchase/", views.add_purchase, name="add_purchase"), | ||||||
|     path("list-sessions/", views.list_sessions, name="list_sessions"), |     path("list-sessions/", views.list_sessions, name="list_sessions"), | ||||||
|     path( |     path( | ||||||
|  | |||||||
| @ -1,10 +1,11 @@ | |||||||
| from django.shortcuts import render | from django.shortcuts import render, redirect | ||||||
|  |  | ||||||
| from .models import Game, Platform, Purchase, Session | from .models import Game, Platform, Purchase, Session | ||||||
| from .forms import SessionForm, PurchaseForm, GameForm, PlatformForm | from .forms import SessionForm, PurchaseForm, GameForm, PlatformForm | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from zoneinfo import ZoneInfo | from zoneinfo import ZoneInfo | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
|  | from common.util.time import now as now_with_tz | ||||||
|  |  | ||||||
|  |  | ||||||
| def model_counts(request): | def model_counts(request): | ||||||
| @ -18,16 +19,30 @@ def model_counts(request): | |||||||
|  |  | ||||||
| def add_session(request): | def add_session(request): | ||||||
|     context = {} |     context = {} | ||||||
|     now = datetime.now() |     now = now_with_tz() | ||||||
|     initial = {"timestamp_start": now, "timestamp_end": now} |     initial = {"timestamp_start": now} | ||||||
|     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") | ||||||
|  |  | ||||||
|     context["form"] = form |     context["form"] = form | ||||||
|     return render(request, "add_session.html", context) |     return render(request, "add_session.html", context) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def update_session(request, session_id=None): | ||||||
|  |     session = Session.objects.get(id=session_id) | ||||||
|  |     session.finish_now() | ||||||
|  |     session.save() | ||||||
|  |     return redirect("list_sessions") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def delete_session(request, session_id=None): | ||||||
|  |     session = Session.objects.get(id=session_id) | ||||||
|  |     session.delete() | ||||||
|  |     return redirect("list_sessions") | ||||||
|  |  | ||||||
|  |  | ||||||
| def list_sessions(request, purchase_id=None): | def list_sessions(request, purchase_id=None): | ||||||
|     context = {} |     context = {} | ||||||
|  |  | ||||||
| @ -54,6 +69,7 @@ def add_purchase(request): | |||||||
|     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() |         form.save() | ||||||
|  |         return redirect("index") | ||||||
|  |  | ||||||
|     context["form"] = form |     context["form"] = form | ||||||
|     context["title"] = "Add New Purchase" |     context["title"] = "Add New Purchase" | ||||||
| @ -65,6 +81,7 @@ def add_game(request): | |||||||
|     form = GameForm(request.POST or None) |     form = GameForm(request.POST or None) | ||||||
|     if form.is_valid(): |     if form.is_valid(): | ||||||
|         form.save() |         form.save() | ||||||
|  |         return redirect("index") | ||||||
|  |  | ||||||
|     context["form"] = form |     context["form"] = form | ||||||
|     context["title"] = "Add New Game" |     context["title"] = "Add New Game" | ||||||
| @ -76,6 +93,7 @@ def add_platform(request): | |||||||
|     form = PlatformForm(request.POST or None) |     form = PlatformForm(request.POST or None) | ||||||
|     if form.is_valid(): |     if form.is_valid(): | ||||||
|         form.save() |         form.save() | ||||||
|  |         return redirect("index") | ||||||
|  |  | ||||||
|     context["form"] = form |     context["form"] = form | ||||||
|     context["title"] = "Add New Platform" |     context["title"] = "Add New Platform" | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user