diff --git a/CHANGELOG.md b/CHANGELOG.md index 79c4159..4c2e17e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ ## Unreleased +### New +* More fields are now optional. This is to make it easier to add new items in bulk. + * Game: Wikidata ID + * Edition: Platform, Year + * Purchase: Platform + * Platform: Group + * Session: Device +* New fields: + * Game: Year Released + * To record original year of release + * Upon migration, this will be set to a year of any of the game's edition that has it set + * Purchase: Date Finished +* Editions are now unique combination of name and platform + ### Improved * game overview: simplify playtime range display diff --git a/games/forms.py b/games/forms.py index e83606a..38be10e 100644 --- a/games/forms.py +++ b/games/forms.py @@ -50,6 +50,7 @@ class PurchaseForm(forms.ModelForm): widgets = { "date_purchased": custom_date_widget, "date_refunded": custom_date_widget, + "date_finished": custom_date_widget, } model = Purchase fields = [ @@ -57,6 +58,7 @@ class PurchaseForm(forms.ModelForm): "platform", "date_purchased", "date_refunded", + "date_finished", "price", "price_currency", "ownership_type", @@ -67,7 +69,9 @@ class EditionForm(forms.ModelForm): game = forms.ModelChoiceField( queryset=Game.objects.order_by("name"), widget=autofocus_select_widget ) - platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name")) + platform = forms.ModelChoiceField( + queryset=Platform.objects.order_by("name"), required=False + ) class Meta: model = Edition @@ -77,7 +81,7 @@ class EditionForm(forms.ModelForm): class GameForm(forms.ModelForm): class Meta: model = Game - fields = ["name", "wikidata"] + fields = ["name", "year_released", "wikidata"] widgets = {"name": autofocus_input_widget} diff --git a/games/migrations/0016_alter_edition_platform_alter_edition_year_released_and_more.py b/games/migrations/0016_alter_edition_platform_alter_edition_year_released_and_more.py new file mode 100644 index 0000000..0f53c85 --- /dev/null +++ b/games/migrations/0016_alter_edition_platform_alter_edition_year_released_and_more.py @@ -0,0 +1,51 @@ +# Generated by Django 4.1.5 on 2023-11-06 11:10 + +from django.db import migrations, models +import django.db.models.deletion + + +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", + ), + ), + ] diff --git a/games/migrations/0016_alter_edition_platform_alter_edition_year_released_and_more_squashed_0022_rename_year_game_year_released.py b/games/migrations/0016_alter_edition_platform_alter_edition_year_released_and_more_squashed_0022_rename_year_game_year_released.py new file mode 100644 index 0000000..ed8b9b1 --- /dev/null +++ b/games/migrations/0016_alter_edition_platform_alter_edition_year_released_and_more_squashed_0022_rename_year_game_year_released.py @@ -0,0 +1,141 @@ +# Generated by Django 4.1.5 on 2023-11-06 18:14 + +from django.db import migrations, models +import django.db.models.deletion + + +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", + ), + ] diff --git a/games/migrations/0017_alter_device_type_alter_purchase_platform.py b/games/migrations/0017_alter_device_type_alter_purchase_platform.py new file mode 100644 index 0000000..63e4754 --- /dev/null +++ b/games/migrations/0017_alter_device_type_alter_purchase_platform.py @@ -0,0 +1,41 @@ +# Generated by Django 4.1.5 on 2023-11-06 16:53 + +from django.db import migrations, models +import django.db.models.deletion + + +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", + ), + ), + ] diff --git a/games/migrations/0018_auto_20231106_1825.py b/games/migrations/0018_auto_20231106_1825.py new file mode 100644 index 0000000..b7eb3b1 --- /dev/null +++ b/games/migrations/0018_auto_20231106_1825.py @@ -0,0 +1,34 @@ +# 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), + ] diff --git a/games/migrations/0019_alter_edition_unique_together.py b/games/migrations/0019_alter_edition_unique_together.py new file mode 100644 index 0000000..c946406 --- /dev/null +++ b/games/migrations/0019_alter_edition_unique_together.py @@ -0,0 +1,17 @@ +# 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")}, + ), + ] diff --git a/games/migrations/0020_game_year.py b/games/migrations/0020_game_year.py new file mode 100644 index 0000000..7ea19f3 --- /dev/null +++ b/games/migrations/0020_game_year.py @@ -0,0 +1,18 @@ +# 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), + ), + ] diff --git a/games/migrations/0021_auto_20231106_1909.py b/games/migrations/0021_auto_20231106_1909.py new file mode 100644 index 0000000..96898a6 --- /dev/null +++ b/games/migrations/0021_auto_20231106_1909.py @@ -0,0 +1,24 @@ +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), + ] diff --git a/games/migrations/0022_rename_year_game_year_released.py b/games/migrations/0022_rename_year_game_year_released.py new file mode 100644 index 0000000..3cc93b5 --- /dev/null +++ b/games/migrations/0022_rename_year_game_year_released.py @@ -0,0 +1,18 @@ +# 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", + ), + ] diff --git a/games/migrations/0023_purchase_date_finished.py b/games/migrations/0023_purchase_date_finished.py new file mode 100644 index 0000000..63d8cfa --- /dev/null +++ b/games/migrations/0023_purchase_date_finished.py @@ -0,0 +1,21 @@ +# 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), + ), + ] diff --git a/games/models.py b/games/models.py index 22865e0..082631d 100644 --- a/games/models.py +++ b/games/models.py @@ -10,17 +10,23 @@ from django.db.models import F, Manager, Sum class Game(models.Model): name = models.CharField(max_length=255) - wikidata = models.CharField(max_length=50) + year_released = models.IntegerField(null=True, blank=True, default=None) + wikidata = models.CharField(max_length=50, null=True, blank=True, default=None) def __str__(self): return self.name class Edition(models.Model): + class Meta: + unique_together = [["name", "platform"]] + game = models.ForeignKey("Game", on_delete=models.CASCADE) name = models.CharField(max_length=255) - platform = models.ForeignKey("Platform", on_delete=models.CASCADE) - year_released = models.IntegerField(default=datetime.today().year) + platform = models.ForeignKey( + "Platform", on_delete=models.CASCADE, 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) def __str__(self): @@ -48,9 +54,12 @@ class Purchase(models.Model): ] edition = models.ForeignKey("Edition", on_delete=models.CASCADE) - platform = models.ForeignKey("Platform", on_delete=models.CASCADE) + platform = models.ForeignKey( + "Platform", on_delete=models.CASCADE, default=None, null=True, blank=True + ) date_purchased = models.DateField() date_refunded = models.DateField(blank=True, null=True) + date_finished = models.DateField(blank=True, null=True) price = models.IntegerField(default=0) price_currency = models.CharField(max_length=3, default="USD") ownership_type = models.CharField( @@ -66,7 +75,7 @@ class Purchase(models.Model): class Platform(models.Model): name = models.CharField(max_length=255) - group = models.CharField(max_length=255) + group = models.CharField(max_length=255, null=True, blank=True, default=None) def __str__(self): return self.name @@ -89,7 +98,13 @@ class Session(models.Model): timestamp_end = models.DateTimeField(blank=True, null=True) duration_manual = models.DurationField(blank=True, null=True, default=timedelta(0)) duration_calculated = models.DurationField(blank=True, null=True) - device = models.ForeignKey("Device", on_delete=models.CASCADE, null=True) + device = models.ForeignKey( + "Device", + on_delete=models.CASCADE, + null=True, + blank=True, + default=None, + ) note = models.TextField(blank=True, null=True) objects = SessionQuerySet.as_manager() @@ -126,6 +141,12 @@ class Session(models.Model): self.duration_calculated = self.timestamp_end - self.timestamp_start else: self.duration_calculated = timedelta(0) + + if not self.device: + default_device, _ = Device.objects.get_or_create( + type=Device.UNKNOWN, defaults={"name": "Unknown"} + ) + self.device = default_device super(Session, self).save(*args, **kwargs) @@ -135,15 +156,17 @@ class Device(models.Model): HANDHELD = "ha" MOBILE = "mo" SBC = "sbc" + UNKNOWN = "un" DEVICE_TYPES = [ (PC, "PC"), (CONSOLE, "Console"), (HANDHELD, "Handheld"), (MOBILE, "Mobile"), (SBC, "Single-board computer"), + (UNKNOWN, "Unknown"), ] name = models.CharField(max_length=255) - type = models.CharField(max_length=3, choices=DEVICE_TYPES, default=PC) + type = models.CharField(max_length=3, choices=DEVICE_TYPES, default=UNKNOWN) def __str__(self): return f"{self.name} ({self.get_type_display()})"