vmi-virtual-memorial/vmi-wd-backend / acc213c

Browse files

search, index, models

Authored by espadonne
SHA
acc213cb13722fdf51683fd6b8965d1dcaf54ae2
Parents
01e7229
Tree
f321baa

6 changed files

StatusFile+-
M memorial/admin.py 5 2
A memorial/migrations/0002_person_class_year.py 20 0
M memorial/models.py 17 0
M memorial/serializers.py 20 3
M memorial/urls.py 3 1
M memorial/views.py 98 4
memorial/admin.pymodified
@@ -12,8 +12,8 @@ class ConflictAdmin(admin.ModelAdmin):
1212
 
1313
 @admin.register(Person)
1414
 class PersonAdmin(admin.ModelAdmin):
15
-    list_display = ['display_name', 'conflict', 'rank', 'date_of_death']
16
-    list_filter = ['conflict', 'rank']
15
+    list_display = ['display_name', 'class_year', 'conflict', 'rank', 'date_of_death']
16
+    list_filter = ['conflict', 'class_year', 'rank']
1717
     search_fields = ['first_name', 'last_name', 'unit']
1818
     autocomplete_fields = ['conflict']
1919
     
@@ -21,6 +21,9 @@ class PersonAdmin(admin.ModelAdmin):
2121
         ('Name', {
2222
             'fields': ('first_name', 'middle_name', 'last_name', 'suffix')
2323
         }),
24
+        ('VMI Information', {
25
+            'fields': ('class_year',)
26
+        }),
2427
         ('Military Information', {
2528
             'fields': ('conflict', 'rank', 'unit', 'date_of_death')
2629
         }),
memorial/migrations/0002_person_class_year.pyadded
@@ -0,0 +1,20 @@
1
+# Generated by Django 5.0.6 on 2025-07-13 17:11
2
+
3
+from django.db import migrations, models
4
+
5
+
6
+class Migration(migrations.Migration):
7
+
8
+    dependencies = [
9
+        ("memorial", "0001_initial"),
10
+    ]
11
+
12
+    operations = [
13
+        migrations.AddField(
14
+            model_name="person",
15
+            name="class_year",
16
+            field=models.IntegerField(
17
+                blank=True, help_text="VMI graduation year (e.g., 1965)", null=True
18
+            ),
19
+        ),
20
+    ]
memorial/models.pymodified
@@ -31,6 +31,13 @@ class Person(models.Model):
3131
     middle_name = models.CharField(max_length=100, blank=True)
3232
     suffix = models.CharField(max_length=20, blank=True)  # Jr., III, etc.
3333
     
34
+    # VMI info
35
+    class_year = models.IntegerField(
36
+        null=True, 
37
+        blank=True,
38
+        help_text="VMI graduation year (e.g., 1965)"
39
+    )
40
+    
3441
     # Military info
