add support for class letters
- SHA
4faa21c1e980925b0a8c72bdb411528ff39f6206- Parents
-
b063067 - Tree
d1b15ad
4faa21c
4faa21c1e980925b0a8c72bdb411528ff39f6206b063067
d1b15ad| Status | File | + | - |
|---|---|---|---|
| M |
memorial/admin.py
|
1 | 1 |
| A |
memorial/migrations/0006_person_class_letter.py
|
23 | 0 |
| M |
memorial/models.py
|
20 | 4 |
| M |
memorial/serializers.py
|
9 | 9 |
memorial/admin.pymodified@@ -89,7 +89,7 @@ class PersonAdmin(admin.ModelAdmin): | ||
| 89 | 89 | 'fields': ('first_name', 'middle_name', 'last_name', 'suffix') |
| 90 | 90 | }), |
| 91 | 91 | ('VMI Information', { |
| 92 | - 'fields': ('class_year',) | |
| 92 | + 'fields': ('class_year', 'class_letter') | |
| 93 | 93 | }), |
| 94 | 94 | ('Military Information', { |
| 95 | 95 | 'fields': ('conflict', 'rank', 'unit') |
memorial/migrations/0006_person_class_letter.pyadded@@ -0,0 +1,23 @@ | ||
| 1 | +# Generated by Django 5.0.6 on 2025-10-16 03:29 | |
| 2 | + | |
| 3 | +from django.db import migrations, models | |
| 4 | + | |
| 5 | + | |
| 6 | +class Migration(migrations.Migration): | |
| 7 | + | |
| 8 | + dependencies = [ | |
| 9 | + ("memorial", "0005_contribution"), | |
| 10 | + ] | |
| 11 | + | |
| 12 | + operations = [ | |
| 13 | + migrations.AddField( | |
| 14 | + model_name="person", | |
| 15 | + name="class_letter", | |
| 16 | + field=models.CharField( | |
| 17 | + blank=True, | |
| 18 | + default="", | |
| 19 | + help_text="Optional letter suffix for class year (e.g., 'M' for 1956M)", | |
| 20 | + max_length=1, | |
| 21 | + ), | |
| 22 | + ), | |
| 23 | + ] | |
memorial/models.pymodified@@ -34,10 +34,16 @@ class Person(models.Model): | ||
| 34 | 34 | |
| 35 | 35 | # VMI info |
| 36 | 36 | class_year = models.IntegerField( |
| 37 | - null=True, | |
| 37 | + null=True, | |
| 38 | 38 | blank=True, |
| 39 | 39 | help_text="VMI graduation year (e.g., 1965)" |
| 40 | 40 | ) |
| 41 | + class_letter = models.CharField( | |
| 42 | + max_length=1, | |
| 43 | + blank=True, | |
| 44 | + default='', | |
| 45 | + help_text="Optional letter suffix for class year (e.g., 'M' for 1956M)" | |
| 46 | + ) | |
| 41 | 47 | |
| 42 | 48 | # Military info |
| 43 | 49 | conflict = models.ForeignKey( |
@@ -78,7 +84,17 @@ class Person(models.Model): | ||
| 78 | 84 | # Metadata |
| 79 | 85 | created_at = models.DateTimeField(auto_now_add=True) |
| 80 | 86 | updated_at = models.DateTimeField(auto_now=True) |
| 81 | - | |
| 87 | + | |
| 88 | + def clean(self): | |
| 89 | + """Validate and normalize class_letter""" | |
| 90 | + from django.core.exceptions import ValidationError | |
| 91 | + if self.class_letter: | |
| 92 | + self.class_letter = self.class_letter.upper().strip() | |
| 93 | + if len(self.class_letter) > 1 or not self.class_letter.isalpha(): | |
| 94 | + raise ValidationError({ | |
| 95 | + 'class_letter': 'Must be a single letter (A-Z)' | |
| 96 | + }) | |
| 97 | + | |
| 82 | 98 | class Meta: |
| 83 | 99 | ordering = ['last_name', 'first_name'] |
| 84 | 100 | verbose_name_plural = "People" |
@@ -90,7 +106,7 @@ class Person(models.Model): | ||
| 90 | 106 | if self.suffix: |
| 91 | 107 | full_name = f"{full_name} {self.suffix}" |
| 92 | 108 | if self.class_year: |
| 93 | - full_name = f"{full_name} '{str(self.class_year)[2:]}" # e.g., John Doe '65 | |
| 109 | + full_name = f"{full_name} '{str(self.class_year)[2:]}{self.class_letter}" # e.g., John Doe '65M | |
| 94 | 110 | return full_name |
| 95 | 111 | |
| 96 | 112 | @property |
@@ -112,7 +128,7 @@ class Person(models.Model): | ||
| 112 | 128 | """Display name with class year""" |
| 113 | 129 | name = self.display_name |
| 114 | 130 | if self.class_year: |
| 115 | - name = f"{name} '{str(self.class_year)[2:]}" | |
| 131 | + name = f"{name} '{str(self.class_year)[2:]}{self.class_letter}" | |
| 116 | 132 | return name |
| 117 | 133 | |
| 118 | 134 | @property |
memorial/serializers.pymodified@@ -7,10 +7,10 @@ class PersonListSerializer(serializers.ModelSerializer): | ||
| 7 | 7 | display_name = serializers.ReadOnlyField() |
| 8 | 8 | full_display_name = serializers.ReadOnlyField() |
| 9 | 9 | death_date_display = serializers.ReadOnlyField() |
| 10 | - | |
| 10 | + | |
| 11 | 11 | class Meta: |
| 12 | 12 | model = Person |
| 13 | - fields = ['id', 'display_name', 'full_display_name', 'rank', 'unit', 'class_year', 'death_description', 'death_date_display'] | |
| 13 | + fields = ['id', 'display_name', 'full_display_name', 'rank', 'unit', 'class_year', 'class_letter', 'death_description', 'death_date_display'] | |
| 14 | 14 | |
| 15 | 15 | |
| 16 | 16 | class PersonDetailSerializer(serializers.ModelSerializer): |
@@ -20,14 +20,14 @@ class PersonDetailSerializer(serializers.ModelSerializer): | ||
| 20 | 20 | conflict_name = serializers.CharField(source='conflict.name', read_only=True) |
| 21 | 21 | pdf_url = serializers.SerializerMethodField() |
| 22 | 22 | death_date_display = serializers.ReadOnlyField() |
| 23 | - | |
| 23 | + | |
| 24 | 24 | class Meta: |
| 25 | 25 | model = Person |
| 26 | 26 | fields = [ |
| 27 | 27 | 'id', 'first_name', 'middle_name', 'last_name', 'suffix', |
| 28 | - 'display_name', 'full_display_name', 'class_year', 'rank', 'unit', | |
| 29 | - 'date_of_death', 'death_date_precision', 'death_date_display', | |
| 30 | - 'death_description', 'conflict', 'conflict_name', | |
| 28 | + 'display_name', 'full_display_name', 'class_year', 'class_letter', 'rank', 'unit', | |
| 29 | + 'date_of_death', 'death_date_precision', 'death_date_display', | |
| 30 | + 'death_description', 'conflict', 'conflict_name', | |
| 31 | 31 | 'pdf_key', 'pdf_url' |
| 32 | 32 | ] |
| 33 | 33 | |
@@ -46,12 +46,12 @@ class PersonSearchSerializer(serializers.ModelSerializer): | ||
| 46 | 46 | conflict_name = serializers.CharField(source='conflict.name', read_only=True) |
| 47 | 47 | conflict_id = serializers.IntegerField(source='conflict.id', read_only=True) |
| 48 | 48 | death_date_display = serializers.ReadOnlyField() |
| 49 | - | |
| 49 | + | |
| 50 | 50 | class Meta: |
| 51 | 51 | model = Person |
| 52 | 52 | fields = [ |
| 53 | - 'id', 'display_name', 'full_display_name', 'class_year', | |
| 54 | - 'rank', 'unit', 'date_of_death', 'death_date_display', | |
| 53 | + 'id', 'display_name', 'full_display_name', 'class_year', 'class_letter', | |
| 54 | + 'rank', 'unit', 'date_of_death', 'death_date_display', | |
| 55 | 55 | 'conflict_name', 'conflict_id' |
| 56 | 56 | ] |
| 57 | 57 | |