diff --git a/frontend/src/index.css b/frontend/src/index.css index 49b6857..e8a7e7d 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -72,6 +72,10 @@ textarea:disabled { @apply dark:bg-slate-700 dark:text-slate-400; } +.errorlist { + @apply mt-4 mb-1 pl-3 py-2 bg-red-600 text-slate-200 w-[300px]; +} + @media screen and (min-width: 768px) { form input, select, diff --git a/games/forms.py b/games/forms.py index b92e8f5..19af792 100644 --- a/games/forms.py +++ b/games/forms.py @@ -58,7 +58,8 @@ class PurchaseForm(forms.ModelForm): related_purchase = forms.ModelChoiceField( queryset=Purchase.objects.filter(type=Purchase.GAME).order_by( "edition__sort_name" - ) + ), + required=False, ) class Meta: @@ -82,6 +83,27 @@ class PurchaseForm(forms.ModelForm): "name", ] + def clean(self): + cleaned_data = super().clean() + purchase_type = cleaned_data.get("type") + related_purchase = cleaned_data.get("related_purchase") + name = cleaned_data.get("name") + + # Set the type on the instance to use get_type_display() + # This is safe because we're not saving the instance. + self.instance.type = purchase_type + + if purchase_type != Purchase.GAME: + type_display = self.instance.get_type_display() + if not related_purchase: + self.add_error( + "related_purchase", + f"{type_display} must have a related purchase.", + ) + if not name: + self.add_error("name", f"{type_display} must have a name.") + return cleaned_data + class IncludeNameSelect(forms.Select): def create_option(self, name, value, *args, **kwargs): diff --git a/games/migrations/0030_alter_purchase_name.py b/games/migrations/0030_alter_purchase_name.py new file mode 100644 index 0000000..25f96e5 --- /dev/null +++ b/games/migrations/0030_alter_purchase_name.py @@ -0,0 +1,18 @@ +# 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), + ), + ] diff --git a/games/models.py b/games/models.py index c04323f..7ac16d8 100644 --- a/games/models.py +++ b/games/models.py @@ -2,6 +2,7 @@ from common.time import format_duration from datetime import datetime, timedelta from django.conf import settings from django.db import models +from django.core.exceptions import ValidationError from django.db.models import F, Manager, Sum from zoneinfo import ZoneInfo @@ -118,9 +119,7 @@ class Purchase(models.Model): max_length=2, choices=OWNERSHIP_TYPES, default=DIGITAL ) type = models.CharField(max_length=255, choices=TYPES, default=GAME) - name = models.CharField( - max_length=255, default="Unknown Name", null=True, blank=True - ) + name = models.CharField(max_length=255, default="", null=True, blank=True) related_purchase = models.ForeignKey( "Purchase", on_delete=models.SET_NULL, @@ -147,6 +146,10 @@ class Purchase(models.Model): def save(self, *args, **kwargs): if self.type == Purchase.GAME: self.name = "" + elif self.type != Purchase.GAME and not self.related_purchase: + raise ValidationError( + f"{self.get_type_display()} must have a related purchase." + ) super().save(*args, **kwargs) diff --git a/games/static/base.css b/games/static/base.css index 0dfa762..07970f4 100644 --- a/games/static/base.css +++ b/games/static/base.css @@ -1231,6 +1231,19 @@ textarea:disabled) { color: rgb(148 163 184 / var(--tw-text-opacity)); } +.errorlist { + margin-top: 1rem; + margin-bottom: 0.25rem; + width: 300px; + --tw-bg-opacity: 1; + background-color: rgb(220 38 38 / var(--tw-bg-opacity)); + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 0.75rem; + --tw-text-opacity: 1; + color: rgb(226 232 240 / var(--tw-text-opacity)); +} + @media screen and (min-width: 768px) { form input, select,