3542
     conflict = models.ForeignKey(
3643
         Conflict, 
@@ -62,6 +69,8 @@ class Person(models.Model):
6269
             full_name = f"{self.first_name} {self.middle_name} {self.last_name}"
6370
         if self.suffix:
6471
             full_name = f"{full_name} {self.suffix}"
72
+        if self.class_year:
73
+            full_name = f"{full_name} '{str(self.class_year)[2:]}"  # e.g., John Doe '65
6574
         return full_name
6675
     
6776
     @property
@@ -78,5 +87,13 @@ class Person(models.Model):
7887
             name_parts.append(self.suffix)
7988
         return ' '.join(name_parts)
8089
     
90
+    @property
91
+    def full_display_name(self):
92
+        """Display name with class year"""
93
+        name = self.display_name
94
+        if self.class_year:
95
+            name = f"{name} '{str(self.class_year)[2:]}"
96
+        return name
97
+    
8198
     def get_absolute_url(self):
8299
         return reverse('person-detail', kwargs={'pk': self.pk})
memorial/serializers.pymodified
@@ -5,15 +5,17 @@ from .models import Conflict, Person
55
 class PersonListSerializer(serializers.ModelSerializer):
66
     """Lightweight serializer for listing people"""
77
     display_name = serializers.ReadOnlyField()
8
+    full_display_name = serializers.ReadOnlyField()
89
     
910
     class Meta:
1011
         model = Person
11
-        fields = ['id', 'display_name', 'rank', 'unit']
12
+        fields = ['id', 'display_name', 'full_display_name', 'rank', 'unit', 'class_year']
1213
 
1314
 
1415
 class PersonDetailSerializer(serializers.ModelSerializer):
1516
     """Full details for individual person"""
1617
     display_name = serializers.ReadOnlyField()
18
+    full_display_name = serializers.ReadOnlyField()
1719
     conflict_name = serializers.CharField(source='conflict.name', read_only=True)
1820
     pdf_url = serializers.SerializerMethodField()
1921
     
@@ -21,8 +23,8 @@ class PersonDetailSerializer(serializers.ModelSerializer):
2123
         model = Person
2224
         fields = [
2325
             'id', 'first_name', 'middle_name', 'last_name', 'suffix',
24
-            'display_name', 'rank', 'unit', 'date_of_death',
25
-            'conflict', 'conflict_name', 'pdf_key', 'pdf_url'
26
+            'display_name', 'full_display_name', 'class_year', 'rank', 'unit', 
27
+            'date_of_death', 'conflict', 'conflict_name', 'pdf_key', 'pdf_url'
2628
         ]
2729
     
2830
     def get_pdf_url(self, obj):
@@ -33,6 +35,21 @@ class PersonDetailSerializer(serializers.ModelSerializer):
3335
         return None
3436
 
3537
 
38
+class PersonSearchSerializer(serializers.ModelSerializer):
39
+    """Serializer for search results"""
40
+    display_name = serializers.ReadOnlyField()
41
+    full_display_name = serializers.ReadOnlyField()
42
+    conflict_name = serializers.CharField(source='conflict.name', read_only=True)
43
+    conflict_id = serializers.IntegerField(source='conflict.id', read_only=True)
44
+    
45
+    class Meta:
46
+        model = Person
47
+        fields = [
48
+            'id', 'display_name', 'full_display_name', 'class_year',
49
+            'rank', 'unit', 'date_of_death', 'conflict_name', 'conflict_id'
50
+        ]
51
+
52
+
3653
 class ConflictSerializer(serializers.ModelSerializer):
3754
     """Conflict with casualty count"""
3855
     casualty_count = serializers.ReadOnlyField()
memorial/urls.pymodified
@@ -1,6 +1,6 @@
11
 from django.urls import path, include
22
 from rest_framework.routers import DefaultRouter
3
-from .views import ConflictViewSet, PersonViewSet
3
+from .views import ConflictViewSet, PersonViewSet, memorial_index, search_filters
44
 
55
 router = DefaultRouter()
66
 router.register('conflicts', ConflictViewSet)
@@ -8,4 +8,6 @@ router.register('persons', PersonViewSet)
88
 
99
 urlpatterns = [
1010
     path('', include(router.urls)),
11
+    path('index/', memorial_index, name='memorial-index'),
12
+    path('search-filters/', search_filters, name='search-filters'),
1113
 ]
memorial/views.pymodified
@@ -1,11 +1,14 @@
11
 from rest_framework import viewsets, status
2
-from rest_framework.decorators import action
2
+from rest_framework.decorators import action, api_view
33
 from rest_framework.response import Response
44
 from django.http import HttpResponse
5
+from django.db.models import Q
6
+from datetime import datetime
57
 from .models import Conflict, Person
68
 from .serializers import (
79
     ConflictSerializer, ConflictDetailSerializer,
8
-    PersonListSerializer, PersonDetailSerializer
10
+    PersonListSerializer, PersonDetailSerializer,
11
+    PersonSearchSerializer
912
 )
1013
 
1114
 
@@ -26,6 +29,8 @@ class PersonViewSet(viewsets.ReadOnlyModelViewSet):
2629
     def get_serializer_class(self):
2730
         if self.action == 'list':
2831
             return PersonListSerializer
32
+        elif self.action == 'search':
33
+            return PersonSearchSerializer
2934
         return PersonDetailSerializer
3035
     
3136
     def get_queryset(self):
@@ -33,7 +38,69 @@ class PersonViewSet(viewsets.ReadOnlyModelViewSet):
3338
         conflict_id = self.request.query_params.get('conflict', None)
3439
         if conflict_id is not None:
3540
             queryset = queryset.filter(conflict_id=conflict_id)
36
-        return queryset
41
+        # Always order by last name, then first name for consistency
42
+        return queryset.order_by('last_name', 'first_name')
43
+    
44
+    @action(detail=False, methods=['get'])
45
+    def search(self, request):
46
+        """Search and filter people"""
47
+        queryset = Person.objects.all()
48
+        
49
+        # Name search (across all name fields)
50
+        search_term = request.query_params.get('q', '')
51
+        if search_term:
52
+            queryset = queryset.filter(
53
+                Q(first_name__icontains=search_term) |
54
+                Q(middle_name__icontains=search_term) |
55
+                Q(last_name__icontains=search_term) |
56
+                Q(suffix__icontains=search_term)
57
+            )
58
+        
59
+        # Class year filter (can be comma-separated)
60
+        class_years = request.query_params.get('class_year', '')
61
+        if class_years:
62
+            years = [int(y.strip()) for y in class_years.split(',') if y.strip().isdigit()]
63
+            if years:
64
+                queryset = queryset.filter(class_year__in=years)
65
+        
66
+        # Conflict filter (can be comma-separated)
67
+        conflict_ids = request.query_params.get('conflict', '')
68
+        if conflict_ids:
69
+            ids = [int(id.strip()) for id in conflict_ids.split(',') if id.strip().isdigit()]
70
+            if ids:
71
+                queryset = queryset.filter(conflict_id__in=ids)
72
+        
73
+        # Date range filter
74
+        date_from = request.query_params.get('date_from', '')
75
+        date_to = request.query_params.get('date_to', '')
76
+        no_date = request.query_params.get('no_date', '').lower() == 'true'
77
+        
78
+        if no_date:
79
+            queryset = queryset.filter(date_of_death__isnull=True)
80
+        else:
81
+            if date_from:
82
+                try:
83
+                    date_from_parsed = datetime.strptime(date_from, '%Y-%m-%d').date()
84
+                    queryset = queryset.filter(date_of_death__gte=date_from_parsed)
85
+                except ValueError:
86
+                    pass
87
+            
88
+            if date_to:
89
+                try:
90
+                    date_to_parsed = datetime.strptime(date_to, '%Y-%m-%d').date()
91
+                    queryset = queryset.filter(date_of_death__lte=date_to_parsed)
92
+                except ValueError:
93
+                    pass
94
+        
95
+        # Order results
96
+        queryset = queryset.order_by('last_name', 'first_name')
97
+        
98
+        # Paginate if needed (for now, return all for infinite scroll)
99
+        serializer = PersonSearchSerializer(queryset, many=True)
100
+        return Response({
101
+            'count': queryset.count(),
102
+            'results': serializer.data
103
+        })
37104
     
38105
     @action(detail=True, methods=['get'])
39106
     def pdf(self, request, pk=None):
@@ -53,4 +120,31 @@ class PersonViewSet(viewsets.ReadOnlyModelViewSet):
53120
             f"S3 Key: {person.pdf_key}\n"
54121
             f"This will redirect to S3 presigned URL in production",
55122
             content_type="text/plain"
56
-        )
123
+        )
124
+
125
+
126
+@api_view(['GET'])
127
+def memorial_index(request):
128
+    """Get all conflicts with their casualties for the memorial index"""
129
+    conflicts = Conflict.objects.all()
130
+    data = []
131
+    
132
+    for conflict in conflicts:
133
+        casualties = Person.objects.filter(conflict=conflict).order_by('last_name', 'first_name')
134
+        conflict_data = ConflictSerializer(conflict).data
135
+        conflict_data['casualties'] = PersonListSerializer(casualties, many=True).data
136
+        data.append(conflict_data)
137
+    
138
+    return Response(data)
139
+
140
+
141
+@api_view(['GET'])
142
+def search_filters(request):
143
+    """Get available filter options for the search page"""
144
+    conflicts = Conflict.objects.all().values('id', 'name', 'start_year', 'end_year')
145
+    class_years = Person.objects.exclude(class_year__isnull=True).values_list('class_year', flat=True).distinct().order_by('class_year')
146
+    
147
+    return Response({
148
+        'conflicts': list(conflicts),
149
+        'class_years': list(class_years)
150
+    })