Compare commits
	
		
			15 Commits
		
	
	
		
			remove_edi
			...
			89d1bbdd9e
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 89d1bbdd9e | |||
| 637e3e6493 | |||
| d213a3d35d | |||
| 2f4e16dd54 | |||
| 6f62889e92 | |||
| 4ec808eeec | |||
| 69d27958f3 | |||
| 4ec1cf5f28 | |||
| d936fdc60d | |||
| 2116cfc219 | |||
| 6bd8271291 | |||
| e571feadef | |||
| 23c1ce1f96 | |||
| 33103daebc | |||
| ba6028e43d | 
| @ -8,6 +8,7 @@ | |||||||
| * Add all-time stats | * Add all-time stats | ||||||
| * Manage purchases | * Manage purchases | ||||||
| * Automatically convert purchase prices | * Automatically convert purchase prices | ||||||
|  | * Add emulated property to sessions | ||||||
|  |  | ||||||
| ## Improved | ## Improved | ||||||
| * mark refunded purchases red on game overview | * mark refunded purchases red on game overview | ||||||
|  | |||||||
| @ -9,7 +9,7 @@ from django.urls import NoReverseMatch, reverse | |||||||
| from django.utils.safestring import SafeText, mark_safe | from django.utils.safestring import SafeText, mark_safe | ||||||
|  |  | ||||||
| from common.utils import truncate | from common.utils import truncate | ||||||
| from games.models import Purchase | from games.models import Game, Purchase, Session | ||||||
|  |  | ||||||
| HTMLAttribute = tuple[str, str | int | bool] | HTMLAttribute = tuple[str, str | int | bool] | ||||||
| HTMLTag = str | HTMLTag = str | ||||||
| @ -32,7 +32,7 @@ def Component( | |||||||
|         attributesList = [f'{name}="{value}"' for name, value in attributes] |         attributesList = [f'{name}="{value}"' for name, value in attributes] | ||||||
|         # make attribute list into a string |         # make attribute list into a string | ||||||
|         # and insert space between tag and attribute list |         # and insert space between tag and attribute list | ||||||
|         attributesBlob = f" {" ".join(attributesList)}" |         attributesBlob = f" {' '.join(attributesList)}" | ||||||
|     tag: str = "" |     tag: str = "" | ||||||
|     if tag_name != "": |     if tag_name != "": | ||||||
|         tag = f"<{tag_name}{attributesBlob}>{childrenBlob}</{tag_name}>" |         tag = f"<{tag_name}{attributesBlob}>{childrenBlob}</{tag_name}>" | ||||||
| @ -188,49 +188,28 @@ def Icon( | |||||||
|     return result |     return result | ||||||
|  |  | ||||||
|  |  | ||||||
| def LinkedNameWithPlatformIcon(name: str, game_id: int, platform: str) -> SafeText: |  | ||||||
|     link = reverse("view_game", args=[int(game_id)]) |  | ||||||
|     a_content = Div( |  | ||||||
|         [("class", "inline-flex gap-2 items-center")], |  | ||||||
|         [ |  | ||||||
|             Icon( |  | ||||||
|                 platform.icon, |  | ||||||
|                 [("title", platform.name)], |  | ||||||
|             ), |  | ||||||
|             PopoverTruncated(name), |  | ||||||
|         ], |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     return mark_safe( |  | ||||||
|         A( |  | ||||||
|             url=link, |  | ||||||
|             children=[a_content], |  | ||||||
|         ), |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def LinkedPurchase(purchase: Purchase) -> SafeText: | def LinkedPurchase(purchase: Purchase) -> SafeText: | ||||||
|     link = reverse("view_purchase", args=[int(purchase.id)]) |     link = reverse("view_purchase", args=[int(purchase.id)]) | ||||||
|     link_content = "" |     link_content = "" | ||||||
|     popover_content = "" |     popover_content = "" | ||||||
|     edition_count = purchase.editions.count() |     game_count = purchase.games.count() | ||||||
|     popover_if_not_truncated = False |     popover_if_not_truncated = False | ||||||
|     if edition_count == 1: |     if game_count == 1: | ||||||
|         link_content += purchase.editions.first().name |         link_content += purchase.games.first().name | ||||||
|         popover_content = link_content |         popover_content = link_content | ||||||
|     if edition_count > 1: |     if game_count > 1: | ||||||
|         if purchase.name: |         if purchase.name: | ||||||
|             link_content += f"{purchase.name}" |             link_content += f"{purchase.name}" | ||||||
|             popover_content += f"<h1>{purchase.name}</h1><br>" |             popover_content += f"<h1>{purchase.name}</h1><br>" | ||||||
|         else: |         else: | ||||||
|             link_content += f"{edition_count} games" |             link_content += f"{game_count} games" | ||||||
|             popover_if_not_truncated = True |             popover_if_not_truncated = True | ||||||
|         popover_content += f""" |         popover_content += f""" | ||||||
|         <ul class="list-disc list-inside"> |         <ul class="list-disc list-inside"> | ||||||
|             {"".join(f"<li>{edition.name}</li>" for edition in purchase.editions.all())} |             {"".join(f"<li>{game.name}</li>" for game in purchase.games.all())} | ||||||
|         </ul> |         </ul> | ||||||
|         """ |         """ | ||||||
|     icon = purchase.platform.icon if edition_count == 1 else "unspecified" |     icon = purchase.platform.icon if game_count == 1 else "unspecified" | ||||||
|     if link_content == "": |     if link_content == "": | ||||||
|         raise ValueError("link_content is empty!!") |         raise ValueError("link_content is empty!!") | ||||||
|     a_content = Div( |     a_content = Div( | ||||||
| @ -250,19 +229,54 @@ def LinkedPurchase(purchase: Purchase) -> SafeText: | |||||||
|     return mark_safe(A(url=link, children=[a_content])) |     return mark_safe(A(url=link, children=[a_content])) | ||||||
|  |  | ||||||
|  |  | ||||||
| def NameWithPlatformIcon(name: str, platform: str) -> SafeText: | def NameWithIcon( | ||||||
|  |     name: str = "", | ||||||
|  |     platform: str = "", | ||||||
|  |     game_id: int = 0, | ||||||
|  |     session_id: int = 0, | ||||||
|  |     purchase_id: int = 0, | ||||||
|  |     linkify: bool = True, | ||||||
|  |     emulated: bool = False, | ||||||
|  | ) -> SafeText: | ||||||
|  |     create_link = False | ||||||
|  |     link = "" | ||||||
|  |     platform = None | ||||||
|  |     if (game_id != 0 or session_id != 0 or purchase_id != 0) and linkify: | ||||||
|  |         create_link = True | ||||||
|  |         if session_id: | ||||||
|  |             session = Session.objects.get(pk=session_id) | ||||||
|  |             emulated = session.emulated | ||||||
|  |             game_id = session.game.pk | ||||||
|  |         if purchase_id: | ||||||
|  |             purchase = Purchase.objects.get(pk=purchase_id) | ||||||
|  |             game_id = purchase.games.first().pk | ||||||
|  |         if game_id: | ||||||
|  |             game = Game.objects.get(pk=game_id) | ||||||
|  |         name = name or game.name | ||||||
|  |         platform = game.platform | ||||||
|  |         link = reverse("view_game", args=[int(game_id)]) | ||||||
|     content = Div( |     content = Div( | ||||||
|         [("class", "inline-flex gap-2 items-center")], |         [("class", "inline-flex gap-2 items-center")], | ||||||
|         [ |         [ | ||||||
|             Icon( |             Icon( | ||||||
|                 platform.icon, |                 platform.icon, | ||||||
|                 [("title", platform.name)], |                 [("title", platform.name)], | ||||||
|             ), |             ) | ||||||
|  |             if platform | ||||||
|  |             else "", | ||||||
|  |             Icon("emulated", [("title", "Emulated")]) if emulated else "", | ||||||
|             PopoverTruncated(name), |             PopoverTruncated(name), | ||||||
|         ], |         ], | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     return mark_safe(content) |     return mark_safe( | ||||||
|  |         A( | ||||||
|  |             url=link, | ||||||
|  |             children=[content], | ||||||
|  |         ) | ||||||
|  |         if create_link | ||||||
|  |         else content, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def PurchasePrice(purchase) -> str: | def PurchasePrice(purchase) -> str: | ||||||
|  | |||||||
| @ -2,7 +2,6 @@ from django.contrib import admin | |||||||
|  |  | ||||||
| from games.models import ( | from games.models import ( | ||||||
|     Device, |     Device, | ||||||
|     Edition, |  | ||||||
|     ExchangeRate, |     ExchangeRate, | ||||||
|     Game, |     Game, | ||||||
|     Platform, |     Platform, | ||||||
| @ -15,6 +14,5 @@ admin.site.register(Game) | |||||||
| admin.site.register(Purchase) | admin.site.register(Purchase) | ||||||
| admin.site.register(Platform) | admin.site.register(Platform) | ||||||
| admin.site.register(Session) | admin.site.register(Session) | ||||||
| admin.site.register(Edition) |  | ||||||
| admin.site.register(Device) | admin.site.register(Device) | ||||||
| admin.site.register(ExchangeRate) | admin.site.register(ExchangeRate) | ||||||
|  | |||||||
| @ -11,6 +11,8 @@ class GamesConfig(AppConfig): | |||||||
|     name = "games" |     name = "games" | ||||||
|  |  | ||||||
|     def ready(self): |     def ready(self): | ||||||
|  |         import games.signals  # noqa: F401 | ||||||
|  |  | ||||||
|         post_migrate.connect(schedule_tasks, sender=self) |         post_migrate.connect(schedule_tasks, sender=self) | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -24,6 +26,16 @@ def schedule_tasks(sender, **kwargs): | |||||||
|             name="Update converted prices", |             name="Update converted prices", | ||||||
|             schedule_type=Schedule.MINUTES, |             schedule_type=Schedule.MINUTES, | ||||||
|             next_run=now() + timedelta(seconds=30), |             next_run=now() + timedelta(seconds=30), | ||||||
|  |             catchup=False, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     if not Schedule.objects.filter(name="Update price per game").exists(): | ||||||
|  |         schedule( | ||||||
|  |             "games.tasks.calculate_price_per_game", | ||||||
|  |             name="Update price per game", | ||||||
|  |             schedule_type=Schedule.MINUTES, | ||||||
|  |             next_run=now() + timedelta(seconds=30), | ||||||
|  |             catchup=False, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     from games.models import ExchangeRate |     from games.models import ExchangeRate | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ from django import forms | |||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  |  | ||||||
| from common.utils import safe_getattr | from common.utils import safe_getattr | ||||||
| from games.models import Device, Edition, Game, Platform, Purchase, Session | from games.models import Device, Game, Platform, Purchase, Session | ||||||
|  |  | ||||||
| custom_date_widget = forms.DateInput(attrs={"type": "date"}) | custom_date_widget = forms.DateInput(attrs={"type": "date"}) | ||||||
| custom_datetime_widget = forms.DateTimeInput( | custom_datetime_widget = forms.DateTimeInput( | ||||||
| @ -12,11 +12,8 @@ autofocus_input_widget = forms.TextInput(attrs={"autofocus": "autofocus"}) | |||||||
|  |  | ||||||
|  |  | ||||||
| class SessionForm(forms.ModelForm): | class SessionForm(forms.ModelForm): | ||||||
|     # purchase = forms.ModelChoiceField( |     game = forms.ModelChoiceField( | ||||||
|     #     queryset=Purchase.objects.filter(date_refunded=None).order_by("edition__name") |         queryset=Game.objects.order_by("sort_name"), | ||||||
|     # ) |  | ||||||
|     purchase = forms.ModelChoiceField( |  | ||||||
|         queryset=Purchase.objects.all(), |  | ||||||
|         widget=forms.Select(attrs={"autofocus": "autofocus"}), |         widget=forms.Select(attrs={"autofocus": "autofocus"}), | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
| @ -29,16 +26,17 @@ class SessionForm(forms.ModelForm): | |||||||
|         } |         } | ||||||
|         model = Session |         model = Session | ||||||
|         fields = [ |         fields = [ | ||||||
|             "purchase", |             "game", | ||||||
|             "timestamp_start", |             "timestamp_start", | ||||||
|             "timestamp_end", |             "timestamp_end", | ||||||
|             "duration_manual", |             "duration_manual", | ||||||
|  |             "emulated", | ||||||
|             "device", |             "device", | ||||||
|             "note", |             "note", | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class EditionChoiceField(forms.ModelMultipleChoiceField): | class GameChoiceField(forms.ModelMultipleChoiceField): | ||||||
|     def label_from_instance(self, obj) -> str: |     def label_from_instance(self, obj) -> str: | ||||||
|         return f"{obj.sort_name} ({obj.platform}, {obj.year_released})" |         return f"{obj.sort_name} ({obj.platform}, {obj.year_released})" | ||||||
|  |  | ||||||
| @ -56,19 +54,19 @@ class PurchaseForm(forms.ModelForm): | |||||||
|         super().__init__(*args, **kwargs) |         super().__init__(*args, **kwargs) | ||||||
|  |  | ||||||
|         # Automatically update related_purchase <select/> |         # Automatically update related_purchase <select/> | ||||||
|         # to only include purchases of the selected edition. |         # to only include purchases of the selected game. | ||||||
|         related_purchase_by_edition_url = reverse("related_purchase_by_edition") |         related_purchase_by_game_url = reverse("related_purchase_by_game") | ||||||
|         self.fields["editions"].widget.attrs.update( |         self.fields["games"].widget.attrs.update( | ||||||
|             { |             { | ||||||
|                 "hx-trigger": "load, click", |                 "hx-trigger": "load, click", | ||||||
|                 "hx-get": related_purchase_by_edition_url, |                 "hx-get": related_purchase_by_game_url, | ||||||
|                 "hx-target": "#id_related_purchase", |                 "hx-target": "#id_related_purchase", | ||||||
|                 "hx-swap": "outerHTML", |                 "hx-swap": "outerHTML", | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     editions = EditionChoiceField( |     games = GameChoiceField( | ||||||
|         queryset=Edition.objects.order_by("sort_name"), |         queryset=Game.objects.order_by("sort_name"), | ||||||
|         widget=IncludePlatformSelect(attrs={"autoselect": "autoselect"}), |         widget=IncludePlatformSelect(attrs={"autoselect": "autoselect"}), | ||||||
|     ) |     ) | ||||||
|     platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name")) |     platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name")) | ||||||
| @ -86,7 +84,7 @@ class PurchaseForm(forms.ModelForm): | |||||||
|         } |         } | ||||||
|         model = Purchase |         model = Purchase | ||||||
|         fields = [ |         fields = [ | ||||||
|             "editions", |             "games", | ||||||
|             "platform", |             "platform", | ||||||
|             "date_purchased", |             "date_purchased", | ||||||
|             "date_refunded", |             "date_refunded", | ||||||
| @ -138,24 +136,14 @@ class GameModelChoiceField(forms.ModelChoiceField): | |||||||
|         return obj.sort_name |         return obj.sort_name | ||||||
|  |  | ||||||
|  |  | ||||||
| class EditionForm(forms.ModelForm): | class GameForm(forms.ModelForm): | ||||||
|     game = GameModelChoiceField( |  | ||||||
|         queryset=Game.objects.order_by("sort_name"), |  | ||||||
|         widget=IncludeNameSelect(attrs={"autofocus": "autofocus"}), |  | ||||||
|     ) |  | ||||||
|     platform = forms.ModelChoiceField( |     platform = forms.ModelChoiceField( | ||||||
|         queryset=Platform.objects.order_by("name"), required=False |         queryset=Platform.objects.order_by("name"), required=False | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     class Meta: |  | ||||||
|         model = Edition |  | ||||||
|         fields = ["game", "name", "sort_name", "platform", "year_released", "wikidata"] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class GameForm(forms.ModelForm): |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = Game |         model = Game | ||||||
|         fields = ["name", "sort_name", "year_released", "wikidata"] |         fields = ["name", "sort_name", "platform", "year_released", "wikidata"] | ||||||
|         widgets = {"name": autofocus_input_widget} |         widgets = {"name": autofocus_input_widget} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,5 +1,4 @@ | |||||||
| from .device import Query as DeviceQuery | from .device import Query as DeviceQuery | ||||||
| from .edition import Query as EditionQuery |  | ||||||
| from .game import Query as GameQuery | from .game import Query as GameQuery | ||||||
| from .platform import Query as PlatformQuery | from .platform import Query as PlatformQuery | ||||||
| from .purchase import Query as PurchaseQuery | from .purchase import Query as PurchaseQuery | ||||||
|  | |||||||
| @ -1,11 +0,0 @@ | |||||||
| import graphene |  | ||||||
|  |  | ||||||
| from games.graphql.types import Edition |  | ||||||
| from games.models import Game as EditionModel |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Query(graphene.ObjectType): |  | ||||||
|     editions = graphene.List(Edition) |  | ||||||
|  |  | ||||||
|     def resolve_editions(self, info, **kwargs): |  | ||||||
|         return EditionModel.objects.all() |  | ||||||
| @ -1,5 +1,6 @@ | |||||||
| # Generated by Django 4.1.4 on 2023-01-02 18:27 | # Generated by Django 5.1.5 on 2025-01-29 21:26 | ||||||
|  |  | ||||||
|  | import datetime | ||||||
| import django.db.models.deletion | import django.db.models.deletion | ||||||
| from django.db import migrations, models | from django.db import migrations, models | ||||||
|  |  | ||||||
| @ -8,94 +9,96 @@ class Migration(migrations.Migration): | |||||||
|  |  | ||||||
|     initial = True |     initial = True | ||||||
|  |  | ||||||
|     dependencies = [] |     dependencies = [ | ||||||
|  |     ] | ||||||
|  |  | ||||||
|     operations = [ |     operations = [ | ||||||
|         migrations.CreateModel( |         migrations.CreateModel( | ||||||
|             name="Game", |             name='Device', | ||||||
|             fields=[ |             fields=[ | ||||||
|                 ( |                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|                     "id", |                 ('name', models.CharField(max_length=255)), | ||||||
|                     models.BigAutoField( |                 ('type', models.CharField(choices=[('PC', 'PC'), ('Console', 'Console'), ('Handheld', 'Handheld'), ('Mobile', 'Mobile'), ('Single-board computer', 'Single-board computer'), ('Unknown', 'Unknown')], default='Unknown', max_length=255)), | ||||||
|                         auto_created=True, |                 ('created_at', models.DateTimeField(auto_now_add=True)), | ||||||
|                         primary_key=True, |  | ||||||
|                         serialize=False, |  | ||||||
|                         verbose_name="ID", |  | ||||||
|                     ), |  | ||||||
|                 ), |  | ||||||
|                 ("name", models.CharField(max_length=255)), |  | ||||||
|                 ("wikidata", models.CharField(max_length=50)), |  | ||||||
|             ], |             ], | ||||||
|         ), |         ), | ||||||
|         migrations.CreateModel( |         migrations.CreateModel( | ||||||
|             name="Platform", |             name='Platform', | ||||||
|             fields=[ |             fields=[ | ||||||
|                 ( |                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|                     "id", |                 ('name', models.CharField(max_length=255)), | ||||||
|                     models.BigAutoField( |                 ('group', models.CharField(blank=True, default=None, max_length=255, null=True)), | ||||||
|                         auto_created=True, |                 ('icon', models.SlugField(blank=True)), | ||||||
|                         primary_key=True, |                 ('created_at', models.DateTimeField(auto_now_add=True)), | ||||||
|                         serialize=False, |  | ||||||
|                         verbose_name="ID", |  | ||||||
|                     ), |  | ||||||
|                 ), |  | ||||||
|                 ("name", models.CharField(max_length=255)), |  | ||||||
|                 ("group", models.CharField(max_length=255)), |  | ||||||
|             ], |             ], | ||||||
|         ), |         ), | ||||||
|         migrations.CreateModel( |         migrations.CreateModel( | ||||||
|             name="Purchase", |             name='ExchangeRate', | ||||||
|             fields=[ |             fields=[ | ||||||
|                 ( |                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|                     "id", |                 ('currency_from', models.CharField(max_length=255)), | ||||||
|                     models.BigAutoField( |                 ('currency_to', models.CharField(max_length=255)), | ||||||
|                         auto_created=True, |                 ('year', models.PositiveIntegerField()), | ||||||
|                         primary_key=True, |                 ('rate', models.FloatField()), | ||||||
|                         serialize=False, |             ], | ||||||
|                         verbose_name="ID", |             options={ | ||||||
|                     ), |                 'unique_together': {('currency_from', 'currency_to', 'year')}, | ||||||
|                 ), |             }, | ||||||
|                 ("date_purchased", models.DateField()), |         ), | ||||||
|                 ("date_refunded", models.DateField(blank=True, null=True)), |         migrations.CreateModel( | ||||||
|                 ( |             name='Game', | ||||||
|                     "game", |             fields=[ | ||||||
|                     models.ForeignKey( |                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|                         on_delete=django.db.models.deletion.CASCADE, to="games.game" |                 ('name', models.CharField(max_length=255)), | ||||||
|                     ), |                 ('sort_name', models.CharField(blank=True, default=None, max_length=255, null=True)), | ||||||
|                 ), |                 ('year_released', models.IntegerField(blank=True, default=None, null=True)), | ||||||
|                 ( |                 ('wikidata', models.CharField(blank=True, default=None, max_length=50, null=True)), | ||||||
|                     "platform", |                 ('created_at', models.DateTimeField(auto_now_add=True)), | ||||||
|                     models.ForeignKey( |                 ('platform', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='games.platform')), | ||||||
|                         on_delete=django.db.models.deletion.CASCADE, |             ], | ||||||
|                         to="games.platform", |             options={ | ||||||
|                     ), |                 'unique_together': {('name', 'platform', 'year_released')}, | ||||||
|                 ), |             }, | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='Purchase', | ||||||
|  |             fields=[ | ||||||
|  |                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|  |                 ('date_purchased', models.DateField()), | ||||||
|  |                 ('date_refunded', models.DateField(blank=True, null=True)), | ||||||
|  |                 ('date_finished', models.DateField(blank=True, null=True)), | ||||||
|  |                 ('date_dropped', models.DateField(blank=True, null=True)), | ||||||
|  |                 ('infinite', models.BooleanField(default=False)), | ||||||
|  |                 ('price', models.FloatField(default=0)), | ||||||
|  |                 ('price_currency', models.CharField(default='USD', max_length=3)), | ||||||
|  |                 ('converted_price', models.FloatField(null=True)), | ||||||
|  |                 ('converted_currency', models.CharField(max_length=3, null=True)), | ||||||
|  |                 ('ownership_type', models.CharField(choices=[('ph', 'Physical'), ('di', 'Digital'), ('du', 'Digital Upgrade'), ('re', 'Rented'), ('bo', 'Borrowed'), ('tr', 'Trial'), ('de', 'Demo'), ('pi', 'Pirated')], default='di', max_length=2)), | ||||||
|  |                 ('type', models.CharField(choices=[('game', 'Game'), ('dlc', 'DLC'), ('season_pass', 'Season Pass'), ('battle_pass', 'Battle Pass')], default='game', max_length=255)), | ||||||
|  |                 ('name', models.CharField(blank=True, default='', max_length=255, null=True)), | ||||||
|  |                 ('created_at', models.DateTimeField(auto_now_add=True)), | ||||||
|  |                 ('games', models.ManyToManyField(blank=True, related_name='purchases', to='games.game')), | ||||||
|  |                 ('platform', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='games.platform')), | ||||||
|  |                 ('related_purchase', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='related_purchases', to='games.purchase')), | ||||||
|             ], |             ], | ||||||
|         ), |         ), | ||||||
|         migrations.CreateModel( |         migrations.CreateModel( | ||||||
|             name="Session", |             name='Session', | ||||||
|             fields=[ |             fields=[ | ||||||
|                 ( |                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|                     "id", |                 ('timestamp_start', models.DateTimeField()), | ||||||
|                     models.BigAutoField( |                 ('timestamp_end', models.DateTimeField(blank=True, null=True)), | ||||||
|                         auto_created=True, |                 ('duration_manual', models.DurationField(blank=True, default=datetime.timedelta(0), null=True)), | ||||||
|                         primary_key=True, |                 ('duration_calculated', models.DurationField(blank=True, null=True)), | ||||||
|                         serialize=False, |                 ('note', models.TextField(blank=True, null=True)), | ||||||
|                         verbose_name="ID", |                 ('emulated', models.BooleanField(default=False)), | ||||||
|                     ), |                 ('created_at', models.DateTimeField(auto_now_add=True)), | ||||||
|                 ), |                 ('modified_at', models.DateTimeField(auto_now=True)), | ||||||
|                 ("timestamp_start", models.DateTimeField()), |                 ('device', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='games.device')), | ||||||
|                 ("timestamp_end", models.DateTimeField()), |                 ('game', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sessions', to='games.game')), | ||||||
|                 ("duration_manual", models.DurationField(blank=True, null=True)), |  | ||||||
|                 ("duration_calculated", models.DurationField(blank=True, null=True)), |  | ||||||
|                 ("note", models.TextField(blank=True, null=True)), |  | ||||||
|                 ( |  | ||||||
|                     "purchase", |  | ||||||
|                     models.ForeignKey( |  | ||||||
|                         on_delete=django.db.models.deletion.CASCADE, |  | ||||||
|                         to="games.purchase", |  | ||||||
|                     ), |  | ||||||
|                 ), |  | ||||||
|             ], |             ], | ||||||
|  |             options={ | ||||||
|  |                 'get_latest_by': 'timestamp_start', | ||||||
|  |             }, | ||||||
|         ), |         ), | ||||||
|     ] |     ] | ||||||
|  | |||||||
| @ -1,22 +0,0 @@ | |||||||
| # Generated by Django 4.1.4 on 2023-01-02 18:55 |  | ||||||
|  |  | ||||||
| import datetime |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0001_initial"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="session", |  | ||||||
|             name="duration_manual", |  | ||||||
|             field=models.DurationField( |  | ||||||
|                 blank=True, default=datetime.timedelta(0), null=True |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
							
								
								
									
										18
									
								
								games/migrations/0002_purchase_price_per_game.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								games/migrations/0002_purchase_price_per_game.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | # Generated by Django 5.1.5 on 2025-01-30 11:04 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ('games', '0001_initial'), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='purchase', | ||||||
|  |             name='price_per_game', | ||||||
|  |             field=models.FloatField(null=True), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -1,23 +0,0 @@ | |||||||
| # Generated by Django 4.1.4 on 2023-01-02 23:11 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0002_alter_session_duration_manual"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="session", |  | ||||||
|             name="duration_manual", |  | ||||||
|             field=models.DurationField(blank=True, null=True), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="session", |  | ||||||
|             name="timestamp_end", |  | ||||||
|             field=models.DateTimeField(blank=True, null=True), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
							
								
								
									
										18
									
								
								games/migrations/0003_purchase_updated_at.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								games/migrations/0003_purchase_updated_at.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | # Generated by Django 5.1.5 on 2025-01-30 11:12 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ('games', '0002_purchase_price_per_game'), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='purchase', | ||||||
|  |             name='updated_at', | ||||||
|  |             field=models.DateTimeField(auto_now=True), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -1,22 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-01-09 14:49 |  | ||||||
|  |  | ||||||
| import datetime |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0003_alter_session_duration_manual_and_more"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="session", |  | ||||||
|             name="duration_manual", |  | ||||||
|             field=models.DurationField( |  | ||||||
|                 blank=True, default=datetime.timedelta(0), null=True |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
							
								
								
									
										28
									
								
								games/migrations/0004_purchase_num_purchases.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								games/migrations/0004_purchase_num_purchases.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | |||||||
|  | # Generated by Django 5.1.5 on 2025-01-30 11:57 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  | from django.db.models import Count | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def initialize_num_purchases(apps, schema_editor): | ||||||
|  |     Purchase = apps.get_model("games", "Purchase") | ||||||
|  |     purchases = Purchase.objects.annotate(num_games=Count("games")) | ||||||
|  |  | ||||||
|  |     for purchase in purchases: | ||||||
|  |         purchase.num_purchases = purchase.num_games | ||||||
|  |         purchase.save(update_fields=["num_purchases"]) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |     dependencies = [ | ||||||
|  |         ("games", "0003_purchase_updated_at"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name="purchase", | ||||||
|  |             name="num_purchases", | ||||||
|  |             field=models.IntegerField(default=0), | ||||||
|  |         ), | ||||||
|  |         migrations.RunPython(initialize_num_purchases), | ||||||
|  |     ] | ||||||
| @ -1,35 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-01-09 17:43 |  | ||||||
|  |  | ||||||
| from datetime import timedelta |  | ||||||
|  |  | ||||||
| from django.db import migrations |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def set_duration_calculated_none_to_zero(apps, schema_editor): |  | ||||||
|     Session = apps.get_model("games", "Session") |  | ||||||
|     for session in Session.objects.all(): |  | ||||||
|         if session.duration_calculated == None: |  | ||||||
|             session.duration_calculated = timedelta(0) |  | ||||||
|             session.save() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def revert_set_duration_calculated_none_to_zero(apps, schema_editor): |  | ||||||
|     Session = apps.get_model("games", "Session") |  | ||||||
|     for session in Session.objects.all(): |  | ||||||
|         if session.duration_calculated == timedelta(0): |  | ||||||
|             session.duration_calculated = None |  | ||||||
|             session.save() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0004_alter_session_duration_manual"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.RunPython( |  | ||||||
|             set_duration_calculated_none_to_zero, |  | ||||||
|             revert_set_duration_calculated_none_to_zero, |  | ||||||
|         ) |  | ||||||
|     ] |  | ||||||
| @ -1,35 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-01-09 18:04 |  | ||||||
|  |  | ||||||
| from datetime import timedelta |  | ||||||
|  |  | ||||||
| from django.db import migrations |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def set_duration_manual_none_to_zero(apps, schema_editor): |  | ||||||
|     Session = apps.get_model("games", "Session") |  | ||||||
|     for session in Session.objects.all(): |  | ||||||
|         if session.duration_manual == None: |  | ||||||
|             session.duration_manual = timedelta(0) |  | ||||||
|             session.save() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def revert_set_duration_manual_none_to_zero(apps, schema_editor): |  | ||||||
|     Session = apps.get_model("games", "Session") |  | ||||||
|     for session in Session.objects.all(): |  | ||||||
|         if session.duration_manual == timedelta(0): |  | ||||||
|             session.duration_manual = None |  | ||||||
|             session.save() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0005_auto_20230109_1843"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.RunPython( |  | ||||||
|             set_duration_manual_none_to_zero, |  | ||||||
|             revert_set_duration_manual_none_to_zero, |  | ||||||
|         ) |  | ||||||
|     ] |  | ||||||
| @ -1,35 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-01-19 18:30 |  | ||||||
|  |  | ||||||
| import django.db.models.deletion |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0006_auto_20230109_1904"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="purchase", |  | ||||||
|             name="game", |  | ||||||
|             field=models.ForeignKey( |  | ||||||
|                 on_delete=django.db.models.deletion.CASCADE, to="games.game" |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="purchase", |  | ||||||
|             name="platform", |  | ||||||
|             field=models.ForeignKey( |  | ||||||
|                 on_delete=django.db.models.deletion.CASCADE, to="games.platform" |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="session", |  | ||||||
|             name="purchase", |  | ||||||
|             field=models.ForeignKey( |  | ||||||
|                 on_delete=django.db.models.deletion.CASCADE, to="games.purchase" |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,41 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-02-18 16:29 |  | ||||||
|  |  | ||||||
| import django.db.models.deletion |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0007_alter_purchase_game_alter_purchase_platform_and_more"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.CreateModel( |  | ||||||
|             name="Edition", |  | ||||||
|             fields=[ |  | ||||||
|                 ( |  | ||||||
|                     "id", |  | ||||||
|                     models.BigAutoField( |  | ||||||
|                         auto_created=True, |  | ||||||
|                         primary_key=True, |  | ||||||
|                         serialize=False, |  | ||||||
|                         verbose_name="ID", |  | ||||||
|                     ), |  | ||||||
|                 ), |  | ||||||
|                 ("name", models.CharField(max_length=255)), |  | ||||||
|                 ( |  | ||||||
|                     "game", |  | ||||||
|                     models.ForeignKey( |  | ||||||
|                         on_delete=django.db.models.deletion.CASCADE, to="games.game" |  | ||||||
|                     ), |  | ||||||
|                 ), |  | ||||||
|                 ( |  | ||||||
|                     "platform", |  | ||||||
|                     models.ForeignKey( |  | ||||||
|                         on_delete=django.db.models.deletion.CASCADE, to="games.platform" |  | ||||||
|                     ), |  | ||||||
|                 ), |  | ||||||
|             ], |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,34 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-02-18 18:51 |  | ||||||
|  |  | ||||||
| from django.db import migrations |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def create_edition_of_game(apps, schema_editor): |  | ||||||
|     Game = apps.get_model("games", "Game") |  | ||||||
|     Edition = apps.get_model("games", "Edition") |  | ||||||
|     Platform = apps.get_model("games", "Platform") |  | ||||||
|     first_platform = Platform.objects.first() |  | ||||||
|     all_games = Game.objects.all() |  | ||||||
|     all_editions = Edition.objects.all() |  | ||||||
|     for game in all_games: |  | ||||||
|         existing_edition = None |  | ||||||
|         try: |  | ||||||
|             existing_edition = all_editions.objects.get(game=game.id) |  | ||||||
|         except: |  | ||||||
|             pass |  | ||||||
|         if existing_edition == None: |  | ||||||
|             edition = Edition() |  | ||||||
|             edition.id = game.id |  | ||||||
|             edition.game = game |  | ||||||
|             edition.name = game.name |  | ||||||
|             edition.platform = first_platform |  | ||||||
|             edition.save() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0008_edition"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [migrations.RunPython(create_edition_of_game)] |  | ||||||
| @ -1,21 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-02-18 19:06 |  | ||||||
|  |  | ||||||
| import django.db.models.deletion |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0009_create_editions"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="purchase", |  | ||||||
|             name="game", |  | ||||||
|             field=models.ForeignKey( |  | ||||||
|                 on_delete=django.db.models.deletion.CASCADE, to="games.edition" |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,18 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-02-18 19:18 |  | ||||||
|  |  | ||||||
| from django.db import migrations |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0010_alter_purchase_game"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.RenameField( |  | ||||||
|             model_name="purchase", |  | ||||||
|             old_name="game", |  | ||||||
|             new_name="edition", |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,23 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-02-18 19:53 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0011_rename_game_purchase_edition"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="purchase", |  | ||||||
|             name="price", |  | ||||||
|             field=models.IntegerField(default=0), |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="purchase", |  | ||||||
|             name="price_currency", |  | ||||||
|             field=models.CharField(default="USD", max_length=3), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,31 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-02-18 19:54 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0012_purchase_price_purchase_price_currency"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="purchase", |  | ||||||
|             name="ownership_type", |  | ||||||
|             field=models.CharField( |  | ||||||
|                 choices=[ |  | ||||||
|                     ("ph", "Physical"), |  | ||||||
|                     ("di", "Digital"), |  | ||||||
|                     ("du", "Digital Upgrade"), |  | ||||||
|                     ("re", "Rented"), |  | ||||||
|                     ("bo", "Borrowed"), |  | ||||||
|                     ("tr", "Trial"), |  | ||||||
|                     ("de", "Demo"), |  | ||||||
|                     ("pi", "Pirated"), |  | ||||||
|                 ], |  | ||||||
|                 default="di", |  | ||||||
|                 max_length=2, |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,52 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-02-18 19:59 |  | ||||||
|  |  | ||||||
| import django.db.models.deletion |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0013_purchase_ownership_type"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.CreateModel( |  | ||||||
|             name="Device", |  | ||||||
|             fields=[ |  | ||||||
|                 ( |  | ||||||
|                     "id", |  | ||||||
|                     models.BigAutoField( |  | ||||||
|                         auto_created=True, |  | ||||||
|                         primary_key=True, |  | ||||||
|                         serialize=False, |  | ||||||
|                         verbose_name="ID", |  | ||||||
|                     ), |  | ||||||
|                 ), |  | ||||||
|                 ("name", models.CharField(max_length=255)), |  | ||||||
|                 ( |  | ||||||
|                     "type", |  | ||||||
|                     models.CharField( |  | ||||||
|                         choices=[ |  | ||||||
|                             ("pc", "PC"), |  | ||||||
|                             ("co", "Console"), |  | ||||||
|                             ("ha", "Handheld"), |  | ||||||
|                             ("mo", "Mobile"), |  | ||||||
|                             ("sbc", "Single-board computer"), |  | ||||||
|                         ], |  | ||||||
|                         default="pc", |  | ||||||
|                         max_length=3, |  | ||||||
|                     ), |  | ||||||
|                 ), |  | ||||||
|             ], |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="session", |  | ||||||
|             name="device", |  | ||||||
|             field=models.ForeignKey( |  | ||||||
|                 null=True, |  | ||||||
|                 on_delete=django.db.models.deletion.CASCADE, |  | ||||||
|                 to="games.device", |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,23 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-02-20 14:55 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0014_device_session_device"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="edition", |  | ||||||
|             name="wikidata", |  | ||||||
|             field=models.CharField(blank=True, default=None, max_length=50, null=True), |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="edition", |  | ||||||
|             name="year_released", |  | ||||||
|             field=models.IntegerField(default=2023), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,51 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-11-06 11:10 |  | ||||||
|  |  | ||||||
| import django.db.models.deletion |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0015_edition_wikidata_edition_year_released"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="edition", |  | ||||||
|             name="platform", |  | ||||||
|             field=models.ForeignKey( |  | ||||||
|                 blank=True, |  | ||||||
|                 default=None, |  | ||||||
|                 null=True, |  | ||||||
|                 on_delete=django.db.models.deletion.CASCADE, |  | ||||||
|                 to="games.platform", |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="edition", |  | ||||||
|             name="year_released", |  | ||||||
|             field=models.IntegerField(blank=True, default=None, null=True), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="game", |  | ||||||
|             name="wikidata", |  | ||||||
|             field=models.CharField(blank=True, default=None, max_length=50, null=True), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="platform", |  | ||||||
|             name="group", |  | ||||||
|             field=models.CharField(blank=True, default=None, max_length=255, null=True), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="session", |  | ||||||
|             name="device", |  | ||||||
|             field=models.ForeignKey( |  | ||||||
|                 blank=True, |  | ||||||
|                 default=None, |  | ||||||
|                 null=True, |  | ||||||
|                 on_delete=django.db.models.deletion.CASCADE, |  | ||||||
|                 to="games.device", |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,141 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-11-06 18:14 |  | ||||||
|  |  | ||||||
| import django.db.models.deletion |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def rename_duplicates(apps, schema_editor): |  | ||||||
|     Edition = apps.get_model("games", "Edition") |  | ||||||
|  |  | ||||||
|     duplicates = ( |  | ||||||
|         Edition.objects.values("name", "platform") |  | ||||||
|         .annotate(name_count=models.Count("id")) |  | ||||||
|         .filter(name_count__gt=1) |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     for duplicate in duplicates: |  | ||||||
|         counter = 1 |  | ||||||
|         duplicate_editions = Edition.objects.filter( |  | ||||||
|             name=duplicate["name"], platform_id=duplicate["platform"] |  | ||||||
|         ).order_by("id") |  | ||||||
|  |  | ||||||
|         for edition in duplicate_editions[1:]:  # Skip the first one |  | ||||||
|             edition.name = f"{edition.name} {counter}" |  | ||||||
|             edition.save() |  | ||||||
|             counter += 1 |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def update_game_year(apps, schema_editor): |  | ||||||
|     Game = apps.get_model("games", "Game") |  | ||||||
|     Edition = apps.get_model("games", "Edition") |  | ||||||
|  |  | ||||||
|     for game in Game.objects.filter(year__isnull=True): |  | ||||||
|         # Try to get the first related edition with a non-null year_released |  | ||||||
|         edition = Edition.objects.filter(game=game, year_released__isnull=False).first() |  | ||||||
|         if edition: |  | ||||||
|             # If an edition is found, update the game's year |  | ||||||
|             game.year = edition.year_released |  | ||||||
|             game.save() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|     replaces = [ |  | ||||||
|         ("games", "0016_alter_edition_platform_alter_edition_year_released_and_more"), |  | ||||||
|         ("games", "0017_alter_device_type_alter_purchase_platform"), |  | ||||||
|         ("games", "0018_auto_20231106_1825"), |  | ||||||
|         ("games", "0019_alter_edition_unique_together"), |  | ||||||
|         ("games", "0020_game_year"), |  | ||||||
|         ("games", "0021_auto_20231106_1909"), |  | ||||||
|         ("games", "0022_rename_year_game_year_released"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0015_edition_wikidata_edition_year_released"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="edition", |  | ||||||
|             name="platform", |  | ||||||
|             field=models.ForeignKey( |  | ||||||
|                 blank=True, |  | ||||||
|                 default=None, |  | ||||||
|                 null=True, |  | ||||||
|                 on_delete=django.db.models.deletion.CASCADE, |  | ||||||
|                 to="games.platform", |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="edition", |  | ||||||
|             name="year_released", |  | ||||||
|             field=models.IntegerField(blank=True, default=None, null=True), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="game", |  | ||||||
|             name="wikidata", |  | ||||||
|             field=models.CharField(blank=True, default=None, max_length=50, null=True), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="platform", |  | ||||||
|             name="group", |  | ||||||
|             field=models.CharField(blank=True, default=None, max_length=255, null=True), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="session", |  | ||||||
|             name="device", |  | ||||||
|             field=models.ForeignKey( |  | ||||||
|                 blank=True, |  | ||||||
|                 default=None, |  | ||||||
|                 null=True, |  | ||||||
|                 on_delete=django.db.models.deletion.CASCADE, |  | ||||||
|                 to="games.device", |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="device", |  | ||||||
|             name="type", |  | ||||||
|             field=models.CharField( |  | ||||||
|                 choices=[ |  | ||||||
|                     ("pc", "PC"), |  | ||||||
|                     ("co", "Console"), |  | ||||||
|                     ("ha", "Handheld"), |  | ||||||
|                     ("mo", "Mobile"), |  | ||||||
|                     ("sbc", "Single-board computer"), |  | ||||||
|                     ("un", "Unknown"), |  | ||||||
|                 ], |  | ||||||
|                 default="un", |  | ||||||
|                 max_length=3, |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="purchase", |  | ||||||
|             name="platform", |  | ||||||
|             field=models.ForeignKey( |  | ||||||
|                 blank=True, |  | ||||||
|                 default=None, |  | ||||||
|                 null=True, |  | ||||||
|                 on_delete=django.db.models.deletion.CASCADE, |  | ||||||
|                 to="games.platform", |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|         migrations.RunPython( |  | ||||||
|             code=rename_duplicates, |  | ||||||
|         ), |  | ||||||
|         migrations.AlterUniqueTogether( |  | ||||||
|             name="edition", |  | ||||||
|             unique_together={("name", "platform")}, |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="game", |  | ||||||
|             name="year", |  | ||||||
|             field=models.IntegerField(blank=True, default=None, null=True), |  | ||||||
|         ), |  | ||||||
|         migrations.RunPython( |  | ||||||
|             code=update_game_year, |  | ||||||
|         ), |  | ||||||
|         migrations.RenameField( |  | ||||||
|             model_name="game", |  | ||||||
|             old_name="year", |  | ||||||
|             new_name="year_released", |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,41 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-11-06 16:53 |  | ||||||
|  |  | ||||||
| import django.db.models.deletion |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0016_alter_edition_platform_alter_edition_year_released_and_more"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="device", |  | ||||||
|             name="type", |  | ||||||
|             field=models.CharField( |  | ||||||
|                 choices=[ |  | ||||||
|                     ("pc", "PC"), |  | ||||||
|                     ("co", "Console"), |  | ||||||
|                     ("ha", "Handheld"), |  | ||||||
|                     ("mo", "Mobile"), |  | ||||||
|                     ("sbc", "Single-board computer"), |  | ||||||
|                     ("un", "Unknown"), |  | ||||||
|                 ], |  | ||||||
|                 default="un", |  | ||||||
|                 max_length=3, |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="purchase", |  | ||||||
|             name="platform", |  | ||||||
|             field=models.ForeignKey( |  | ||||||
|                 blank=True, |  | ||||||
|                 default=None, |  | ||||||
|                 null=True, |  | ||||||
|                 on_delete=django.db.models.deletion.CASCADE, |  | ||||||
|                 to="games.platform", |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,34 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-11-06 17:25 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def rename_duplicates(apps, schema_editor): |  | ||||||
|     Edition = apps.get_model("games", "Edition") |  | ||||||
|  |  | ||||||
|     duplicates = ( |  | ||||||
|         Edition.objects.values("name", "platform") |  | ||||||
|         .annotate(name_count=models.Count("id")) |  | ||||||
|         .filter(name_count__gt=1) |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     for duplicate in duplicates: |  | ||||||
|         counter = 1 |  | ||||||
|         duplicate_editions = Edition.objects.filter( |  | ||||||
|             name=duplicate["name"], platform_id=duplicate["platform"] |  | ||||||
|         ).order_by("id") |  | ||||||
|  |  | ||||||
|         for edition in duplicate_editions[1:]:  # Skip the first one |  | ||||||
|             edition.name = f"{edition.name} {counter}" |  | ||||||
|             edition.save() |  | ||||||
|             counter += 1 |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0017_alter_device_type_alter_purchase_platform"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.RunPython(rename_duplicates), |  | ||||||
|     ] |  | ||||||
| @ -1,17 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-11-06 17:26 |  | ||||||
|  |  | ||||||
| from django.db import migrations |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0018_auto_20231106_1825"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AlterUniqueTogether( |  | ||||||
|             name="edition", |  | ||||||
|             unique_together={("name", "platform")}, |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,18 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-11-06 18:05 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0019_alter_edition_unique_together"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="game", |  | ||||||
|             name="year", |  | ||||||
|             field=models.IntegerField(blank=True, default=None, null=True), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,24 +0,0 @@ | |||||||
| from django.db import migrations |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def update_game_year(apps, schema_editor): |  | ||||||
|     Game = apps.get_model("games", "Game") |  | ||||||
|     Edition = apps.get_model("games", "Edition") |  | ||||||
|  |  | ||||||
|     for game in Game.objects.filter(year__isnull=True): |  | ||||||
|         # Try to get the first related edition with a non-null year_released |  | ||||||
|         edition = Edition.objects.filter(game=game, year_released__isnull=False).first() |  | ||||||
|         if edition: |  | ||||||
|             # If an edition is found, update the game's year |  | ||||||
|             game.year = edition.year_released |  | ||||||
|             game.save() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0020_game_year"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.RunPython(update_game_year), |  | ||||||
|     ] |  | ||||||
| @ -1,18 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-11-06 18:12 |  | ||||||
|  |  | ||||||
| from django.db import migrations |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0021_auto_20231106_1909"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.RenameField( |  | ||||||
|             model_name="game", |  | ||||||
|             old_name="year", |  | ||||||
|             new_name="year_released", |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,21 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-11-06 18:24 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ( |  | ||||||
|             "games", |  | ||||||
|             "0016_alter_edition_platform_alter_edition_year_released_and_more_squashed_0022_rename_year_game_year_released", |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="purchase", |  | ||||||
|             name="date_finished", |  | ||||||
|             field=models.DateField(blank=True, null=True), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,39 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-11-09 09:32 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def create_sort_name(apps, schema_editor): |  | ||||||
|     Edition = apps.get_model( |  | ||||||
|         "games", "Edition" |  | ||||||
|     )  # Replace 'your_app_name' with the actual name of your app |  | ||||||
|  |  | ||||||
|     for edition in Edition.objects.all(): |  | ||||||
|         name = edition.name |  | ||||||
|         # Check for articles at the beginning of the name and move them to the end |  | ||||||
|         if name.lower().startswith("the "): |  | ||||||
|             sort_name = f"{name[4:]}, The" |  | ||||||
|         elif name.lower().startswith("a "): |  | ||||||
|             sort_name = f"{name[2:]}, A" |  | ||||||
|         elif name.lower().startswith("an "): |  | ||||||
|             sort_name = f"{name[3:]}, An" |  | ||||||
|         else: |  | ||||||
|             sort_name = name |  | ||||||
|         # Save the sort_name back to the database |  | ||||||
|         edition.sort_name = sort_name |  | ||||||
|         edition.save() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0023_purchase_date_finished"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="edition", |  | ||||||
|             name="sort_name", |  | ||||||
|             field=models.CharField(blank=True, default=None, max_length=255, null=True), |  | ||||||
|         ), |  | ||||||
|         migrations.RunPython(create_sort_name), |  | ||||||
|     ] |  | ||||||
| @ -1,39 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-11-09 09:32 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def create_sort_name(apps, schema_editor): |  | ||||||
|     Game = apps.get_model( |  | ||||||
|         "games", "Game" |  | ||||||
|     )  # Replace 'your_app_name' with the actual name of your app |  | ||||||
|  |  | ||||||
|     for game in Game.objects.all(): |  | ||||||
|         name = game.name |  | ||||||
|         # Check for articles at the beginning of the name and move them to the end |  | ||||||
|         if name.lower().startswith("the "): |  | ||||||
|             sort_name = f"{name[4:]}, The" |  | ||||||
|         elif name.lower().startswith("a "): |  | ||||||
|             sort_name = f"{name[2:]}, A" |  | ||||||
|         elif name.lower().startswith("an "): |  | ||||||
|             sort_name = f"{name[3:]}, An" |  | ||||||
|         else: |  | ||||||
|             sort_name = name |  | ||||||
|         # Save the sort_name back to the database |  | ||||||
|         game.sort_name = sort_name |  | ||||||
|         game.save() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0024_edition_sort_name"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="game", |  | ||||||
|             name="sort_name", |  | ||||||
|             field=models.CharField(blank=True, default=None, max_length=255, null=True), |  | ||||||
|         ), |  | ||||||
|         migrations.RunPython(create_sort_name), |  | ||||||
|     ] |  | ||||||
| @ -1,27 +0,0 @@ | |||||||
| # 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, |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,25 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-11-14 08:41 |  | ||||||
|  |  | ||||||
| import django.db.models.deletion |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| 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", |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,26 +0,0 @@ | |||||||
| # 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), |  | ||||||
|     ] |  | ||||||
| @ -1,26 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-11-14 21:19 |  | ||||||
|  |  | ||||||
| import django.db.models.deletion |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0028_purchase_name"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="purchase", |  | ||||||
|             name="related_purchase", |  | ||||||
|             field=models.ForeignKey( |  | ||||||
|                 blank=True, |  | ||||||
|                 default=None, |  | ||||||
|                 null=True, |  | ||||||
|                 on_delete=django.db.models.deletion.SET_NULL, |  | ||||||
|                 related_name="related_purchases", |  | ||||||
|                 to="games.purchase", |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,18 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-11-15 12:04 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0029_alter_purchase_related_purchase"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="purchase", |  | ||||||
|             name="name", |  | ||||||
|             field=models.CharField(blank=True, default="", max_length=255, null=True), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,44 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-11-15 13:51 |  | ||||||
|  |  | ||||||
| import django.utils.timezone |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0030_alter_purchase_name"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="device", |  | ||||||
|             name="created_at", |  | ||||||
|             field=models.DateTimeField(default=django.utils.timezone.now), |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="edition", |  | ||||||
|             name="created_at", |  | ||||||
|             field=models.DateTimeField(default=django.utils.timezone.now), |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="game", |  | ||||||
|             name="created_at", |  | ||||||
|             field=models.DateTimeField(default=django.utils.timezone.now), |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="platform", |  | ||||||
|             name="created_at", |  | ||||||
|             field=models.DateTimeField(default=django.utils.timezone.now), |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="purchase", |  | ||||||
|             name="created_at", |  | ||||||
|             field=models.DateTimeField(default=django.utils.timezone.now), |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="session", |  | ||||||
|             name="created_at", |  | ||||||
|             field=models.DateTimeField(default=django.utils.timezone.now), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,52 +0,0 @@ | |||||||
| # Generated by Django 4.1.5 on 2023-11-15 18:02 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0031_device_created_at_edition_created_at_game_created_at_and_more"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AlterModelOptions( |  | ||||||
|             name="session", |  | ||||||
|             options={"get_latest_by": "timestamp_start"}, |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="session", |  | ||||||
|             name="modified_at", |  | ||||||
|             field=models.DateTimeField(auto_now=True), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="device", |  | ||||||
|             name="created_at", |  | ||||||
|             field=models.DateTimeField(auto_now_add=True), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="edition", |  | ||||||
|             name="created_at", |  | ||||||
|             field=models.DateTimeField(auto_now_add=True), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="game", |  | ||||||
|             name="created_at", |  | ||||||
|             field=models.DateTimeField(auto_now_add=True), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="platform", |  | ||||||
|             name="created_at", |  | ||||||
|             field=models.DateTimeField(auto_now_add=True), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="purchase", |  | ||||||
|             name="created_at", |  | ||||||
|             field=models.DateTimeField(auto_now_add=True), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="session", |  | ||||||
|             name="created_at", |  | ||||||
|             field=models.DateTimeField(auto_now_add=True), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,17 +0,0 @@ | |||||||
| # Generated by Django 4.2.7 on 2023-11-28 13:43 |  | ||||||
|  |  | ||||||
| from django.db import migrations |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0032_alter_session_options_session_modified_at_and_more"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AlterUniqueTogether( |  | ||||||
|             name="edition", |  | ||||||
|             unique_together={("name", "platform", "year_released")}, |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,23 +0,0 @@ | |||||||
| # Generated by Django 4.2.7 on 2024-01-03 21:27 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0033_alter_edition_unique_together"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="purchase", |  | ||||||
|             name="date_dropped", |  | ||||||
|             field=models.DateField(blank=True, null=True), |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="purchase", |  | ||||||
|             name="infinite", |  | ||||||
|             field=models.BooleanField(default=False), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,25 +0,0 @@ | |||||||
| # Generated by Django 5.1 on 2024-08-11 15:50 |  | ||||||
|  |  | ||||||
| import django.db.models.deletion |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0034_purchase_date_dropped_purchase_infinite"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="session", |  | ||||||
|             name="device", |  | ||||||
|             field=models.ForeignKey( |  | ||||||
|                 blank=True, |  | ||||||
|                 default=None, |  | ||||||
|                 null=True, |  | ||||||
|                 on_delete=django.db.models.deletion.SET_DEFAULT, |  | ||||||
|                 to="games.device", |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,19 +0,0 @@ | |||||||
| # Generated by Django 5.1 on 2024-08-11 16:48 |  | ||||||
|  |  | ||||||
| import django.db.models.deletion |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ('games', '0035_alter_session_device'), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name='edition', |  | ||||||
|             name='platform', |  | ||||||
|             field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='games.platform'), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,26 +0,0 @@ | |||||||
| # Generated by Django 5.1.1 on 2024-09-14 07:05 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
| from django.utils.text import slugify |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def update_empty_icons(apps, schema_editor): |  | ||||||
|     Platform = apps.get_model("games", "Platform") |  | ||||||
|     for platform in Platform.objects.filter(icon=""): |  | ||||||
|         platform.icon = slugify(platform.name) |  | ||||||
|         platform.save() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0036_alter_edition_platform"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="platform", |  | ||||||
|             name="icon", |  | ||||||
|             field=models.SlugField(blank=True), |  | ||||||
|         ), |  | ||||||
|         migrations.RunPython(update_empty_icons), |  | ||||||
|     ] |  | ||||||
| @ -1,18 +0,0 @@ | |||||||
| # Generated by Django 5.1.1 on 2024-10-04 09:23 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ('games', '0037_platform_icon'), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name='purchase', |  | ||||||
|             name='price', |  | ||||||
|             field=models.FloatField(default=0), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,18 +0,0 @@ | |||||||
| # Generated by Django 5.1.2 on 2024-11-09 22:38 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ('games', '0038_alter_purchase_price'), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name='device', |  | ||||||
|             name='type', |  | ||||||
|             field=models.CharField(choices=[('PC', 'PC'), ('Console', 'Console'), ('Handheld', 'Handheld'), ('Mobile', 'Mobile'), ('Single-board computer', 'Single-board computer'), ('Unknown', 'Unknown')], default='Unknown', max_length=255), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,33 +0,0 @@ | |||||||
| # Generated by Django 5.1.2 on 2024-11-09 22:39 |  | ||||||
|  |  | ||||||
| from django.db import migrations |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def update_device_types(apps, schema_editor): |  | ||||||
|     Device = apps.get_model("games", "Device") |  | ||||||
|  |  | ||||||
|     # Mapping of short names to long names |  | ||||||
|     type_map = { |  | ||||||
|         "pc": "PC", |  | ||||||
|         "co": "Console", |  | ||||||
|         "ha": "Handheld", |  | ||||||
|         "mo": "Mobile", |  | ||||||
|         "sbc": "Single-board computer", |  | ||||||
|         "un": "Unknown", |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     # Loop through all devices and update the type field |  | ||||||
|     for device in Device.objects.all(): |  | ||||||
|         if device.type in type_map: |  | ||||||
|             device.type = type_map[device.type] |  | ||||||
|             device.save() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0039_alter_device_type"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.RunPython(update_device_types), |  | ||||||
|     ] |  | ||||||
| @ -1,36 +0,0 @@ | |||||||
| # Generated by Django 5.1.3 on 2024-11-10 15:14 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ('games', '0040_migrate_device_types'), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name='purchase', |  | ||||||
|             name='converted_currency', |  | ||||||
|             field=models.CharField(max_length=3, null=True), |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name='purchase', |  | ||||||
|             name='converted_price', |  | ||||||
|             field=models.FloatField(null=True), |  | ||||||
|         ), |  | ||||||
|         migrations.CreateModel( |  | ||||||
|             name='ExchangeRate', |  | ||||||
|             fields=[ |  | ||||||
|                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |  | ||||||
|                 ('currency_from', models.CharField(max_length=255)), |  | ||||||
|                 ('currency_to', models.CharField(max_length=255)), |  | ||||||
|                 ('year', models.PositiveIntegerField()), |  | ||||||
|                 ('rate', models.FloatField()), |  | ||||||
|             ], |  | ||||||
|             options={ |  | ||||||
|                 'unique_together': {('currency_from', 'currency_to', 'year')}, |  | ||||||
|             }, |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,18 +0,0 @@ | |||||||
| # Generated by Django 5.1.3 on 2025-01-07 20:14 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ('games', '0041_purchase_converted_currency_purchase_converted_price_and_more'), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name='purchase', |  | ||||||
|             name='editions_temp', |  | ||||||
|             field=models.ManyToManyField(blank=True, related_name='temp_purchases', to='games.edition'), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,27 +0,0 @@ | |||||||
| # Generated by Django 5.1.3 on 2025-01-07 20:17 |  | ||||||
|  |  | ||||||
| from django.db import migrations |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def migrate_edition_to_editions_temp(apps, schema_editor): |  | ||||||
|     Purchase = apps.get_model("games", "Purchase") |  | ||||||
|     for purchase in Purchase.objects.all(): |  | ||||||
|         if purchase.edition: |  | ||||||
|             print( |  | ||||||
|                 f"Migrating Purchase {purchase.id} with Edition {purchase.edition.id}" |  | ||||||
|             ) |  | ||||||
|             purchase.editions_temp.add(purchase.edition) |  | ||||||
|             print(purchase.editions_temp.all()) |  | ||||||
|             purchase.save() |  | ||||||
|         else: |  | ||||||
|             print(f"No edition found for Purchase {purchase.id}") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0042_purchase_editions_temp"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.RunPython(migrate_edition_to_editions_temp), |  | ||||||
|     ] |  | ||||||
| @ -1,18 +0,0 @@ | |||||||
| # Generated by Django 5.1.3 on 2025-01-07 20:32 |  | ||||||
|  |  | ||||||
| from django.db import migrations |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|     dependencies = [ |  | ||||||
|         ("games", "0043_auto_20250107_2117"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.RemoveField(model_name="purchase", name="edition"), |  | ||||||
|         migrations.RenameField( |  | ||||||
|             model_name="purchase", |  | ||||||
|             old_name="editions_temp", |  | ||||||
|             new_name="editions", |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,18 +0,0 @@ | |||||||
| # Generated by Django 5.1.3 on 2025-01-07 20:37 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ('games', '0044_auto_20250107_2132'), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name='purchase', |  | ||||||
|             name='editions', |  | ||||||
|             field=models.ManyToManyField(blank=True, related_name='purchases', to='games.edition'), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
							
								
								
									
										109
									
								
								games/models.py
									
									
									
									
									
								
							
							
						
						
									
										109
									
								
								games/models.py
									
									
									
									
									
								
							| @ -3,17 +3,24 @@ from datetime import timedelta | |||||||
| from django.core.exceptions import ValidationError | from django.core.exceptions import ValidationError | ||||||
| from django.db import models | from django.db import models | ||||||
| from django.db.models import F, Sum | from django.db.models import F, Sum | ||||||
| from django.template.defaultfilters import slugify | from django.template.defaultfilters import floatformat, pluralize, slugify | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
|  |  | ||||||
| from common.time import format_duration | from common.time import format_duration | ||||||
|  |  | ||||||
|  |  | ||||||
| class Game(models.Model): | class Game(models.Model): | ||||||
|  |     class Meta: | ||||||
|  |         unique_together = [["name", "platform", "year_released"]] | ||||||
|  |  | ||||||
|     name = models.CharField(max_length=255) |     name = models.CharField(max_length=255) | ||||||
|     sort_name = models.CharField(max_length=255, null=True, blank=True, default=None) |     sort_name = models.CharField(max_length=255, null=True, blank=True, default=None) | ||||||
|     year_released = models.IntegerField(null=True, blank=True, default=None) |     year_released = models.IntegerField(null=True, blank=True, default=None) | ||||||
|     wikidata = models.CharField(max_length=50, null=True, blank=True, default=None) |     wikidata = models.CharField(max_length=50, null=True, blank=True, default=None) | ||||||
|  |     platform = models.ForeignKey( | ||||||
|  |         "Platform", on_delete=models.SET_DEFAULT, null=True, blank=True, default=None | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     created_at = models.DateTimeField(auto_now_add=True) |     created_at = models.DateTimeField(auto_now_add=True) | ||||||
|  |  | ||||||
|     session_average: float | int | timedelta | None |     session_average: float | int | timedelta | None | ||||||
| @ -22,6 +29,17 @@ class Game(models.Model): | |||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         return self.name |         return self.name | ||||||
|  |  | ||||||
|  |     def save(self, *args, **kwargs): | ||||||
|  |         if self.platform is None: | ||||||
|  |             self.platform = get_sentinel_platform() | ||||||
|  |         super().save(*args, **kwargs) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_sentinel_platform(): | ||||||
|  |     return Platform.objects.get_or_create( | ||||||
|  |         name="Unspecified", icon="unspecified", group="Unspecified" | ||||||
|  |     )[0] | ||||||
|  |  | ||||||
|  |  | ||||||
| class Platform(models.Model): | class Platform(models.Model): | ||||||
|     name = models.CharField(max_length=255) |     name = models.CharField(max_length=255) | ||||||
| @ -38,35 +56,6 @@ class Platform(models.Model): | |||||||
|         super().save(*args, **kwargs) |         super().save(*args, **kwargs) | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_sentinel_platform(): |  | ||||||
|     return Platform.objects.get_or_create( |  | ||||||
|         name="Unspecified", icon="unspecified", group="Unspecified" |  | ||||||
|     )[0] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Edition(models.Model): |  | ||||||
|     class Meta: |  | ||||||
|         unique_together = [["name", "platform", "year_released"]] |  | ||||||
|  |  | ||||||
|     game = models.ForeignKey(Game, on_delete=models.CASCADE) |  | ||||||
|     name = models.CharField(max_length=255) |  | ||||||
|     sort_name = models.CharField(max_length=255, null=True, blank=True, default=None) |  | ||||||
|     platform = models.ForeignKey( |  | ||||||
|         Platform, on_delete=models.SET_DEFAULT, null=True, blank=True, default=None |  | ||||||
|     ) |  | ||||||
|     year_released = models.IntegerField(null=True, blank=True, default=None) |  | ||||||
|     wikidata = models.CharField(max_length=50, null=True, blank=True, default=None) |  | ||||||
|     created_at = models.DateTimeField(auto_now_add=True) |  | ||||||
|  |  | ||||||
|     def __str__(self): |  | ||||||
|         return self.sort_name |  | ||||||
|  |  | ||||||
|     def save(self, *args, **kwargs): |  | ||||||
|         if self.platform is None: |  | ||||||
|             self.platform = get_sentinel_platform() |  | ||||||
|         super().save(*args, **kwargs) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class PurchaseQueryset(models.QuerySet): | class PurchaseQueryset(models.QuerySet): | ||||||
|     def refunded(self): |     def refunded(self): | ||||||
|         return self.filter(date_refunded__isnull=False) |         return self.filter(date_refunded__isnull=False) | ||||||
| @ -113,7 +102,8 @@ class Purchase(models.Model): | |||||||
|  |  | ||||||
|     objects = PurchaseQueryset().as_manager() |     objects = PurchaseQueryset().as_manager() | ||||||
|  |  | ||||||
|     editions = models.ManyToManyField(Edition, related_name="purchases", blank=True) |     games = models.ManyToManyField(Game, related_name="purchases", blank=True) | ||||||
|  |  | ||||||
|     platform = models.ForeignKey( |     platform = models.ForeignKey( | ||||||
|         Platform, on_delete=models.CASCADE, default=None, null=True, blank=True |         Platform, on_delete=models.CASCADE, default=None, null=True, blank=True | ||||||
|     ) |     ) | ||||||
| @ -126,6 +116,8 @@ class Purchase(models.Model): | |||||||
|     price_currency = models.CharField(max_length=3, default="USD") |     price_currency = models.CharField(max_length=3, default="USD") | ||||||
|     converted_price = models.FloatField(null=True) |     converted_price = models.FloatField(null=True) | ||||||
|     converted_currency = models.CharField(max_length=3, null=True) |     converted_currency = models.CharField(max_length=3, null=True) | ||||||
|  |     price_per_game = models.FloatField(null=True) | ||||||
|  |     num_purchases = models.IntegerField(default=0) | ||||||
|     ownership_type = models.CharField( |     ownership_type = models.CharField( | ||||||
|         max_length=2, choices=OWNERSHIP_TYPES, default=DIGITAL |         max_length=2, choices=OWNERSHIP_TYPES, default=DIGITAL | ||||||
|     ) |     ) | ||||||
| @ -140,23 +132,43 @@ class Purchase(models.Model): | |||||||
|         related_name="related_purchases", |         related_name="related_purchases", | ||||||
|     ) |     ) | ||||||
|     created_at = models.DateTimeField(auto_now_add=True) |     created_at = models.DateTimeField(auto_now_add=True) | ||||||
|  |     updated_at = models.DateTimeField(auto_now=True) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def first_edition(self): |     def standardized_price(self): | ||||||
|         return self.editions.first() |         return ( | ||||||
|  |             f"{floatformat(self.converted_price, 0)} {self.converted_currency}" | ||||||
|  |             if self.converted_price | ||||||
|  |             else None | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def has_one_item(self): | ||||||
|  |         return self.games.count() == 1 | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def standardized_name(self): | ||||||
|  |         return self.name or self.first_game.name | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def first_game(self): | ||||||
|  |         return self.games.first() | ||||||
|  |  | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|  |         return self.standardized_name | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def full_name(self): | ||||||
|         additional_info = [ |         additional_info = [ | ||||||
|             self.get_type_display() if self.type != Purchase.GAME else "", |             str(item) | ||||||
|             ( |             for item in [ | ||||||
|                 f"{self.first_edition.platform} version on {self.platform}" |                 f"{self.num_purchases} game{pluralize(self.num_purchases)}", | ||||||
|                 if self.platform != self.first_edition.platform |                 self.date_purchased, | ||||||
|                 else self.platform |                 self.standardized_price, | ||||||
|             ), |             ] | ||||||
|             self.first_edition.year_released, |             if item | ||||||
|             self.get_ownership_type_display(), |  | ||||||
|         ] |         ] | ||||||
|         return f"{self.first_edition} ({', '.join(filter(None, map(str, additional_info)))})" |         return f"{self.standardized_name} ({', '.join(additional_info)})" | ||||||
|  |  | ||||||
|     def is_game(self): |     def is_game(self): | ||||||
|         return self.type == self.GAME |         return self.type == self.GAME | ||||||
| @ -207,7 +219,14 @@ class Session(models.Model): | |||||||
|     class Meta: |     class Meta: | ||||||
|         get_latest_by = "timestamp_start" |         get_latest_by = "timestamp_start" | ||||||
|  |  | ||||||
|     purchase = models.ForeignKey(Purchase, on_delete=models.CASCADE) |     game = models.ForeignKey( | ||||||
|  |         Game, | ||||||
|  |         on_delete=models.CASCADE, | ||||||
|  |         blank=True, | ||||||
|  |         null=True, | ||||||
|  |         default=None, | ||||||
|  |         related_name="sessions", | ||||||
|  |     ) | ||||||
|     timestamp_start = models.DateTimeField() |     timestamp_start = models.DateTimeField() | ||||||
|     timestamp_end = models.DateTimeField(blank=True, null=True) |     timestamp_end = models.DateTimeField(blank=True, null=True) | ||||||
|     duration_manual = models.DurationField(blank=True, null=True, default=timedelta(0)) |     duration_manual = models.DurationField(blank=True, null=True, default=timedelta(0)) | ||||||
| @ -220,6 +239,8 @@ class Session(models.Model): | |||||||
|         default=None, |         default=None, | ||||||
|     ) |     ) | ||||||
|     note = models.TextField(blank=True, null=True) |     note = models.TextField(blank=True, null=True) | ||||||
|  |     emulated = models.BooleanField(default=False) | ||||||
|  |  | ||||||
|     created_at = models.DateTimeField(auto_now_add=True) |     created_at = models.DateTimeField(auto_now_add=True) | ||||||
|     modified_at = models.DateTimeField(auto_now=True) |     modified_at = models.DateTimeField(auto_now=True) | ||||||
|  |  | ||||||
| @ -227,7 +248,7 @@ class Session(models.Model): | |||||||
|  |  | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         mark = ", manual" if self.is_manual() 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.game)} {str(self.timestamp_start.date())} ({self.duration_formatted()}{mark})" | ||||||
|  |  | ||||||
|     def finish_now(self): |     def finish_now(self): | ||||||
|         self.timestamp_end = timezone.now() |         self.timestamp_end = timezone.now() | ||||||
|  | |||||||
							
								
								
									
										12
									
								
								games/signals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								games/signals.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | from django.db.models.signals import m2m_changed | ||||||
|  | from django.dispatch import receiver | ||||||
|  | from django.utils.timezone import now | ||||||
|  |  | ||||||
|  | from games.models import Purchase | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @receiver(m2m_changed, sender=Purchase.games.through) | ||||||
|  | def update_num_purchases(sender, instance, **kwargs): | ||||||
|  |     instance.num_purchases = instance.games.count() | ||||||
|  |     instance.updated_at = now() | ||||||
|  |     instance.save(update_fields=["num_purchases"]) | ||||||
| @ -1443,10 +1443,6 @@ input:checked + .toggle-bg { | |||||||
|   margin-top: 1rem; |   margin-top: 1rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .ml-4 { |  | ||||||
|   margin-left: 1rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .block { | .block { | ||||||
|   display: block; |   display: block; | ||||||
| } | } | ||||||
| @ -1475,10 +1471,6 @@ input:checked + .toggle-bg { | |||||||
|   display: grid; |   display: grid; | ||||||
| } | } | ||||||
|  |  | ||||||
| .list-item { |  | ||||||
|   display: list-item; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .hidden { | .hidden { | ||||||
|   display: none; |   display: none; | ||||||
| } | } | ||||||
|  | |||||||
| @ -7,7 +7,7 @@ import { | |||||||
|  |  | ||||||
| let syncData = [ | let syncData = [ | ||||||
|   { |   { | ||||||
|     source: "#id_edition", |     source: "#id_games", | ||||||
|     source_value: "dataset.platform", |     source_value: "dataset.platform", | ||||||
|     target: "#id_platform", |     target: "#id_platform", | ||||||
|     target_value: "value", |     target_value: "value", | ||||||
| @ -36,8 +36,8 @@ getEl("#id_type").onchange = () => { | |||||||
|  |  | ||||||
| document.body.addEventListener("htmx:beforeRequest", function (event) { | document.body.addEventListener("htmx:beforeRequest", function (event) { | ||||||
|   // Assuming 'Purchase1' is the element that triggers the HTMX request |   // Assuming 'Purchase1' is the element that triggers the HTMX request | ||||||
|   if (event.target.id === "id_edition") { |   if (event.target.id === "id_games") { | ||||||
|     var idEditionValue = document.getElementById("id_edition").value; |     var idEditionValue = document.getElementById("id_games").value; | ||||||
|  |  | ||||||
|     // Condition to check - replace this with your actual logic |     // Condition to check - replace this with your actual logic | ||||||
|     if (idEditionValue != "") { |     if (idEditionValue != "") { | ||||||
|  | |||||||
| @ -36,7 +36,7 @@ function addToggleButton(targetNode) { | |||||||
|   targetNode.parentElement.appendChild(manualToggleButton); |   targetNode.parentElement.appendChild(manualToggleButton); | ||||||
| } | } | ||||||
|  |  | ||||||
| const toggleableFields = ["#id_game", "#id_edition", "#id_platform"]; | const toggleableFields = ["#id_games", "#id_platform"]; | ||||||
|  |  | ||||||
| toggleableFields.map((selector) => { | toggleableFields.map((selector) => { | ||||||
|   addToggleButton(document.querySelector(selector)); |   addToggleButton(document.querySelector(selector)); | ||||||
|  | |||||||
| @ -1,4 +1,8 @@ | |||||||
| import requests | import requests | ||||||
|  | from django.db.models import ExpressionWrapper, F, FloatField, Q | ||||||
|  | from django.template.defaultfilters import floatformat | ||||||
|  | from django.utils.timezone import now | ||||||
|  | from django_q.models import Task | ||||||
|  |  | ||||||
| from games.models import ExchangeRate, Purchase | from games.models import ExchangeRate, Purchase | ||||||
|  |  | ||||||
| @ -32,26 +36,53 @@ def convert_prices(): | |||||||
|         ).first() |         ).first() | ||||||
|  |  | ||||||
|         if not exchange_rate: |         if not exchange_rate: | ||||||
|  |             print( | ||||||
|  |                 f"Getting exchange rate from {currency_from} to {currency_to} for {year}..." | ||||||
|  |             ) | ||||||
|             try: |             try: | ||||||
|  |                 # this API endpoint only accepts lowercase currency string | ||||||
|                 response = requests.get( |                 response = requests.get( | ||||||
|                     f"https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@{year}-01-01/v1/currencies/{currency_from}.json" |                     f"https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@{year}-01-01/v1/currencies/{currency_from.lower()}.json" | ||||||
|                 ) |                 ) | ||||||
|                 response.raise_for_status() |                 response.raise_for_status() | ||||||
|                 data = response.json() |                 data = response.json() | ||||||
|                 rate = data[currency_from].get(currency_to) |                 currency_from_data = data.get(currency_from.lower()) | ||||||
|  |                 rate = currency_from_data.get(currency_to.lower()) | ||||||
|  |  | ||||||
|                 if rate: |                 if rate: | ||||||
|  |                     print(f"Got {rate}, saving...") | ||||||
|                     exchange_rate = ExchangeRate.objects.create( |                     exchange_rate = ExchangeRate.objects.create( | ||||||
|                         currency_from=currency_from, |                         currency_from=currency_from, | ||||||
|                         currency_to=currency_to, |                         currency_to=currency_to, | ||||||
|                         year=year, |                         year=year, | ||||||
|                         rate=rate, |                         rate=floatformat(rate, 2), | ||||||
|                     ) |                     ) | ||||||
|  |                 else: | ||||||
|  |                     print("Could not get an exchange rate.") | ||||||
|             except requests.RequestException as e: |             except requests.RequestException as e: | ||||||
|                 print( |                 print( | ||||||
|                     f"Failed to fetch exchange rate for {currency_from}->{currency_to} in {year}: {e}" |                     f"Failed to fetch exchange rate for {currency_from}->{currency_to} in {year}: {e}" | ||||||
|                 ) |                 ) | ||||||
|         if exchange_rate: |         if exchange_rate: | ||||||
|             save_converted_info( |             save_converted_info( | ||||||
|                 purchase, purchase.price * exchange_rate.rate, currency_to |                 purchase, | ||||||
|  |                 floatformat(purchase.price * exchange_rate.rate, 0), | ||||||
|  |                 currency_to, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def calculate_price_per_game(): | ||||||
|  |     try: | ||||||
|  |         last_task = Task.objects.filter(group="Update price per game").first() | ||||||
|  |         last_run = last_task.started | ||||||
|  |     except Task.DoesNotExist or AttributeError: | ||||||
|  |         last_run = now() | ||||||
|  |     purchases = Purchase.objects.filter(converted_price__isnull=False).filter( | ||||||
|  |         Q(updated_at__gte=last_run) | Q(price_per_game__isnull=True) | ||||||
|  |     ) | ||||||
|  |     print(f"Updating {purchases.count()} purchases.") | ||||||
|  |     purchases.update( | ||||||
|  |         price_per_game=ExpressionWrapper( | ||||||
|  |             F("converted_price") / F("num_purchases"), output_field=FloatField() | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  | |||||||
| @ -1,12 +0,0 @@ | |||||||
| <c-layouts.add> |  | ||||||
| <c-slot name="additional_row"> |  | ||||||
| <tr> |  | ||||||
|     <td></td> |  | ||||||
|     <td> |  | ||||||
|         <input type="submit" |  | ||||||
|                name="submit_and_redirect" |  | ||||||
|                value="Submit & Create Purchase" /> |  | ||||||
|     </td> |  | ||||||
| </tr> |  | ||||||
| </c-slot> |  | ||||||
| </c-layouts.add> |  | ||||||
| @ -5,7 +5,7 @@ | |||||||
|     <td> |     <td> | ||||||
|         <input type="submit" |         <input type="submit" | ||||||
|                name="submit_and_redirect" |                name="submit_and_redirect" | ||||||
|                value="Submit & Create Edition" /> |                value="Submit & Create Purchase" /> | ||||||
|     </td> |     </td> | ||||||
| </tr> | </tr> | ||||||
| </c-slot> | </c-slot> | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								games/templates/cotton/icon/emulated.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								games/templates/cotton/icon/emulated.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | |||||||
|  | <c-vars title="Emulated" /> | ||||||
|  | <c-svg :title=title viewbox="0 0 48 48"> | ||||||
|  | <c-slot name="path"> | ||||||
|  |     M 8.5 5 C 6.0324991 5 4 7.0324991 4 9.5 L 4 30.5 C 4 32.967501 6.0324991 35 8.5 35 L 17 35 L 17 40 L 13.5 40 A 1.50015 1.50015 0 1 0 13.5 43 L 18.253906 43 A 1.50015 1.50015 0 0 0 18.740234 43 L 29.253906 43 A 1.50015 1.50015 0 0 0 29.740234 43 L 34.5 43 A 1.50015 1.50015 0 1 0 34.5 40 L 31 40 L 31 35 L 39.5 35 C 41.967501 35 44 32.967501 44 30.5 L 44 9.5 C 44 7.0324991 41.967501 5 39.5 5 L 8.5 5 z M 8.5 8 L 39.5 8 C 40.346499 8 41 8.6535009 41 9.5 L 41 30.5 C 41 31.346499 40.346499 32 39.5 32 L 29.746094 32 A 1.50015 1.50015 0 0 0 29.259766 32 L 18.746094 32 A 1.50015 1.50015 0 0 0 18.259766 32 L 8.5 32 C 7.6535009 32 7 31.346499 7 30.5 L 7 9.5 C 7 8.6535009 7.6535009 8 8.5 8 z M 17.5 12 C 16.136406 12 15 13.136406 15 14.5 L 15 25.5 C 15 26.863594 16.136406 28 17.5 28 L 30.5 28 C 31.863594 28 33 26.863594 33 25.5 L 33 14.5 C 33 13.136406 31.863594 12 30.5 12 L 17.5 12 z M 18 18 L 30 18 L 30 25 L 18 25 L 18 18 z M 20 35 L 28 35 L 28 40 L 20 40 L 20 35 z | ||||||
|  | </c-slot> | ||||||
|  | </c-svg> | ||||||
							
								
								
									
										1
									
								
								games/templates/cotton/price_converted.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								games/templates/cotton/price_converted.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | <span title="Price is a result of conversion and rounding." class="decoration-dotted underline">{{ slot }}</span> | ||||||
| @ -36,8 +36,8 @@ | |||||||
|                             <td class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name relative align-top w-24 h-12 group"> |                             <td class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name relative align-top w-24 h-12 group"> | ||||||
|                                 <span class="inline-block relative"> |                                 <span class="inline-block relative"> | ||||||
|                                     <a class="underline decoration-slate-500 sm:decoration-2 inline-block truncate max-w-20char group-hover:absolute group-hover:max-w-none group-hover:-top-8 group-hover:-left-6 group-hover:min-w-60 group-hover:px-6 group-hover:py-3.5 group-hover:bg-purple-600 group-hover:rounded-sm group-hover:outline-dashed group-hover:outline-purple-400 group-hover:outline-4 group-hover:decoration-purple-900 group-hover:text-purple-100" |                                     <a class="underline decoration-slate-500 sm:decoration-2 inline-block truncate max-w-20char group-hover:absolute group-hover:max-w-none group-hover:-top-8 group-hover:-left-6 group-hover:min-w-60 group-hover:px-6 group-hover:py-3.5 group-hover:bg-purple-600 group-hover:rounded-sm group-hover:outline-dashed group-hover:outline-purple-400 group-hover:outline-4 group-hover:decoration-purple-900 group-hover:text-purple-100" | ||||||
|                                        href="{% url 'view_game' session.purchase.edition.game.id %}"> |                                        href="{% url 'view_game' session.game.id %}"> | ||||||
|                                         {{ session.purchase.edition.name }} |                                         {{ session.game.name }} | ||||||
|                                     </a> |                                     </a> | ||||||
|                                 </span> |                                 </span> | ||||||
|                             </td> |                             </td> | ||||||
|  | |||||||
| @ -57,10 +57,6 @@ | |||||||
|                                 <a href="{% url 'add_game' %}" |                                 <a href="{% url 'add_game' %}" | ||||||
|                                    class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Game</a> |                                    class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Game</a> | ||||||
|                             </li> |                             </li> | ||||||
|                             <li> |  | ||||||
|                                 <a href="{% url 'add_edition' %}" |  | ||||||
|                                    class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Edition</a> |  | ||||||
|                             </li> |  | ||||||
|                             <li> |                             <li> | ||||||
|                                 <a href="{% url 'add_platform' %}" |                                 <a href="{% url 'add_platform' %}" | ||||||
|                                    class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Platform</a> |                                    class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Platform</a> | ||||||
| @ -102,10 +98,6 @@ | |||||||
|                                 <a href="{% url 'list_games' %}" |                                 <a href="{% url 'list_games' %}" | ||||||
|                                    class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Games</a> |                                    class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Games</a> | ||||||
|                             </li> |                             </li> | ||||||
|                             <li> |  | ||||||
|                                 <a href="{% url 'list_editions' %}" |  | ||||||
|                                    class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Editions</a> |  | ||||||
|                             </li> |  | ||||||
|                             <li> |                             <li> | ||||||
|                                 <a href="{% url 'list_platforms' %}" |                                 <a href="{% url 'list_platforms' %}" | ||||||
|                                    class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Platforms</a> |                                    class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Platforms</a> | ||||||
|  | |||||||
| @ -2,11 +2,11 @@ | |||||||
| {% load static %} | {% load static %} | ||||||
| {% partialdef purchase-name %} | {% partialdef purchase-name %} | ||||||
| {% if purchase.type != 'game' %} | {% if purchase.type != 'game' %} | ||||||
|     <c-gamelink :game_id=purchase.first_edition.game.id> |     <c-gamelink :game_id=purchase.first_game.id> | ||||||
|     {{ purchase.name }} ({{ purchase.first_edition.edition.name }} {{ purchase.get_type_display }}) |     {{ purchase.name }} ({{ purchase.first_game.name }} {{ purchase.get_type_display }}) | ||||||
|     </c-gamelink> |     </c-gamelink> | ||||||
| {% else %} | {% else %} | ||||||
|     <c-gamelink :game_id=purchase.first_edition.game.id :name=purchase.first_edition.name /> |     <c-gamelink :game_id=purchase.first_game.id :name=purchase.first_game.name /> | ||||||
| {% endif %} | {% endif %} | ||||||
| {% endpartialdef %} | {% endpartialdef %} | ||||||
| <div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto"> | <div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto"> | ||||||
| @ -46,7 +46,7 @@ | |||||||
|             {% endif %} |             {% endif %} | ||||||
|             <tr> |             <tr> | ||||||
|                 <td class="px-2 sm:px-4 md:px-6 md:py-2">Games ({{ year }})</td> |                 <td class="px-2 sm:px-4 md:px-6 md:py-2">Games ({{ year }})</td> | ||||||
|                 <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_2023_games }}</td> |                 <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_year_games }}</td> | ||||||
|             </tr> |             </tr> | ||||||
|             {% if all_finished_this_year_count %} |             {% if all_finished_this_year_count %} | ||||||
|                 <tr> |                 <tr> | ||||||
| @ -148,7 +148,7 @@ | |||||||
|             </tr> |             </tr> | ||||||
|         </tbody> |         </tbody> | ||||||
|     </table> |     </table> | ||||||
|     <h1 class="text-5xl text-center my-6">Top games by playtime</h1> |     <h1 class="text-5xl text-center my-6">Games by playtime</h1> | ||||||
|     <table class="responsive-table"> |     <table class="responsive-table"> | ||||||
|         <thead> |         <thead> | ||||||
|             <tr> |             <tr> | ||||||
|  | |||||||
| @ -67,17 +67,21 @@ | |||||||
|                 </a> |                 </a> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|         <c-h1 :badge="edition_count">Editions</c-h1> |  | ||||||
|         <div class="mb-6"> |  | ||||||
|             <c-simple-table :rows=edition_data.rows :columns=edition_data.columns /> |  | ||||||
|         </div> |  | ||||||
|         <div class="mb-6"> |         <div class="mb-6"> | ||||||
|             <c-h1 :badge="purchase_count">Purchases</c-h1> |             <c-h1 :badge="purchase_count">Purchases</c-h1> | ||||||
|  |             {% if purchase_count %} | ||||||
|             <c-simple-table :rows=purchase_data.rows :columns=purchase_data.columns /> |             <c-simple-table :rows=purchase_data.rows :columns=purchase_data.columns /> | ||||||
|  |             {% else %} | ||||||
|  |             No purchases yet. | ||||||
|  |             {% endif %} | ||||||
|         </div> |         </div> | ||||||
|         <div class="mb-6"> |         <div class="mb-6"> | ||||||
|             <c-h1 :badge="session_count">Sessions</c-h1> |             <c-h1 :badge="session_count">Sessions</c-h1> | ||||||
|  |             {% if session_count %} | ||||||
|             <c-simple-table :rows=session_data.rows :columns=session_data.columns :header_action=session_data.header_action :page_obj=session_page_obj :elided_page_range=session_elided_page_range /> |             <c-simple-table :rows=session_data.rows :columns=session_data.columns :header_action=session_data.header_action :page_obj=session_page_obj :elided_page_range=session_elided_page_range /> | ||||||
|  |             {% else %} | ||||||
|  |             No sessions yet. | ||||||
|  |             {% endif %} | ||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
|     <script> |     <script> | ||||||
|  | |||||||
| @ -2,8 +2,17 @@ | |||||||
|     <div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto"> |     <div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto"> | ||||||
|      |      | ||||||
|     <div class="flex flex-col gap-5 mb-3"> |     <div class="flex flex-col gap-5 mb-3"> | ||||||
|  |         <div class="font-bold font-serif text-slate-500 text-2xl"> | ||||||
|  |         {% if not purchase.name %} | ||||||
|  |             Unnamed purchase | ||||||
|  |         {% else %} | ||||||
|  |             {{ purchase.name }} | ||||||
|  |         {% endif %} | ||||||
|  |         </div> | ||||||
|     <span class="text-balance max-w-[30rem] text-4xl"> |     <span class="text-balance max-w-[30rem] text-4xl"> | ||||||
|         <span class="font-bold font-serif">{% if purchase.name %}{{ purchase.name }}{% else %}Unnamed purchase{% endif %}</span> <span class="text-slate-500 text-2xl">({{ purchase.editions.count }} games)</span> |         <span class="font-bold font-serif"> | ||||||
|  |             {{ purchase.date_purchased }} ({{ purchase.num_purchases }} game{{ purchase.num_purchases|pluralize}}) | ||||||
|  |         </span> | ||||||
|     </span> |     </span> | ||||||
|     <div class="inline-flex rounded-md shadow-sm mb-3" role="group"> |     <div class="inline-flex rounded-md shadow-sm mb-3" role="group"> | ||||||
|         <a href="{% url 'edit_purchase' purchase.id %}"> |         <a href="{% url 'edit_purchase' purchase.id %}"> | ||||||
| @ -19,12 +28,19 @@ | |||||||
|             </button> |             </button> | ||||||
|         </a> |         </a> | ||||||
|     </div> |     </div> | ||||||
|     <div>Price: {{ purchase.converted_price | floatformat }} {{ purchase.converted_currency }} ({{ purchase.price | floatformat }} {{ purchase.price_currency }})</div> |     <div> | ||||||
|  |         <p> | ||||||
|  |             Price: | ||||||
|  |             <c-price-converted>{{ purchase.standardized_price }}</c-price-converted> | ||||||
|  |              ({{ purchase.price | floatformat:2 }} {{ purchase.price_currency }}) | ||||||
|  |         </p> | ||||||
|  |         <p>Price per game: <c-price-converted>{{ purchase.price_per_game | floatformat:0 }} {{ purchase.converted_currency }}</c-price-converted> </p> | ||||||
|  |     </div> | ||||||
|     <div> |     <div> | ||||||
|         <h2 class="text-base">Items:</h2> |         <h2 class="text-base">Items:</h2> | ||||||
|         <ul class="list-disc list-inside"> |         <ul class="list-disc list-inside"> | ||||||
|         {% for edition in purchase.editions.all %} |         {% for game in purchase.games.all %} | ||||||
|         <li><c-gamelink :game_id=edition.game.id :name=edition.name /></li> |         <li><c-gamelink :game_id=game.id :name=game.name /></li> | ||||||
|         {% endfor %} |         {% endfor %} | ||||||
|         </ul> |         </ul> | ||||||
|     </div> |     </div> | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| from django.urls import path | from django.urls import path | ||||||
|  |  | ||||||
| from games.views import device, edition, game, general, platform, purchase, session | from games.views import device, game, general, platform, purchase, session | ||||||
|  |  | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|     path("", general.index, name="index"), |     path("", general.index, name="index"), | ||||||
| @ -8,19 +8,6 @@ urlpatterns = [ | |||||||
|     path("device/delete/<int:device_id>", device.delete_device, name="delete_device"), |     path("device/delete/<int:device_id>", device.delete_device, name="delete_device"), | ||||||
|     path("device/edit/<int:device_id>", device.edit_device, name="edit_device"), |     path("device/edit/<int:device_id>", device.edit_device, name="edit_device"), | ||||||
|     path("device/list", device.list_devices, name="list_devices"), |     path("device/list", device.list_devices, name="list_devices"), | ||||||
|     path("edition/add", edition.add_edition, name="add_edition"), |  | ||||||
|     path( |  | ||||||
|         "edition/add/for-game/<int:game_id>", |  | ||||||
|         edition.add_edition, |  | ||||||
|         name="add_edition_for_game", |  | ||||||
|     ), |  | ||||||
|     path("edition/<int:edition_id>/edit", edition.edit_edition, name="edit_edition"), |  | ||||||
|     path("edition/list", edition.list_editions, name="list_editions"), |  | ||||||
|     path( |  | ||||||
|         "edition/<int:edition_id>/delete", |  | ||||||
|         edition.delete_edition, |  | ||||||
|         name="delete_edition", |  | ||||||
|     ), |  | ||||||
|     path("game/add", game.add_game, name="add_game"), |     path("game/add", game.add_game, name="add_game"), | ||||||
|     path("game/<int:game_id>/edit", game.edit_game, name="edit_game"), |     path("game/<int:game_id>/edit", game.edit_game, name="edit_game"), | ||||||
|     path("game/<int:game_id>/view", game.view_game, name="view_game"), |     path("game/<int:game_id>/view", game.view_game, name="view_game"), | ||||||
| @ -39,6 +26,11 @@ urlpatterns = [ | |||||||
|     ), |     ), | ||||||
|     path("platform/list", platform.list_platforms, name="list_platforms"), |     path("platform/list", platform.list_platforms, name="list_platforms"), | ||||||
|     path("purchase/add", purchase.add_purchase, name="add_purchase"), |     path("purchase/add", purchase.add_purchase, name="add_purchase"), | ||||||
|  |     path( | ||||||
|  |         "purchase/add/for-game/<int:game_id>", | ||||||
|  |         purchase.add_purchase, | ||||||
|  |         name="add_purchase_for_game", | ||||||
|  |     ), | ||||||
|     path( |     path( | ||||||
|         "purchase/<int:purchase_id>/edit", |         "purchase/<int:purchase_id>/edit", | ||||||
|         purchase.edit_purchase, |         purchase.edit_purchase, | ||||||
| @ -75,20 +67,15 @@ urlpatterns = [ | |||||||
|         name="refund_purchase", |         name="refund_purchase", | ||||||
|     ), |     ), | ||||||
|     path( |     path( | ||||||
|         "purchase/related-purchase-by-edition", |         "purchase/related-purchase-by-game", | ||||||
|         purchase.related_purchase_by_edition, |         purchase.related_purchase_by_game, | ||||||
|         name="related_purchase_by_edition", |         name="related_purchase_by_game", | ||||||
|     ), |  | ||||||
|     path( |  | ||||||
|         "purchase/add/for-edition/<int:edition_id>", |  | ||||||
|         purchase.add_purchase, |  | ||||||
|         name="add_purchase_for_edition", |  | ||||||
|     ), |     ), | ||||||
|     path("session/add", session.add_session, name="add_session"), |     path("session/add", session.add_session, name="add_session"), | ||||||
|     path( |     path( | ||||||
|         "session/add/for-purchase/<int:purchase_id>", |         "session/add/for-game/<int:game_id>", | ||||||
|         session.add_session, |         session.add_session, | ||||||
|         name="add_session_for_purchase", |         name="add_session_for_game", | ||||||
|     ), |     ), | ||||||
|     path( |     path( | ||||||
|         "session/add/from-game/<int:session_id>", |         "session/add/from-game/<int:session_id>", | ||||||
|  | |||||||
| @ -1,154 +0,0 @@ | |||||||
| from typing import Any |  | ||||||
|  |  | ||||||
| from django.contrib.auth.decorators import login_required |  | ||||||
| from django.core.paginator import Paginator |  | ||||||
| from django.http import HttpRequest, HttpResponse, HttpResponseRedirect |  | ||||||
| from django.shortcuts import get_object_or_404, redirect, render |  | ||||||
| from django.template.loader import render_to_string |  | ||||||
| from django.urls import reverse |  | ||||||
|  |  | ||||||
| from common.components import ( |  | ||||||
|     A, |  | ||||||
|     Button, |  | ||||||
|     Icon, |  | ||||||
|     LinkedNameWithPlatformIcon, |  | ||||||
|     PopoverTruncated, |  | ||||||
| ) |  | ||||||
| from common.time import dateformat, local_strftime |  | ||||||
| from games.forms import EditionForm |  | ||||||
| from games.models import Edition, Game |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @login_required |  | ||||||
| def list_editions(request: HttpRequest) -> HttpResponse: |  | ||||||
|     context: dict[Any, Any] = {} |  | ||||||
|     page_number = request.GET.get("page", 1) |  | ||||||
|     limit = request.GET.get("limit", 10) |  | ||||||
|     editions = Edition.objects.order_by("-created_at") |  | ||||||
|     page_obj = None |  | ||||||
|     if int(limit) != 0: |  | ||||||
|         paginator = Paginator(editions, limit) |  | ||||||
|         page_obj = paginator.get_page(page_number) |  | ||||||
|         editions = page_obj.object_list |  | ||||||
|  |  | ||||||
|     context = { |  | ||||||
|         "title": "Manage editions", |  | ||||||
|         "page_obj": page_obj or None, |  | ||||||
|         "elided_page_range": ( |  | ||||||
|             page_obj.paginator.get_elided_page_range( |  | ||||||
|                 page_number, on_each_side=1, on_ends=1 |  | ||||||
|             ) |  | ||||||
|             if page_obj |  | ||||||
|             else None |  | ||||||
|         ), |  | ||||||
|         "data": { |  | ||||||
|             "header_action": A([], Button([], "Add edition"), url="add_edition"), |  | ||||||
|             "columns": [ |  | ||||||
|                 "Game", |  | ||||||
|                 "Name", |  | ||||||
|                 "Sort Name", |  | ||||||
|                 "Year", |  | ||||||
|                 "Wikidata", |  | ||||||
|                 "Created", |  | ||||||
|                 "Actions", |  | ||||||
|             ], |  | ||||||
|             "rows": [ |  | ||||||
|                 [ |  | ||||||
|                     LinkedNameWithPlatformIcon( |  | ||||||
|                         name=edition.name, |  | ||||||
|                         game_id=edition.game.id, |  | ||||||
|                         platform=edition.platform, |  | ||||||
|                     ), |  | ||||||
|                     PopoverTruncated( |  | ||||||
|                         edition.name |  | ||||||
|                         if edition.game.name != edition.name |  | ||||||
|                         else "(identical)" |  | ||||||
|                     ), |  | ||||||
|                     PopoverTruncated( |  | ||||||
|                         edition.sort_name |  | ||||||
|                         if edition.sort_name is not None |  | ||||||
|                         and edition.game.name != edition.sort_name |  | ||||||
|                         else "(identical)" |  | ||||||
|                     ), |  | ||||||
|                     edition.year_released, |  | ||||||
|                     edition.wikidata, |  | ||||||
|                     local_strftime(edition.created_at, dateformat), |  | ||||||
|                     render_to_string( |  | ||||||
|                         "cotton/button_group.html", |  | ||||||
|                         { |  | ||||||
|                             "buttons": [ |  | ||||||
|                                 { |  | ||||||
|                                     "href": reverse("edit_edition", args=[edition.pk]), |  | ||||||
|                                     "slot": Icon("edit"), |  | ||||||
|                                     "color": "gray", |  | ||||||
|                                 }, |  | ||||||
|                                 { |  | ||||||
|                                     "href": reverse( |  | ||||||
|                                         "delete_edition", args=[edition.pk] |  | ||||||
|                                     ), |  | ||||||
|                                     "slot": Icon("delete"), |  | ||||||
|                                     "color": "red", |  | ||||||
|                                 }, |  | ||||||
|                             ] |  | ||||||
|                         }, |  | ||||||
|                     ), |  | ||||||
|                 ] |  | ||||||
|                 for edition in editions |  | ||||||
|             ], |  | ||||||
|         }, |  | ||||||
|     } |  | ||||||
|     return render(request, "list_purchases.html", context) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @login_required |  | ||||||
| def edit_edition(request: HttpRequest, edition_id: int = 0) -> HttpResponse: |  | ||||||
|     edition = get_object_or_404(Edition, id=edition_id) |  | ||||||
|     form = EditionForm(request.POST or None, instance=edition) |  | ||||||
|     if form.is_valid(): |  | ||||||
|         form.save() |  | ||||||
|         return redirect("list_editions") |  | ||||||
|  |  | ||||||
|     context: dict[str, Any] = {"form": form, "title": "Edit edition"} |  | ||||||
|     return render(request, "add.html", context) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @login_required |  | ||||||
| def delete_edition(request: HttpRequest, edition_id: int) -> HttpResponse: |  | ||||||
|     edition = get_object_or_404(Edition, id=edition_id) |  | ||||||
|     edition.delete() |  | ||||||
|     return redirect("list_editions") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @login_required |  | ||||||
| def add_edition(request: HttpRequest, game_id: int = 0) -> HttpResponse: |  | ||||||
|     context: dict[str, Any] = {} |  | ||||||
|     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 = get_object_or_404(Game, 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_edition.html", context) |  | ||||||
| @ -14,7 +14,7 @@ from common.components import ( | |||||||
|     Div, |     Div, | ||||||
|     Icon, |     Icon, | ||||||
|     LinkedPurchase, |     LinkedPurchase, | ||||||
|     NameWithPlatformIcon, |     NameWithIcon, | ||||||
|     Popover, |     Popover, | ||||||
|     PopoverTruncated, |     PopoverTruncated, | ||||||
|     PurchasePrice, |     PurchasePrice, | ||||||
| @ -29,7 +29,7 @@ from common.time import ( | |||||||
| ) | ) | ||||||
| from common.utils import safe_division, truncate | from common.utils import safe_division, truncate | ||||||
| from games.forms import GameForm | from games.forms import GameForm | ||||||
| from games.models import Edition, Game, Purchase, Session | from games.models import Game, Purchase | ||||||
| from games.views.general import use_custom_redirect | from games.views.general import use_custom_redirect | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -67,18 +67,7 @@ def list_games(request: HttpRequest) -> HttpResponse: | |||||||
|             ], |             ], | ||||||
|             "rows": [ |             "rows": [ | ||||||
|                 [ |                 [ | ||||||
|                     A( |                     NameWithIcon(game_id=game.pk), | ||||||
|                         [ |  | ||||||
|                             ( |  | ||||||
|                                 "href", |  | ||||||
|                                 reverse( |  | ||||||
|                                     "view_game", |  | ||||||
|                                     args=[game.pk], |  | ||||||
|                                 ), |  | ||||||
|                             ) |  | ||||||
|                         ], |  | ||||||
|                         PopoverTruncated(game.name), |  | ||||||
|                     ), |  | ||||||
|                     PopoverTruncated( |                     PopoverTruncated( | ||||||
|                         game.sort_name |                         game.sort_name | ||||||
|                         if game.sort_name is not None and game.name != game.sort_name |                         if game.sort_name is not None and game.name != game.sort_name | ||||||
| @ -120,7 +109,7 @@ def add_game(request: HttpRequest) -> HttpResponse: | |||||||
|         game = form.save() |         game = form.save() | ||||||
|         if "submit_and_redirect" in request.POST: |         if "submit_and_redirect" in request.POST: | ||||||
|             return HttpResponseRedirect( |             return HttpResponseRedirect( | ||||||
|                 reverse("add_edition_for_game", kwargs={"game_id": game.id}) |                 reverse("add_purchase_for_game", kwargs={"game_id": game.id}) | ||||||
|             ) |             ) | ||||||
|         else: |         else: | ||||||
|             return redirect("list_games") |             return redirect("list_games") | ||||||
| @ -169,23 +158,14 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse: | |||||||
|         ), |         ), | ||||||
|         to_attr="game_purchases", |         to_attr="game_purchases", | ||||||
|     ) |     ) | ||||||
|     editions = ( |  | ||||||
|         Edition.objects.filter(game=game) |  | ||||||
|         .prefetch_related(game_purchases_prefetch) |  | ||||||
|         .order_by("year_released") |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     purchases = Purchase.objects.filter(editions__game=game).order_by("date_purchased") |     purchases = game.purchases.order_by("date_purchased") | ||||||
|  |  | ||||||
|     sessions = Session.objects.prefetch_related("device").filter( |     sessions = game.sessions | ||||||
|         purchase__editions__game=game |  | ||||||
|     ) |  | ||||||
|     session_count = sessions.count() |     session_count = sessions.count() | ||||||
|     session_count_without_manual = ( |     session_count_without_manual = game.sessions.without_manual().count() | ||||||
|         Session.objects.without_manual().filter(purchase__editions__game=game).count() |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     if sessions: |     if sessions.exists(): | ||||||
|         playrange_start = local_strftime(sessions.earliest().timestamp_start, "%b %Y") |         playrange_start = local_strftime(sessions.earliest().timestamp_start, "%b %Y") | ||||||
|         latest_session = sessions.latest() |         latest_session = sessions.latest() | ||||||
|         playrange_end = local_strftime(latest_session.timestamp_start, "%b %Y") |         playrange_end = local_strftime(latest_session.timestamp_start, "%b %Y") | ||||||
| @ -204,41 +184,6 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse: | |||||||
|         format_duration(sessions.calculated_duration_unformatted(), "%2.1H") |         format_duration(sessions.calculated_duration_unformatted(), "%2.1H") | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     edition_data: dict[str, Any] = { |  | ||||||
|         "columns": [ |  | ||||||
|             "Name", |  | ||||||
|             "Year Released", |  | ||||||
|             "Actions", |  | ||||||
|         ], |  | ||||||
|         "rows": [ |  | ||||||
|             [ |  | ||||||
|                 NameWithPlatformIcon( |  | ||||||
|                     name=edition.name, |  | ||||||
|                     platform=edition.platform, |  | ||||||
|                 ), |  | ||||||
|                 edition.year_released, |  | ||||||
|                 render_to_string( |  | ||||||
|                     "cotton/button_group.html", |  | ||||||
|                     { |  | ||||||
|                         "buttons": [ |  | ||||||
|                             { |  | ||||||
|                                 "href": reverse("edit_edition", args=[edition.pk]), |  | ||||||
|                                 "slot": Icon("edit"), |  | ||||||
|                                 "color": "gray", |  | ||||||
|                             }, |  | ||||||
|                             { |  | ||||||
|                                 "href": reverse("delete_edition", args=[edition.pk]), |  | ||||||
|                                 "slot": Icon("delete"), |  | ||||||
|                                 "color": "red", |  | ||||||
|                             }, |  | ||||||
|                         ] |  | ||||||
|                     }, |  | ||||||
|                 ), |  | ||||||
|             ] |  | ||||||
|             for edition in editions |  | ||||||
|         ], |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     purchase_data: dict[str, Any] = { |     purchase_data: dict[str, Any] = { | ||||||
|         "columns": ["Name", "Type", "Date", "Price", "Actions"], |         "columns": ["Name", "Type", "Date", "Price", "Actions"], | ||||||
|         "rows": [ |         "rows": [ | ||||||
| @ -269,9 +214,8 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse: | |||||||
|         ], |         ], | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     sessions_all = Session.objects.filter(purchase__editions__game=game).order_by( |     sessions_all = game.sessions.order_by("-timestamp_start") | ||||||
|         "-timestamp_start" |  | ||||||
|     ) |  | ||||||
|     last_session = None |     last_session = None | ||||||
|     if sessions_all.exists(): |     if sessions_all.exists(): | ||||||
|         last_session = sessions_all.latest() |         last_session = sessions_all.latest() | ||||||
| @ -298,7 +242,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse: | |||||||
|                         args=[last_session.pk], |                         args=[last_session.pk], | ||||||
|                     ), |                     ), | ||||||
|                     children=Popover( |                     children=Popover( | ||||||
|                         popover_content=last_session.purchase.first_edition.name, |                         popover_content=last_session.game.name, | ||||||
|                         children=[ |                         children=[ | ||||||
|                             Button( |                             Button( | ||||||
|                                 icon=True, |                                 icon=True, | ||||||
| @ -306,9 +250,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse: | |||||||
|                                 size="xs", |                                 size="xs", | ||||||
|                                 children=[ |                                 children=[ | ||||||
|                                     Icon("play"), |                                     Icon("play"), | ||||||
|                                     truncate( |                                     truncate(f"{last_session.game.name}"), | ||||||
|                                         f"{last_session.purchase.first_edition.name}" |  | ||||||
|                                     ), |  | ||||||
|                                 ], |                                 ], | ||||||
|                             ) |                             ) | ||||||
|                         ], |                         ], | ||||||
| @ -318,16 +260,13 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse: | |||||||
|                 else "", |                 else "", | ||||||
|             ], |             ], | ||||||
|         ), |         ), | ||||||
|         "columns": ["Edition", "Date", "Duration", "Actions"], |         "columns": ["Game", "Date", "Duration", "Actions"], | ||||||
|         "rows": [ |         "rows": [ | ||||||
|             [ |             [ | ||||||
|                 NameWithPlatformIcon( |                 NameWithIcon( | ||||||
|                     name=session.purchase.name |                     session_id=session.pk, | ||||||
|                     if session.purchase.name |  | ||||||
|                     else session.purchase.first_edition.name, |  | ||||||
|                     platform=session.purchase.platform, |  | ||||||
|                 ), |                 ), | ||||||
|                 f"{local_strftime(session.timestamp_start)}{f" — {local_strftime(session.timestamp_end, timeformat)}" if session.timestamp_end else ""}", |                 f"{local_strftime(session.timestamp_start)}{f' — {local_strftime(session.timestamp_end, timeformat)}' if session.timestamp_end else ''}", | ||||||
|                 ( |                 ( | ||||||
|                     format_duration(session.duration_calculated, durationformat) |                     format_duration(session.duration_calculated, durationformat) | ||||||
|                     if session.duration_calculated |                     if session.duration_calculated | ||||||
| @ -371,11 +310,9 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse: | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     context: dict[str, Any] = { |     context: dict[str, Any] = { | ||||||
|         "edition_count": editions.count(), |  | ||||||
|         "editions": editions, |  | ||||||
|         "game": game, |         "game": game, | ||||||
|         "playrange": playrange, |         "playrange": playrange, | ||||||
|         "purchase_count": Purchase.objects.filter(editions__game=game).count(), |         "purchase_count": game.purchases.count(), | ||||||
|         "session_average_without_manual": round( |         "session_average_without_manual": round( | ||||||
|             safe_division( |             safe_division( | ||||||
|                 total_hours_without_manual, int(session_count_without_manual) |                 total_hours_without_manual, int(session_count_without_manual) | ||||||
| @ -386,7 +323,6 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse: | |||||||
|         "sessions": sessions, |         "sessions": sessions, | ||||||
|         "title": f"Game Overview - {game.name}", |         "title": f"Game Overview - {game.name}", | ||||||
|         "hours_sum": total_hours, |         "hours_sum": total_hours, | ||||||
|         "edition_data": edition_data, |  | ||||||
|         "purchase_data": purchase_data, |         "purchase_data": purchase_data, | ||||||
|         "session_data": session_data, |         "session_data": session_data, | ||||||
|         "session_page_obj": session_page_obj, |         "session_page_obj": session_page_obj, | ||||||
|  | |||||||
| @ -11,13 +11,12 @@ from django.urls import reverse | |||||||
|  |  | ||||||
| from common.time import available_stats_year_range, dateformat, format_duration | from common.time import available_stats_year_range, dateformat, format_duration | ||||||
| from common.utils import safe_division | from common.utils import safe_division | ||||||
| from games.models import Edition, Game, Platform, Purchase, Session | from games.models import Game, Platform, Purchase, Session | ||||||
|  |  | ||||||
|  |  | ||||||
| def model_counts(request: HttpRequest) -> dict[str, bool]: | def model_counts(request: HttpRequest) -> dict[str, bool]: | ||||||
|     return { |     return { | ||||||
|         "game_available": Game.objects.exists(), |         "game_available": Game.objects.exists(), | ||||||
|         "edition_available": Edition.objects.exists(), |  | ||||||
|         "platform_available": Platform.objects.exists(), |         "platform_available": Platform.objects.exists(), | ||||||
|         "purchase_available": Purchase.objects.exists(), |         "purchase_available": Purchase.objects.exists(), | ||||||
|         "session_count": Session.objects.exists(), |         "session_count": Session.objects.exists(), | ||||||
| @ -49,9 +48,7 @@ def use_custom_redirect( | |||||||
| @login_required | @login_required | ||||||
| def stats_alltime(request: HttpRequest) -> HttpResponse: | def stats_alltime(request: HttpRequest) -> HttpResponse: | ||||||
|     year = "Alltime" |     year = "Alltime" | ||||||
|     this_year_sessions = Session.objects.all().prefetch_related( |     this_year_sessions = Session.objects.all().prefetch_related(Prefetch("game")) | ||||||
|         Prefetch("purchase__editions") |  | ||||||
|     ) |  | ||||||
|     this_year_sessions_with_durations = this_year_sessions.annotate( |     this_year_sessions_with_durations = this_year_sessions.annotate( | ||||||
|         duration=ExpressionWrapper( |         duration=ExpressionWrapper( | ||||||
|             F("timestamp_end") - F("timestamp_start"), |             F("timestamp_end") - F("timestamp_start"), | ||||||
| @ -59,11 +56,9 @@ def stats_alltime(request: HttpRequest) -> HttpResponse: | |||||||
|         ) |         ) | ||||||
|     ) |     ) | ||||||
|     longest_session = this_year_sessions_with_durations.order_by("-duration").first() |     longest_session = this_year_sessions_with_durations.order_by("-duration").first() | ||||||
|     this_year_games = Game.objects.filter( |     this_year_games = Game.objects.filter(sessions__in=this_year_sessions).distinct() | ||||||
|         editions__purchase__session__in=this_year_sessions |  | ||||||
|     ).distinct() |  | ||||||
|     this_year_games_with_session_counts = this_year_games.annotate( |     this_year_games_with_session_counts = this_year_games.annotate( | ||||||
|         session_count=Count("editions__purchase__session"), |         session_count=Count("sessions"), | ||||||
|     ) |     ) | ||||||
|     game_highest_session_count = this_year_games_with_session_counts.order_by( |     game_highest_session_count = this_year_games_with_session_counts.order_by( | ||||||
|         "-session_count" |         "-session_count" | ||||||
| @ -76,11 +71,11 @@ def stats_alltime(request: HttpRequest) -> HttpResponse: | |||||||
|         .aggregate(dates=Count("date")) |         .aggregate(dates=Count("date")) | ||||||
|     ) |     ) | ||||||
|     this_year_played_purchases = Purchase.objects.filter( |     this_year_played_purchases = Purchase.objects.filter( | ||||||
|         session__in=this_year_sessions |         games__sessions__in=this_year_sessions | ||||||
|     ).distinct() |     ).distinct() | ||||||
|  |  | ||||||
|     this_year_purchases = Purchase.objects.all() |     this_year_purchases = Purchase.objects.all() | ||||||
|     this_year_purchases_with_currency = this_year_purchases.select_related("editions") |     this_year_purchases_with_currency = this_year_purchases.select_related("games") | ||||||
|     this_year_purchases_without_refunded = this_year_purchases_with_currency.filter( |     this_year_purchases_without_refunded = this_year_purchases_with_currency.filter( | ||||||
|         date_refunded=None |         date_refunded=None | ||||||
|     ) |     ) | ||||||
| @ -129,11 +124,10 @@ def stats_alltime(request: HttpRequest) -> HttpResponse: | |||||||
|     total_spent = this_year_spendings["total_spent"] or 0 |     total_spent = this_year_spendings["total_spent"] or 0 | ||||||
|  |  | ||||||
|     games_with_playtime = ( |     games_with_playtime = ( | ||||||
|         Game.objects.filter(editions__purchase__session__in=this_year_sessions) |         Game.objects.filter(sessions__in=this_year_sessions) | ||||||
|         .annotate( |         .annotate( | ||||||
|             total_playtime=Sum( |             total_playtime=Sum( | ||||||
|                 F("editions__purchase__session__duration_calculated") |                 F("sessions__duration_calculated") + F("sessions__duration_manual") | ||||||
|                 + F("editions__purchase__session__duration_manual") |  | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|         .values("id", "name", "total_playtime") |         .values("id", "name", "total_playtime") | ||||||
| @ -148,10 +142,8 @@ def stats_alltime(request: HttpRequest) -> HttpResponse: | |||||||
|         month["playtime"] = format_duration(month["playtime"], "%2.0H") |         month["playtime"] = format_duration(month["playtime"], "%2.0H") | ||||||
|  |  | ||||||
|     highest_session_average_game = ( |     highest_session_average_game = ( | ||||||
|         Game.objects.filter(editions__purchase__session__in=this_year_sessions) |         Game.objects.filter(sessions__in=this_year_sessions) | ||||||
|         .annotate( |         .annotate(session_average=Avg("sessions__duration_calculated")) | ||||||
|             session_average=Avg("editions__purchase__session__duration_calculated") |  | ||||||
|         ) |  | ||||||
|         .order_by("-session_average") |         .order_by("-session_average") | ||||||
|         .first() |         .first() | ||||||
|     ) |     ) | ||||||
| @ -160,9 +152,9 @@ def stats_alltime(request: HttpRequest) -> HttpResponse: | |||||||
|         game["formatted_playtime"] = format_duration(game["total_playtime"], "%2.0H") |         game["formatted_playtime"] = format_duration(game["total_playtime"], "%2.0H") | ||||||
|  |  | ||||||
|     total_playtime_per_platform = ( |     total_playtime_per_platform = ( | ||||||
|         this_year_sessions.values("purchase__platform__name") |         this_year_sessions.values("game__platform__name") | ||||||
|         .annotate(total_playtime=Sum(F("duration_calculated") + F("duration_manual"))) |         .annotate(total_playtime=Sum(F("duration_calculated") + F("duration_manual"))) | ||||||
|         .annotate(platform_name=F("purchase__platform__name")) |         .annotate(platform_name=F("game__platform__name")) | ||||||
|         .values("platform_name", "total_playtime") |         .values("platform_name", "total_playtime") | ||||||
|         .order_by("-total_playtime") |         .order_by("-total_playtime") | ||||||
|     ) |     ) | ||||||
| @ -177,10 +169,10 @@ def stats_alltime(request: HttpRequest) -> HttpResponse: | |||||||
|     last_play_date = "N/A" |     last_play_date = "N/A" | ||||||
|     if this_year_sessions: |     if this_year_sessions: | ||||||
|         first_session = this_year_sessions.earliest() |         first_session = this_year_sessions.earliest() | ||||||
|         first_play_game = first_session.purchase.first_edition.game |         first_play_game = first_session.game | ||||||
|         first_play_date = first_session.timestamp_start.strftime(dateformat) |         first_play_date = first_session.timestamp_start.strftime(dateformat) | ||||||
|         last_session = this_year_sessions.latest() |         last_session = this_year_sessions.latest() | ||||||
|         last_play_game = last_session.purchase.first_edition.game |         last_play_game = last_session.game | ||||||
|         last_play_date = last_session.timestamp_start.strftime(dateformat) |         last_play_date = last_session.timestamp_start.strftime(dateformat) | ||||||
|  |  | ||||||
|     all_purchased_this_year_count = this_year_purchases_with_currency.count() |     all_purchased_this_year_count = this_year_purchases_with_currency.count() | ||||||
| @ -195,7 +187,7 @@ def stats_alltime(request: HttpRequest) -> HttpResponse: | |||||||
|         "total_hours": format_duration( |         "total_hours": format_duration( | ||||||
|             this_year_sessions.total_duration_unformatted(), "%2.0H" |             this_year_sessions.total_duration_unformatted(), "%2.0H" | ||||||
|         ), |         ), | ||||||
|         "total_2023_games": this_year_played_purchases.all().count(), |         "total_year_games": this_year_played_purchases.all().count(), | ||||||
|         "top_10_games_by_playtime": top_10_games_by_playtime, |         "top_10_games_by_playtime": top_10_games_by_playtime, | ||||||
|         "year": year, |         "year": year, | ||||||
|         "total_playtime_per_platform": total_playtime_per_platform, |         "total_playtime_per_platform": total_playtime_per_platform, | ||||||
| @ -228,9 +220,7 @@ def stats_alltime(request: HttpRequest) -> HttpResponse: | |||||||
|             if longest_session |             if longest_session | ||||||
|             else 0 |             else 0 | ||||||
|         ), |         ), | ||||||
|         "longest_session_game": ( |         "longest_session_game": (longest_session.game if longest_session else None), | ||||||
|             longest_session.purchase.first_edition.game if longest_session else None |  | ||||||
|         ), |  | ||||||
|         "highest_session_count": ( |         "highest_session_count": ( | ||||||
|             game_highest_session_count.session_count |             game_highest_session_count.session_count | ||||||
|             if game_highest_session_count |             if game_highest_session_count | ||||||
| @ -268,7 +258,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse: | |||||||
|         return HttpResponseRedirect(reverse("stats_alltime")) |         return HttpResponseRedirect(reverse("stats_alltime")) | ||||||
|     this_year_sessions = Session.objects.filter( |     this_year_sessions = Session.objects.filter( | ||||||
|         timestamp_start__year=year |         timestamp_start__year=year | ||||||
|     ).prefetch_related("purchase__editions") |     ).prefetch_related("game") | ||||||
|     this_year_sessions_with_durations = this_year_sessions.annotate( |     this_year_sessions_with_durations = this_year_sessions.annotate( | ||||||
|         duration=ExpressionWrapper( |         duration=ExpressionWrapper( | ||||||
|             F("timestamp_end") - F("timestamp_start"), |             F("timestamp_end") - F("timestamp_start"), | ||||||
| @ -276,13 +266,11 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse: | |||||||
|         ) |         ) | ||||||
|     ) |     ) | ||||||
|     longest_session = this_year_sessions_with_durations.order_by("-duration").first() |     longest_session = this_year_sessions_with_durations.order_by("-duration").first() | ||||||
|     this_year_games = Game.objects.filter( |     this_year_games = Game.objects.filter(sessions__in=this_year_sessions).distinct() | ||||||
|         edition__purchases__session__in=this_year_sessions |  | ||||||
|     ).distinct() |  | ||||||
|     this_year_games_with_session_counts = this_year_games.annotate( |     this_year_games_with_session_counts = this_year_games.annotate( | ||||||
|         session_count=Count( |         session_count=Count( | ||||||
|             "edition__purchases__session", |             "sessions", | ||||||
|             filter=Q(edition__purchases__session__timestamp_start__year=year), |             filter=Q(sessions__timestamp_start__year=year), | ||||||
|         ) |         ) | ||||||
|     ) |     ) | ||||||
|     game_highest_session_count = this_year_games_with_session_counts.order_by( |     game_highest_session_count = this_year_games_with_session_counts.order_by( | ||||||
| @ -296,11 +284,15 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse: | |||||||
|         .aggregate(dates=Count("date")) |         .aggregate(dates=Count("date")) | ||||||
|     ) |     ) | ||||||
|     this_year_played_purchases = Purchase.objects.filter( |     this_year_played_purchases = Purchase.objects.filter( | ||||||
|         session__in=this_year_sessions |         games__sessions__in=this_year_sessions | ||||||
|  |     ).distinct() | ||||||
|  |  | ||||||
|  |     this_year_played_games = Game.objects.filter( | ||||||
|  |         sessions__in=this_year_sessions | ||||||
|     ).distinct() |     ).distinct() | ||||||
|  |  | ||||||
|     this_year_purchases = Purchase.objects.filter(date_purchased__year=year) |     this_year_purchases = Purchase.objects.filter(date_purchased__year=year) | ||||||
|     this_year_purchases_with_currency = this_year_purchases.prefetch_related("editions") |     this_year_purchases_with_currency = this_year_purchases.prefetch_related("games") | ||||||
|     this_year_purchases_without_refunded = this_year_purchases_with_currency.filter( |     this_year_purchases_without_refunded = this_year_purchases_with_currency.filter( | ||||||
|         date_refunded=None |         date_refunded=None | ||||||
|     ).exclude(ownership_type=Purchase.DEMO) |     ).exclude(ownership_type=Purchase.DEMO) | ||||||
| @ -337,7 +329,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse: | |||||||
|  |  | ||||||
|     purchases_finished_this_year = Purchase.objects.filter(date_finished__year=year) |     purchases_finished_this_year = Purchase.objects.filter(date_finished__year=year) | ||||||
|     purchases_finished_this_year_released_this_year = ( |     purchases_finished_this_year_released_this_year = ( | ||||||
|         purchases_finished_this_year.filter(editions__year_released=year).order_by( |         purchases_finished_this_year.filter(games__year_released=year).order_by( | ||||||
|             "date_finished" |             "date_finished" | ||||||
|         ) |         ) | ||||||
|     ) |     ) | ||||||
| @ -351,11 +343,10 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse: | |||||||
|     total_spent = this_year_spendings["total_spent"] or 0 |     total_spent = this_year_spendings["total_spent"] or 0 | ||||||
|  |  | ||||||
|     games_with_playtime = ( |     games_with_playtime = ( | ||||||
|         Game.objects.filter(edition__purchases__session__in=this_year_sessions) |         Game.objects.filter(sessions__in=this_year_sessions) | ||||||
|         .annotate( |         .annotate( | ||||||
|             total_playtime=Sum( |             total_playtime=Sum( | ||||||
|                 F("edition__purchases__session__duration_calculated") |                 F("sessions__duration_calculated") + F("sessions__duration_manual") | ||||||
|                 + F("edition__purchases__session__duration_manual") |  | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|         .values("id", "name", "total_playtime") |         .values("id", "name", "total_playtime") | ||||||
| @ -370,21 +361,19 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse: | |||||||
|         month["playtime"] = format_duration(month["playtime"], "%2.0H") |         month["playtime"] = format_duration(month["playtime"], "%2.0H") | ||||||
|  |  | ||||||
|     highest_session_average_game = ( |     highest_session_average_game = ( | ||||||
|         Game.objects.filter(edition__purchases__session__in=this_year_sessions) |         Game.objects.filter(sessions__in=this_year_sessions) | ||||||
|         .annotate( |         .annotate(session_average=Avg("sessions__duration_calculated")) | ||||||
|             session_average=Avg("edition__purchases__session__duration_calculated") |  | ||||||
|         ) |  | ||||||
|         .order_by("-session_average") |         .order_by("-session_average") | ||||||
|         .first() |         .first() | ||||||
|     ) |     ) | ||||||
|     top_10_games_by_playtime = games_with_playtime.order_by("-total_playtime")[:10] |     top_10_games_by_playtime = games_with_playtime.order_by("-total_playtime") | ||||||
|     for game in top_10_games_by_playtime: |     for game in top_10_games_by_playtime: | ||||||
|         game["formatted_playtime"] = format_duration(game["total_playtime"], "%2.0H") |         game["formatted_playtime"] = format_duration(game["total_playtime"], "%2.0H") | ||||||
|  |  | ||||||
|     total_playtime_per_platform = ( |     total_playtime_per_platform = ( | ||||||
|         this_year_sessions.values("purchase__platform__name") |         this_year_sessions.values("game__platform__name") | ||||||
|         .annotate(total_playtime=Sum(F("duration_calculated") + F("duration_manual"))) |         .annotate(total_playtime=Sum(F("duration_calculated") + F("duration_manual"))) | ||||||
|         .annotate(platform_name=F("purchase__platform__name")) |         .annotate(platform_name=F("game__platform__name")) | ||||||
|         .values("platform_name", "total_playtime") |         .values("platform_name", "total_playtime") | ||||||
|         .order_by("-total_playtime") |         .order_by("-total_playtime") | ||||||
|     ) |     ) | ||||||
| @ -403,10 +392,10 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse: | |||||||
|     last_play_game = None |     last_play_game = None | ||||||
|     if this_year_sessions: |     if this_year_sessions: | ||||||
|         first_session = this_year_sessions.earliest() |         first_session = this_year_sessions.earliest() | ||||||
|         first_play_game = first_session.purchase.first_edition.game |         first_play_game = first_session.game | ||||||
|         first_play_date = first_session.timestamp_start.strftime(dateformat) |         first_play_date = first_session.timestamp_start.strftime(dateformat) | ||||||
|         last_session = this_year_sessions.latest() |         last_session = this_year_sessions.latest() | ||||||
|         last_play_game = last_session.purchase.first_edition.game |         last_play_game = last_session.game | ||||||
|         last_play_date = last_session.timestamp_start.strftime(dateformat) |         last_play_date = last_session.timestamp_start.strftime(dateformat) | ||||||
|  |  | ||||||
|     all_purchased_this_year_count = this_year_purchases_with_currency.count() |     all_purchased_this_year_count = this_year_purchases_with_currency.count() | ||||||
| @ -421,9 +410,9 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse: | |||||||
|         "total_hours": format_duration( |         "total_hours": format_duration( | ||||||
|             this_year_sessions.total_duration_unformatted(), "%2.0H" |             this_year_sessions.total_duration_unformatted(), "%2.0H" | ||||||
|         ), |         ), | ||||||
|         "total_games": this_year_played_purchases.count(), |         "total_games": this_year_played_games.count(), | ||||||
|         "total_2023_games": this_year_played_purchases.filter( |         "total_year_games": this_year_played_purchases.filter( | ||||||
|             editions__year_released=year |             games__year_released=year | ||||||
|         ).count(), |         ).count(), | ||||||
|         "top_10_games_by_playtime": top_10_games_by_playtime, |         "top_10_games_by_playtime": top_10_games_by_playtime, | ||||||
|         "year": year, |         "year": year, | ||||||
| @ -435,15 +424,15 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse: | |||||||
|             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.prefetch_related( |         "all_finished_this_year": purchases_finished_this_year.prefetch_related( | ||||||
|             "editions" |             "games" | ||||||
|         ).order_by("date_finished"), |         ).order_by("date_finished"), | ||||||
|         "all_finished_this_year_count": purchases_finished_this_year.count(), |         "all_finished_this_year_count": purchases_finished_this_year.count(), | ||||||
|         "this_year_finished_this_year": purchases_finished_this_year_released_this_year.prefetch_related( |         "this_year_finished_this_year": purchases_finished_this_year_released_this_year.prefetch_related( | ||||||
|             "editions" |             "games" | ||||||
|         ).order_by("date_finished"), |         ).order_by("date_finished"), | ||||||
|         "this_year_finished_this_year_count": purchases_finished_this_year_released_this_year.count(), |         "this_year_finished_this_year_count": purchases_finished_this_year_released_this_year.count(), | ||||||
|         "purchased_this_year_finished_this_year": purchased_this_year_finished_this_year.prefetch_related( |         "purchased_this_year_finished_this_year": purchased_this_year_finished_this_year.prefetch_related( | ||||||
|             "editions" |             "games" | ||||||
|         ).order_by("date_finished"), |         ).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"], | ||||||
| @ -472,9 +461,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse: | |||||||
|             if longest_session |             if longest_session | ||||||
|             else 0 |             else 0 | ||||||
|         ), |         ), | ||||||
|         "longest_session_game": ( |         "longest_session_game": (longest_session.game if longest_session else None), | ||||||
|             longest_session.purchase.first_edition.game if longest_session else None |  | ||||||
|         ), |  | ||||||
|         "highest_session_count": ( |         "highest_session_count": ( | ||||||
|             game_highest_session_count.session_count |             game_highest_session_count.session_count | ||||||
|             if game_highest_session_count |             if game_highest_session_count | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ from django.utils import timezone | |||||||
| from common.components import A, Button, Icon, LinkedPurchase, PurchasePrice | from common.components import A, Button, Icon, LinkedPurchase, PurchasePrice | ||||||
| from common.time import dateformat | from common.time import dateformat | ||||||
| from games.forms import PurchaseForm | from games.forms import PurchaseForm | ||||||
| from games.models import Edition, Purchase | from games.models import Game, Purchase | ||||||
| from games.views.general import use_custom_redirect | from games.views.general import use_custom_redirect | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -138,7 +138,7 @@ def list_purchases(request: HttpRequest) -> HttpResponse: | |||||||
|  |  | ||||||
|  |  | ||||||
| @login_required | @login_required | ||||||
| def add_purchase(request: HttpRequest, edition_id: int = 0) -> HttpResponse: | def add_purchase(request: HttpRequest, game_id: int = 0) -> HttpResponse: | ||||||
|     context: dict[str, Any] = {} |     context: dict[str, Any] = {} | ||||||
|     initial = {"date_purchased": timezone.now()} |     initial = {"date_purchased": timezone.now()} | ||||||
|  |  | ||||||
| @ -149,19 +149,20 @@ def add_purchase(request: HttpRequest, edition_id: int = 0) -> HttpResponse: | |||||||
|             if "submit_and_redirect" in request.POST: |             if "submit_and_redirect" in request.POST: | ||||||
|                 return HttpResponseRedirect( |                 return HttpResponseRedirect( | ||||||
|                     reverse( |                     reverse( | ||||||
|                         "add_session_for_purchase", kwargs={"purchase_id": purchase.id} |                         "add_session_for_game", | ||||||
|  |                         kwargs={"game_id": purchase.first_game.id}, | ||||||
|                     ) |                     ) | ||||||
|                 ) |                 ) | ||||||
|             else: |             else: | ||||||
|                 return redirect("list_purchases") |                 return redirect("list_purchases") | ||||||
|     else: |     else: | ||||||
|         if edition_id: |         if game_id: | ||||||
|             edition = Edition.objects.get(id=edition_id) |             game = Game.objects.get(id=game_id) | ||||||
|             form = PurchaseForm( |             form = PurchaseForm( | ||||||
|                 initial={ |                 initial={ | ||||||
|                     **initial, |                     **initial, | ||||||
|                     "edition": edition, |                     "games": [game], | ||||||
|                     "platform": edition.platform, |                     "platform": game.platform, | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
|         else: |         else: | ||||||
| @ -199,7 +200,11 @@ def delete_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse: | |||||||
| @login_required | @login_required | ||||||
| def view_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse: | def view_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse: | ||||||
|     purchase = get_object_or_404(Purchase, id=purchase_id) |     purchase = get_object_or_404(Purchase, id=purchase_id) | ||||||
|     return render(request, "view_purchase.html", {"purchase": purchase}) |     return render( | ||||||
|  |         request, | ||||||
|  |         "view_purchase.html", | ||||||
|  |         {"purchase": purchase, "title": f"Purchase: {purchase.full_name}"}, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @login_required | @login_required | ||||||
| @ -226,12 +231,14 @@ def finish_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse: | |||||||
|     return redirect("list_purchases") |     return redirect("list_purchases") | ||||||
|  |  | ||||||
|  |  | ||||||
| def related_purchase_by_edition(request: HttpRequest) -> HttpResponse: | def related_purchase_by_game(request: HttpRequest) -> HttpResponse: | ||||||
|     edition_id = request.GET.get("edition") |     games = request.GET.getlist("games") | ||||||
|     if not edition_id: |     if not games: | ||||||
|         return HttpResponseBadRequest("Invalid edition_id") |         return HttpResponseBadRequest("Invalid game_id") | ||||||
|  |     if isinstance(games, int) or isinstance(games, str): | ||||||
|  |         games = [games] | ||||||
|     form = PurchaseForm() |     form = PurchaseForm() | ||||||
|     form.fields["related_purchase"].queryset = Purchase.objects.filter( |     form.fields["related_purchase"].queryset = Purchase.objects.filter( | ||||||
|         edition_id=edition_id, type=Purchase.GAME |         games__in=games, type=Purchase.GAME | ||||||
|     ).order_by("edition__sort_name") |     ).order_by("games__sort_name") | ||||||
|     return render(request, "partials/related_purchase_field.html", {"form": form}) |     return render(request, "partials/related_purchase_field.html", {"form": form}) | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ from common.components import ( | |||||||
|     Div, |     Div, | ||||||
|     Form, |     Form, | ||||||
|     Icon, |     Icon, | ||||||
|     LinkedNameWithPlatformIcon, |     NameWithIcon, | ||||||
|     Popover, |     Popover, | ||||||
| ) | ) | ||||||
| from common.time import ( | from common.time import ( | ||||||
| @ -28,7 +28,7 @@ from common.time import ( | |||||||
| ) | ) | ||||||
| from common.utils import truncate | from common.utils import truncate | ||||||
| from games.forms import SessionForm | from games.forms import SessionForm | ||||||
| from games.models import Purchase, Session | from games.models import Game, Session | ||||||
| from games.views.general import use_custom_redirect | from games.views.general import use_custom_redirect | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -37,13 +37,13 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse | |||||||
|     context: dict[Any, Any] = {} |     context: dict[Any, Any] = {} | ||||||
|     page_number = request.GET.get("page", 1) |     page_number = request.GET.get("page", 1) | ||||||
|     limit = request.GET.get("limit", 10) |     limit = request.GET.get("limit", 10) | ||||||
|     sessions = Session.objects.order_by("-timestamp_start") |     sessions = Session.objects.order_by("-timestamp_start", "created_at") | ||||||
|     search_string = request.GET.get("search_string", search_string) |     search_string = request.GET.get("search_string", search_string) | ||||||
|     if search_string != "": |     if search_string != "": | ||||||
|         sessions = sessions.filter( |         sessions = sessions.filter( | ||||||
|             Q(purchase__edition__name__icontains=search_string) |             Q(game__name__icontains=search_string) | ||||||
|             | Q(purchase__edition__game__name__icontains=search_string) |             | Q(game__name__icontains=search_string) | ||||||
|             | Q(purchase__platform__name__icontains=search_string) |             | Q(game__platform__name__icontains=search_string) | ||||||
|             | Q(device__name__icontains=search_string) |             | Q(device__name__icontains=search_string) | ||||||
|             | Q(device__type__icontains=search_string) |             | Q(device__type__icontains=search_string) | ||||||
|         ) |         ) | ||||||
| @ -97,7 +97,7 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse | |||||||
|                                     args=[last_session.pk], |                                     args=[last_session.pk], | ||||||
|                                 ), |                                 ), | ||||||
|                                 children=Popover( |                                 children=Popover( | ||||||
|                                     popover_content=last_session.purchase.first_edition.name, |                                     popover_content=last_session.game.name, | ||||||
|                                     children=[ |                                     children=[ | ||||||
|                                         Button( |                                         Button( | ||||||
|                                             icon=True, |                                             icon=True, | ||||||
| @ -105,9 +105,7 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse | |||||||
|                                             size="xs", |                                             size="xs", | ||||||
|                                             children=[ |                                             children=[ | ||||||
|                                                 Icon("play"), |                                                 Icon("play"), | ||||||
|                                                 truncate( |                                                 truncate(f"{last_session.game.name}"), | ||||||
|                                                     f"{last_session.purchase.first_edition.name}" |  | ||||||
|                                                 ), |  | ||||||
|                                             ], |                                             ], | ||||||
|                                         ) |                                         ) | ||||||
|                                     ], |                                     ], | ||||||
| @ -130,12 +128,8 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse | |||||||
|             ], |             ], | ||||||
|             "rows": [ |             "rows": [ | ||||||
|                 [ |                 [ | ||||||
|                     LinkedNameWithPlatformIcon( |                     NameWithIcon(session_id=session.pk), | ||||||
|                         name=session.purchase.first_edition.name, |                     f"{local_strftime(session.timestamp_start)}{f' — {local_strftime(session.timestamp_end, timeformat)}' if session.timestamp_end else ''}", | ||||||
|                         game_id=session.purchase.first_edition.game.pk, |  | ||||||
|                         platform=session.purchase.platform, |  | ||||||
|                     ), |  | ||||||
|                     f"{local_strftime(session.timestamp_start)}{f" — {local_strftime(session.timestamp_end, timeformat)}" if session.timestamp_end else ""}", |  | ||||||
|                     ( |                     ( | ||||||
|                         format_duration(session.duration_calculated, durationformat) |                         format_duration(session.duration_calculated, durationformat) | ||||||
|                         if session.duration_calculated |                         if session.duration_calculated | ||||||
| @ -195,13 +189,13 @@ def search_sessions(request: HttpRequest) -> HttpResponse: | |||||||
|  |  | ||||||
|  |  | ||||||
| @login_required | @login_required | ||||||
| def add_session(request: HttpRequest, purchase_id: int = 0) -> HttpResponse: | def add_session(request: HttpRequest, game_id: int = 0) -> HttpResponse: | ||||||
|     context = {} |     context = {} | ||||||
|     initial: dict[str, Any] = {"timestamp_start": timezone.now()} |     initial: dict[str, Any] = {"timestamp_start": timezone.now()} | ||||||
|  |  | ||||||
|     last = Session.objects.last() |     last = Session.objects.last() | ||||||
|     if last != None: |     if last != None: | ||||||
|         initial["purchase"] = last.purchase |         initial["game"] = last.game | ||||||
|  |  | ||||||
|     if request.method == "POST": |     if request.method == "POST": | ||||||
|         form = SessionForm(request.POST or None, initial=initial) |         form = SessionForm(request.POST or None, initial=initial) | ||||||
| @ -209,12 +203,12 @@ def add_session(request: HttpRequest, purchase_id: int = 0) -> HttpResponse: | |||||||
|             form.save() |             form.save() | ||||||
|             return redirect("list_sessions") |             return redirect("list_sessions") | ||||||
|     else: |     else: | ||||||
|         if purchase_id: |         if game_id: | ||||||
|             purchase = Purchase.objects.get(id=purchase_id) |             game = Game.objects.get(id=game_id) | ||||||
|             form = SessionForm( |             form = SessionForm( | ||||||
|                 initial={ |                 initial={ | ||||||
|                     **initial, |                     **initial, | ||||||
|                     "purchase": purchase, |                     "game": game, | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
|         else: |         else: | ||||||
|  | |||||||
							
								
								
									
										76
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										76
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							| @ -1,4 +1,4 @@ | |||||||
| # This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. | # This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand. | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "asgiref" | name = "asgiref" | ||||||
| @ -6,6 +6,7 @@ version = "3.8.1" | |||||||
| description = "ASGI specs, helper code, and adapters" | description = "ASGI specs, helper code, and adapters" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
|  | groups = ["main", "dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, |     {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, | ||||||
|     {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, |     {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, | ||||||
| @ -20,6 +21,7 @@ version = "2024.8.30" | |||||||
| description = "Python package for providing Mozilla's CA Bundle." | description = "Python package for providing Mozilla's CA Bundle." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.6" | python-versions = ">=3.6" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, |     {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, | ||||||
|     {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, |     {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, | ||||||
| @ -31,6 +33,7 @@ version = "3.4.0" | |||||||
| description = "Validate configuration and produce human readable error messages." | description = "Validate configuration and produce human readable error messages." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, |     {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, | ||||||
|     {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, |     {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, | ||||||
| @ -42,6 +45,7 @@ version = "3.4.0" | |||||||
| description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.7.0" | python-versions = ">=3.7.0" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, |     {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, | ||||||
|     {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, |     {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, | ||||||
| @ -156,6 +160,7 @@ version = "8.1.7" | |||||||
| description = "Composable command line interface toolkit" | description = "Composable command line interface toolkit" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.7" | python-versions = ">=3.7" | ||||||
|  | groups = ["main", "dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, |     {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, | ||||||
|     {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, |     {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, | ||||||
| @ -170,10 +175,12 @@ version = "0.4.6" | |||||||
| description = "Cross-platform colored terminal text." | description = "Cross-platform colored terminal text." | ||||||
| optional = false | optional = false | ||||||
| python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" | ||||||
|  | groups = ["main", "dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, |     {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, | ||||||
|     {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, |     {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, | ||||||
| ] | ] | ||||||
|  | markers = {main = "platform_system == \"Windows\""} | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "croniter" | name = "croniter" | ||||||
| @ -181,6 +188,7 @@ version = "5.0.1" | |||||||
| description = "croniter provides iteration for datetime object with cron like format" | description = "croniter provides iteration for datetime object with cron like format" | ||||||
| optional = false | optional = false | ||||||
| python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.6" | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.6" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "croniter-5.0.1-py2.py3-none-any.whl", hash = "sha256:eb28439742291f6c10b181df1a5ecf421208b1fc62ef44501daec1780a0b09e9"}, |     {file = "croniter-5.0.1-py2.py3-none-any.whl", hash = "sha256:eb28439742291f6c10b181df1a5ecf421208b1fc62ef44501daec1780a0b09e9"}, | ||||||
|     {file = "croniter-5.0.1.tar.gz", hash = "sha256:7d9b1ef25b10eece48fdf29d8ac52f9b6252abff983ac614ade4f3276294019e"}, |     {file = "croniter-5.0.1.tar.gz", hash = "sha256:7d9b1ef25b10eece48fdf29d8ac52f9b6252abff983ac614ade4f3276294019e"}, | ||||||
| @ -196,6 +204,7 @@ version = "1.15.1" | |||||||
| description = "CSS unobfuscator and beautifier." | description = "CSS unobfuscator and beautifier." | ||||||
| optional = false | optional = false | ||||||
| python-versions = "*" | python-versions = "*" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "cssbeautifier-1.15.1.tar.gz", hash = "sha256:9f7064362aedd559c55eeecf6b6bed65e05f33488dcbe39044f0403c26e1c006"}, |     {file = "cssbeautifier-1.15.1.tar.gz", hash = "sha256:9f7064362aedd559c55eeecf6b6bed65e05f33488dcbe39044f0403c26e1c006"}, | ||||||
| ] | ] | ||||||
| @ -211,6 +220,7 @@ version = "0.3.9" | |||||||
| description = "Distribution utilities" | description = "Distribution utilities" | ||||||
| optional = false | optional = false | ||||||
| python-versions = "*" | python-versions = "*" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, |     {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, | ||||||
|     {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, |     {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, | ||||||
| @ -218,13 +228,14 @@ files = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "django" | name = "django" | ||||||
| version = "5.1.3" | version = "5.1.5" | ||||||
| description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." | description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.10" | python-versions = ">=3.10" | ||||||
|  | groups = ["main", "dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "Django-5.1.3-py3-none-any.whl", hash = "sha256:8b38a9a12da3ae00cb0ba72da985ec4b14de6345046b1e174b1fd7254398f818"}, |     {file = "Django-5.1.5-py3-none-any.whl", hash = "sha256:c46eb936111fffe6ec4bc9930035524a8be98ec2f74d8a0ff351226a3e52f459"}, | ||||||
|     {file = "Django-5.1.3.tar.gz", hash = "sha256:c0fa0e619c39325a169208caef234f90baa925227032ad3f44842ba14d75234a"}, |     {file = "Django-5.1.5.tar.gz", hash = "sha256:19bbca786df50b9eca23cee79d495facf55c8f5c54c529d9bf1fe7b5ea086af3"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -242,6 +253,7 @@ version = "1.3.0" | |||||||
| description = "Bringing component based design to Django templates." | description = "Bringing component based design to Django templates." | ||||||
| optional = false | optional = false | ||||||
| python-versions = "<4,>=3.8" | python-versions = "<4,>=3.8" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "django_cotton-1.3.0-py3-none-any.whl", hash = "sha256:a23f29b759c43423e2f901352c0810388839cc412f6985614153c6ccfcfc2595"}, |     {file = "django_cotton-1.3.0-py3-none-any.whl", hash = "sha256:a23f29b759c43423e2f901352c0810388839cc412f6985614153c6ccfcfc2595"}, | ||||||
|     {file = "django_cotton-1.3.0.tar.gz", hash = "sha256:8f4a15dd55c8ee9182cf7234c228ea45d9fcdec1de125221bce8d05af035730a"}, |     {file = "django_cotton-1.3.0.tar.gz", hash = "sha256:8f4a15dd55c8ee9182cf7234c228ea45d9fcdec1de125221bce8d05af035730a"}, | ||||||
| @ -256,6 +268,7 @@ version = "4.4.6" | |||||||
| description = "A configurable set of panels that display various debug information about the current request/response." | description = "A configurable set of panels that display various debug information about the current request/response." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "django_debug_toolbar-4.4.6-py3-none-any.whl", hash = "sha256:3beb671c9ec44ffb817fad2780667f172bd1c067dbcabad6268ce39a81335f45"}, |     {file = "django_debug_toolbar-4.4.6-py3-none-any.whl", hash = "sha256:3beb671c9ec44ffb817fad2780667f172bd1c067dbcabad6268ce39a81335f45"}, | ||||||
|     {file = "django_debug_toolbar-4.4.6.tar.gz", hash = "sha256:36e421cb908c2f0675e07f9f41e3d1d8618dc386392ec82d23bcfcd5d29c7044"}, |     {file = "django_debug_toolbar-4.4.6.tar.gz", hash = "sha256:36e421cb908c2f0675e07f9f41e3d1d8618dc386392ec82d23bcfcd5d29c7044"}, | ||||||
| @ -271,6 +284,7 @@ version = "3.2.3" | |||||||
| description = "Extensions for Django" | description = "Extensions for Django" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.6" | python-versions = ">=3.6" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "django-extensions-3.2.3.tar.gz", hash = "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a"}, |     {file = "django-extensions-3.2.3.tar.gz", hash = "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a"}, | ||||||
|     {file = "django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401"}, |     {file = "django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401"}, | ||||||
| @ -285,6 +299,7 @@ version = "1.21.0" | |||||||
| description = "Extensions for using Django with htmx." | description = "Extensions for using Django with htmx." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.9" | python-versions = ">=3.9" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "django_htmx-1.21.0-py3-none-any.whl", hash = "sha256:64bc31463017a80552b767bc216ee5700248fa72e7ccd2963495e69afbdb6abe"}, |     {file = "django_htmx-1.21.0-py3-none-any.whl", hash = "sha256:64bc31463017a80552b767bc216ee5700248fa72e7ccd2963495e69afbdb6abe"}, | ||||||
|     {file = "django_htmx-1.21.0.tar.gz", hash = "sha256:6ed3b42effd5980f22e68f36cd14ee4311bff3b6cb8435a89e27f45995691572"}, |     {file = "django_htmx-1.21.0.tar.gz", hash = "sha256:6ed3b42effd5980f22e68f36cd14ee4311bff3b6cb8435a89e27f45995691572"}, | ||||||
| @ -300,6 +315,7 @@ version = "3.2" | |||||||
| description = "Pickled object field for Django" | description = "Pickled object field for Django" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3" | python-versions = ">=3" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "django-picklefield-3.2.tar.gz", hash = "sha256:aa463f5d79d497dbe789f14b45180f00a51d0d670067d0729f352a3941cdfa4d"}, |     {file = "django-picklefield-3.2.tar.gz", hash = "sha256:aa463f5d79d497dbe789f14b45180f00a51d0d670067d0729f352a3941cdfa4d"}, | ||||||
|     {file = "django_picklefield-3.2-py3-none-any.whl", hash = "sha256:e9a73539d110f69825d9320db18bcb82e5189ff48dbed41821c026a20497764c"}, |     {file = "django_picklefield-3.2-py3-none-any.whl", hash = "sha256:e9a73539d110f69825d9320db18bcb82e5189ff48dbed41821c026a20497764c"}, | ||||||
| @ -317,6 +333,7 @@ version = "1.7.4" | |||||||
| description = "A multiprocessing distributed task queue for Django" | description = "A multiprocessing distributed task queue for Django" | ||||||
| optional = false | optional = false | ||||||
| python-versions = "<4,>=3.8" | python-versions = "<4,>=3.8" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "django_q2-1.7.4-py3-none-any.whl", hash = "sha256:6eda6d56505822ee5ebc6c4eac1dde726f5dbf20ee9ea7080575535852e2671f"}, |     {file = "django_q2-1.7.4-py3-none-any.whl", hash = "sha256:6eda6d56505822ee5ebc6c4eac1dde726f5dbf20ee9ea7080575535852e2671f"}, | ||||||
|     {file = "django_q2-1.7.4.tar.gz", hash = "sha256:56a3781cc480474fa9c04bbde62445b0a9b4195adc409bd963b8f593b0598c43"}, |     {file = "django_q2-1.7.4.tar.gz", hash = "sha256:56a3781cc480474fa9c04bbde62445b0a9b4195adc409bd963b8f593b0598c43"}, | ||||||
| @ -337,6 +354,7 @@ version = "24.4" | |||||||
| description = "django-template-partials" | description = "django-template-partials" | ||||||
| optional = false | optional = false | ||||||
| python-versions = "*" | python-versions = "*" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "django_template_partials-24.4-py2.py3-none-any.whl", hash = "sha256:ee59d3839385d7f648907c3fa8d5923fcd66cd8090f141fe2a1c338b917984e2"}, |     {file = "django_template_partials-24.4-py2.py3-none-any.whl", hash = "sha256:ee59d3839385d7f648907c3fa8d5923fcd66cd8090f141fe2a1c338b917984e2"}, | ||||||
|     {file = "django_template_partials-24.4.tar.gz", hash = "sha256:25b67301470fc274ecc419e5e5fd4686a5020b1c038fd241a70eb087809034b6"}, |     {file = "django_template_partials-24.4.tar.gz", hash = "sha256:25b67301470fc274ecc419e5e5fd4686a5020b1c038fd241a70eb087809034b6"}, | ||||||
| @ -355,6 +373,7 @@ version = "3.0.7" | |||||||
| description = "Django/Jinja template indenter" | description = "Django/Jinja template indenter" | ||||||
| optional = false | optional = false | ||||||
| python-versions = "*" | python-versions = "*" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "djhtml-3.0.7.tar.gz", hash = "sha256:558c905b092a0c8afcbed27dea2f50aa6eb853a658b309e4e0f2bb378bdf6178"}, |     {file = "djhtml-3.0.7.tar.gz", hash = "sha256:558c905b092a0c8afcbed27dea2f50aa6eb853a658b309e4e0f2bb378bdf6178"}, | ||||||
| ] | ] | ||||||
| @ -368,6 +387,7 @@ version = "1.36.1" | |||||||
| description = "HTML Template Linter and Formatter" | description = "HTML Template Linter and Formatter" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.9" | python-versions = ">=3.9" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "djlint-1.36.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef40527fd6cd82cdd18f65a6bf5b486b767d2386f6c21f2ebd60e5d88f487fe8"}, |     {file = "djlint-1.36.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef40527fd6cd82cdd18f65a6bf5b486b767d2386f6c21f2ebd60e5d88f487fe8"}, | ||||||
|     {file = "djlint-1.36.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4712de3dea172000a098da6a0cd709d158909b4964ba0f68bee584cef18b4878"}, |     {file = "djlint-1.36.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4712de3dea172000a098da6a0cd709d158909b4964ba0f68bee584cef18b4878"}, | ||||||
| @ -410,6 +430,7 @@ version = "0.12.4" | |||||||
| description = "EditorConfig File Locator and Interpreter for Python" | description = "EditorConfig File Locator and Interpreter for Python" | ||||||
| optional = false | optional = false | ||||||
| python-versions = "*" | python-versions = "*" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80"}, |     {file = "EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80"}, | ||||||
| ] | ] | ||||||
| @ -420,6 +441,7 @@ version = "3.16.1" | |||||||
| description = "A platform independent file lock." | description = "A platform independent file lock." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, |     {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, | ||||||
|     {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, |     {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, | ||||||
| @ -436,6 +458,7 @@ version = "3.4.3" | |||||||
| description = "GraphQL Framework for Python" | description = "GraphQL Framework for Python" | ||||||
| optional = false | optional = false | ||||||
| python-versions = "*" | python-versions = "*" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "graphene-3.4.3-py2.py3-none-any.whl", hash = "sha256:820db6289754c181007a150db1f7fff544b94142b556d12e3ebc777a7bf36c71"}, |     {file = "graphene-3.4.3-py2.py3-none-any.whl", hash = "sha256:820db6289754c181007a150db1f7fff544b94142b556d12e3ebc777a7bf36c71"}, | ||||||
|     {file = "graphene-3.4.3.tar.gz", hash = "sha256:2a3786948ce75fe7e078443d37f609cbe5bb36ad8d6b828740ad3b95ed1a0aaa"}, |     {file = "graphene-3.4.3.tar.gz", hash = "sha256:2a3786948ce75fe7e078443d37f609cbe5bb36ad8d6b828740ad3b95ed1a0aaa"}, | ||||||
| @ -457,6 +480,7 @@ version = "3.2.2" | |||||||
| description = "Graphene Django integration" | description = "Graphene Django integration" | ||||||
| optional = false | optional = false | ||||||
| python-versions = "*" | python-versions = "*" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "graphene-django-3.2.2.tar.gz", hash = "sha256:059ccf25d9a5159f28d7ebf1a648c993ab34deb064e80b70ca096aa22a609556"}, |     {file = "graphene-django-3.2.2.tar.gz", hash = "sha256:059ccf25d9a5159f28d7ebf1a648c993ab34deb064e80b70ca096aa22a609556"}, | ||||||
|     {file = "graphene_django-3.2.2-py2.py3-none-any.whl", hash = "sha256:0fd95c8c1cbe77ae2a5940045ce276803c3acbf200a156731e0c730f2776ae2c"}, |     {file = "graphene_django-3.2.2-py2.py3-none-any.whl", hash = "sha256:0fd95c8c1cbe77ae2a5940045ce276803c3acbf200a156731e0c730f2776ae2c"}, | ||||||
| @ -481,6 +505,7 @@ version = "3.2.5" | |||||||
| description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." | description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." | ||||||
| optional = false | optional = false | ||||||
| python-versions = "<4,>=3.6" | python-versions = "<4,>=3.6" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "graphql_core-3.2.5-py3-none-any.whl", hash = "sha256:2f150d5096448aa4f8ab26268567bbfeef823769893b39c1a2e1409590939c8a"}, |     {file = "graphql_core-3.2.5-py3-none-any.whl", hash = "sha256:2f150d5096448aa4f8ab26268567bbfeef823769893b39c1a2e1409590939c8a"}, | ||||||
|     {file = "graphql_core-3.2.5.tar.gz", hash = "sha256:e671b90ed653c808715645e3998b7ab67d382d55467b7e2978549111bbabf8d5"}, |     {file = "graphql_core-3.2.5.tar.gz", hash = "sha256:e671b90ed653c808715645e3998b7ab67d382d55467b7e2978549111bbabf8d5"}, | ||||||
| @ -492,6 +517,7 @@ version = "3.2.0" | |||||||
| description = "Relay library for graphql-core" | description = "Relay library for graphql-core" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.6,<4" | python-versions = ">=3.6,<4" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "graphql-relay-3.2.0.tar.gz", hash = "sha256:1ff1c51298356e481a0be009ccdff249832ce53f30559c1338f22a0e0d17250c"}, |     {file = "graphql-relay-3.2.0.tar.gz", hash = "sha256:1ff1c51298356e481a0be009ccdff249832ce53f30559c1338f22a0e0d17250c"}, | ||||||
|     {file = "graphql_relay-3.2.0-py3-none-any.whl", hash = "sha256:c9b22bd28b170ba1fe674c74384a8ff30a76c8e26f88ac3aa1584dd3179953e5"}, |     {file = "graphql_relay-3.2.0-py3-none-any.whl", hash = "sha256:c9b22bd28b170ba1fe674c74384a8ff30a76c8e26f88ac3aa1584dd3179953e5"}, | ||||||
| @ -506,6 +532,7 @@ version = "22.0.0" | |||||||
| description = "WSGI HTTP Server for UNIX" | description = "WSGI HTTP Server for UNIX" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.7" | python-versions = ">=3.7" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"}, |     {file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"}, | ||||||
|     {file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"}, |     {file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"}, | ||||||
| @ -527,6 +554,7 @@ version = "0.14.0" | |||||||
| description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.7" | python-versions = ">=3.7" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, |     {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, | ||||||
|     {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, |     {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, | ||||||
| @ -538,6 +566,7 @@ version = "2.6.2" | |||||||
| description = "File identification library for Python" | description = "File identification library for Python" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.9" | python-versions = ">=3.9" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "identify-2.6.2-py2.py3-none-any.whl", hash = "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3"}, |     {file = "identify-2.6.2-py2.py3-none-any.whl", hash = "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3"}, | ||||||
|     {file = "identify-2.6.2.tar.gz", hash = "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd"}, |     {file = "identify-2.6.2.tar.gz", hash = "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd"}, | ||||||
| @ -552,6 +581,7 @@ version = "3.10" | |||||||
| description = "Internationalized Domain Names in Applications (IDNA)" | description = "Internationalized Domain Names in Applications (IDNA)" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.6" | python-versions = ">=3.6" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, |     {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, | ||||||
|     {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, |     {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, | ||||||
| @ -566,6 +596,7 @@ version = "2.0.0" | |||||||
| description = "brain-dead simple config-ini parsing" | description = "brain-dead simple config-ini parsing" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.7" | python-versions = ">=3.7" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, |     {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, | ||||||
|     {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, |     {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, | ||||||
| @ -577,6 +608,7 @@ version = "5.13.2" | |||||||
| description = "A Python utility / library to sort Python imports." | description = "A Python utility / library to sort Python imports." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8.0" | python-versions = ">=3.8.0" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, |     {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, | ||||||
|     {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, |     {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, | ||||||
| @ -591,6 +623,7 @@ version = "1.15.1" | |||||||
| description = "JavaScript unobfuscator and beautifier." | description = "JavaScript unobfuscator and beautifier." | ||||||
| optional = false | optional = false | ||||||
| python-versions = "*" | python-versions = "*" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24"}, |     {file = "jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24"}, | ||||||
| ] | ] | ||||||
| @ -605,6 +638,7 @@ version = "0.9.25" | |||||||
| description = "A Python implementation of the JSON5 data format." | description = "A Python implementation of the JSON5 data format." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "json5-0.9.25-py3-none-any.whl", hash = "sha256:34ed7d834b1341a86987ed52f3f76cd8ee184394906b6e22a1e0deb9ab294e8f"}, |     {file = "json5-0.9.25-py3-none-any.whl", hash = "sha256:34ed7d834b1341a86987ed52f3f76cd8ee184394906b6e22a1e0deb9ab294e8f"}, | ||||||
|     {file = "json5-0.9.25.tar.gz", hash = "sha256:548e41b9be043f9426776f05df8635a00fe06104ea51ed24b67f908856e151ae"}, |     {file = "json5-0.9.25.tar.gz", hash = "sha256:548e41b9be043f9426776f05df8635a00fe06104ea51ed24b67f908856e151ae"}, | ||||||
| @ -616,6 +650,7 @@ version = "3.7" | |||||||
| description = "Python implementation of John Gruber's Markdown." | description = "Python implementation of John Gruber's Markdown." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, |     {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, | ||||||
|     {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, |     {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, | ||||||
| @ -631,6 +666,7 @@ version = "1.13.0" | |||||||
| description = "Optional static typing for Python" | description = "Optional static typing for Python" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, |     {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, | ||||||
|     {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, |     {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, | ||||||
| @ -683,6 +719,7 @@ version = "1.0.0" | |||||||
| description = "Type system extensions for programs checked with the mypy type checker." | description = "Type system extensions for programs checked with the mypy type checker." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.5" | python-versions = ">=3.5" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, |     {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, | ||||||
|     {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, |     {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, | ||||||
| @ -694,6 +731,7 @@ version = "1.9.1" | |||||||
| description = "Node.js virtual environment builder" | description = "Node.js virtual environment builder" | ||||||
| optional = false | optional = false | ||||||
| python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, |     {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, | ||||||
|     {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, |     {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, | ||||||
| @ -705,6 +743,7 @@ version = "24.2" | |||||||
| description = "Core utilities for Python packages" | description = "Core utilities for Python packages" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
|  | groups = ["main", "dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, |     {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, | ||||||
|     {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, |     {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, | ||||||
| @ -716,6 +755,7 @@ version = "0.12.1" | |||||||
| description = "Utility library for gitignore style pattern matching of file paths." | description = "Utility library for gitignore style pattern matching of file paths." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, |     {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, | ||||||
|     {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, |     {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, | ||||||
| @ -727,6 +767,7 @@ version = "4.3.6" | |||||||
| description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, |     {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, | ||||||
|     {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, |     {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, | ||||||
| @ -743,6 +784,7 @@ version = "1.5.0" | |||||||
| description = "plugin and hook calling mechanisms for python" | description = "plugin and hook calling mechanisms for python" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, |     {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, | ||||||
|     {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, |     {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, | ||||||
| @ -758,6 +800,7 @@ version = "3.8.0" | |||||||
| description = "A framework for managing and maintaining multi-language pre-commit hooks." | description = "A framework for managing and maintaining multi-language pre-commit hooks." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.9" | python-versions = ">=3.9" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, |     {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, | ||||||
|     {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, |     {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, | ||||||
| @ -776,6 +819,7 @@ version = "2.3" | |||||||
| description = "Promises/A+ implementation for Python" | description = "Promises/A+ implementation for Python" | ||||||
| optional = false | optional = false | ||||||
| python-versions = "*" | python-versions = "*" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"}, |     {file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"}, | ||||||
| ] | ] | ||||||
| @ -792,6 +836,7 @@ version = "8.3.3" | |||||||
| description = "pytest: simple powerful testing with Python" | description = "pytest: simple powerful testing with Python" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, |     {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, | ||||||
|     {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, |     {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, | ||||||
| @ -812,6 +857,7 @@ version = "2.9.0.post0" | |||||||
| description = "Extensions to the standard Python datetime module" | description = "Extensions to the standard Python datetime module" | ||||||
| optional = false | optional = false | ||||||
| python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, |     {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, | ||||||
|     {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, |     {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, | ||||||
| @ -826,6 +872,7 @@ version = "2024.2" | |||||||
| description = "World timezone definitions, modern and historical" | description = "World timezone definitions, modern and historical" | ||||||
| optional = false | optional = false | ||||||
| python-versions = "*" | python-versions = "*" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, |     {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, | ||||||
|     {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, |     {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, | ||||||
| @ -837,6 +884,7 @@ version = "6.0.2" | |||||||
| description = "YAML parser and emitter for Python" | description = "YAML parser and emitter for Python" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
|  | groups = ["main", "dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, |     {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, | ||||||
|     {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, |     {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, | ||||||
| @ -899,6 +947,7 @@ version = "2024.11.6" | |||||||
| description = "Alternative regular expression module, to replace re." | description = "Alternative regular expression module, to replace re." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, |     {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, | ||||||
|     {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, |     {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, | ||||||
| @ -1002,6 +1051,7 @@ version = "2.32.3" | |||||||
| description = "Python HTTP for Humans." | description = "Python HTTP for Humans." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, |     {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, | ||||||
|     {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, |     {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, | ||||||
| @ -1023,6 +1073,7 @@ version = "1.16.0" | |||||||
| description = "Python 2 and 3 compatibility utilities" | description = "Python 2 and 3 compatibility utilities" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" | ||||||
|  | groups = ["main", "dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, |     {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, | ||||||
|     {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, |     {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, | ||||||
| @ -1034,6 +1085,7 @@ version = "0.5.1" | |||||||
| description = "A non-validating SQL parser." | description = "A non-validating SQL parser." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
|  | groups = ["main", "dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, |     {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, | ||||||
|     {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, |     {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, | ||||||
| @ -1049,6 +1101,7 @@ version = "1.3" | |||||||
| description = "The most basic Text::Unidecode port" | description = "The most basic Text::Unidecode port" | ||||||
| optional = false | optional = false | ||||||
| python-versions = "*" | python-versions = "*" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, |     {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, | ||||||
|     {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, |     {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, | ||||||
| @ -1060,6 +1113,7 @@ version = "4.67.0" | |||||||
| description = "Fast, Extensible Progress Meter" | description = "Fast, Extensible Progress Meter" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.7" | python-versions = ">=3.7" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "tqdm-4.67.0-py3-none-any.whl", hash = "sha256:0cd8af9d56911acab92182e88d763100d4788bdf421d251616040cc4d44863be"}, |     {file = "tqdm-4.67.0-py3-none-any.whl", hash = "sha256:0cd8af9d56911acab92182e88d763100d4788bdf421d251616040cc4d44863be"}, | ||||||
|     {file = "tqdm-4.67.0.tar.gz", hash = "sha256:fe5a6f95e6fe0b9755e9469b77b9c3cf850048224ecaa8293d7d2d31f97d869a"}, |     {file = "tqdm-4.67.0.tar.gz", hash = "sha256:fe5a6f95e6fe0b9755e9469b77b9c3cf850048224ecaa8293d7d2d31f97d869a"}, | ||||||
| @ -1081,6 +1135,7 @@ version = "4.12.2" | |||||||
| description = "Backported and Experimental Type Hints for Python 3.8+" | description = "Backported and Experimental Type Hints for Python 3.8+" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
|  | groups = ["main", "dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, |     {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, | ||||||
|     {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, |     {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, | ||||||
| @ -1092,6 +1147,8 @@ version = "2024.2" | |||||||
| description = "Provider of IANA time zone data" | description = "Provider of IANA time zone data" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=2" | python-versions = ">=2" | ||||||
|  | groups = ["main", "dev"] | ||||||
|  | markers = "sys_platform == \"win32\"" | ||||||
| files = [ | files = [ | ||||||
|     {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, |     {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, | ||||||
|     {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, |     {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, | ||||||
| @ -1103,6 +1160,7 @@ version = "2.2.3" | |||||||
| description = "HTTP library with thread-safe connection pooling, file post, and more." | description = "HTTP library with thread-safe connection pooling, file post, and more." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, |     {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, | ||||||
|     {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, |     {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, | ||||||
| @ -1120,6 +1178,7 @@ version = "0.30.6" | |||||||
| description = "The lightning-fast ASGI server." | description = "The lightning-fast ASGI server." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
|  | groups = ["main"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"}, |     {file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"}, | ||||||
|     {file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"}, |     {file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"}, | ||||||
| @ -1134,13 +1193,14 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "virtualenv" | name = "virtualenv" | ||||||
| version = "20.27.1" | version = "20.29.1" | ||||||
| description = "Virtual Python Environment builder" | description = "Virtual Python Environment builder" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
|  | groups = ["dev"] | ||||||
| files = [ | files = [ | ||||||
|     {file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"}, |     {file = "virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779"}, | ||||||
|     {file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"}, |     {file = "virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -1153,6 +1213,6 @@ docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "s | |||||||
| test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] | ||||||
|  |  | ||||||
| [metadata] | [metadata] | ||||||
| lock-version = "2.0" | lock-version = "2.1" | ||||||
| python-versions = "^3.11" | python-versions = "^3.11" | ||||||
| content-hash = "b5bb46a6591964aec145637cd9a412a681f2cc5e7e4fdd6fd9ecb0fe8724b8e3" | content-hash = "b5bb46a6591964aec145637cd9a412a681f2cc5e7e4fdd6fd9ecb0fe8724b8e3" | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings") | |||||||
| django.setup() | django.setup() | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
|  |  | ||||||
| from games.models import Edition, Game, Platform, Purchase, Session | from games.models import Game, Platform, Purchase, Session | ||||||
|  |  | ||||||
| ZONEINFO = ZoneInfo(settings.TIME_ZONE) | ZONEINFO = ZoneInfo(settings.TIME_ZONE) | ||||||
|  |  | ||||||
| @ -21,10 +21,8 @@ class PathWorksTest(TestCase): | |||||||
|         pl.save() |         pl.save() | ||||||
|         g = Game(name="The Test Game") |         g = Game(name="The Test Game") | ||||||
|         g.save() |         g.save() | ||||||
|         e = Edition(game=g, name="The Test Game Edition", platform=pl) |  | ||||||
|         e.save() |  | ||||||
|         p = Purchase( |         p = Purchase( | ||||||
|             edition=e, |             games=[e], | ||||||
|             platform=pl, |             platform=pl, | ||||||
|             date_purchased=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO), |             date_purchased=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO), | ||||||
|         ) |         ) | ||||||
| @ -53,11 +51,6 @@ class PathWorksTest(TestCase): | |||||||
|         response = self.client.get(url) |         response = self.client.get(url) | ||||||
|         self.assertEqual(response.status_code, 200) |         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): |     def test_add_purchase_returns_200(self): | ||||||
|         url = reverse("add_purchase") |         url = reverse("add_purchase") | ||||||
|         response = self.client.get(url) |         response = self.client.get(url) | ||||||
|  | |||||||
| @ -3,14 +3,13 @@ from datetime import datetime | |||||||
| from zoneinfo import ZoneInfo | from zoneinfo import ZoneInfo | ||||||
|  |  | ||||||
| import django | import django | ||||||
| from django.db import models |  | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
|  |  | ||||||
| os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings") | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings") | ||||||
| django.setup() | django.setup() | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
|  |  | ||||||
| from games.models import Edition, Game, Purchase, Session | from games.models import Game, Purchase, Session | ||||||
|  |  | ||||||
| ZONEINFO = ZoneInfo(settings.TIME_ZONE) | ZONEINFO = ZoneInfo(settings.TIME_ZONE) | ||||||
|  |  | ||||||
| @ -22,10 +21,8 @@ class FormatDurationTest(TestCase): | |||||||
|     def test_duration_format(self): |     def test_duration_format(self): | ||||||
|         g = Game(name="The Test Game") |         g = Game(name="The Test Game") | ||||||
|         g.save() |         g.save() | ||||||
|         e = Edition(game=g, name="The Test Game Edition") |  | ||||||
|         e.save() |  | ||||||
|         p = Purchase( |         p = Purchase( | ||||||
|             edition=e, date_purchased=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO) |             game=g, date_purchased=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO) | ||||||
|         ) |         ) | ||||||
|         p.save() |         p.save() | ||||||
|         s = Session( |         s = Session( | ||||||
|  | |||||||
| @ -47,7 +47,7 @@ INSTALLED_APPS = [ | |||||||
|  |  | ||||||
| Q_CLUSTER = { | Q_CLUSTER = { | ||||||
|     "name": "DjangoQ", |     "name": "DjangoQ", | ||||||
|     "workers": 4, |     "workers": 1, | ||||||
|     "recycle": 500, |     "recycle": 500, | ||||||
|     "timeout": 60, |     "timeout": 60, | ||||||
|     "retry": 120, |     "retry": 120, | ||||||
| @ -113,6 +113,10 @@ DATABASES = { | |||||||
|     "default": { |     "default": { | ||||||
|         "ENGINE": "django.db.backends.sqlite3", |         "ENGINE": "django.db.backends.sqlite3", | ||||||
|         "NAME": BASE_DIR / "db.sqlite3", |         "NAME": BASE_DIR / "db.sqlite3", | ||||||
|  |         "OPTIONS": { | ||||||
|  |             "timeout": 20, | ||||||
|  |             "init_command": "PRAGMA synchronous=NORMAL; PRAGMA journal_mode=WAL;", | ||||||
|  |         }, | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user