Python · 9352 bytes Raw Blame History
1 # memorial/admin.py
2 from django.contrib import admin
3 from django import forms
4 from django.core.files.storage import default_storage
5 from django.conf import settings
6 from django.utils.html import format_html
7 from django.urls import reverse
8 from django.utils.safestring import mark_safe
9 from .models import Conflict, Person, Contribution, Award, PersonAward
10 import os
11
12
13 class PersonAdminForm(forms.ModelForm):
14 pdf_file = forms.FileField(
15 required=False,
16 help_text="Upload a PDF file for this person's memorial"
17 )
18
19 class Meta:
20 model = Person
21 fields = '__all__'
22 widgets = {
23 'death_description': forms.Textarea(attrs={'rows': 4, 'cols': 80}),
24 'date_of_death': forms.DateInput(attrs={'type': 'date'}),
25 }
26
27 def __init__(self, *args, **kwargs):
28 super().__init__(*args, **kwargs)
29 # Add help text for date fields
30 self.fields['date_of_death'].help_text = (
31 "Enter the full date. Use January 1st for year-only dates, "
32 "and the 1st of the month for month-year dates."
33 )
34 self.fields['death_date_precision'].help_text = (
35 "Select the precision of the date entered above. "
36 "This controls how the date is displayed on the site."
37 )
38
39 def save(self, commit=True):
40 instance = super().save(commit=False)
41
42 # Handle PDF upload
43 pdf_file = self.cleaned_data.get('pdf_file')
44 if pdf_file:
45 # Save instance first to get an ID if it's new
46 if not instance.id:
47 instance.save()
48
49 # Add environment prefix to separate dev/prod files
50 env_prefix = 'dev/' if settings.DEBUG else 'prod/'
51
52 # Create a meaningful filename with actual ID
53 filename = f"{env_prefix}memorials/{instance.last_name}_{instance.first_name}_{instance.id}.pdf"
54 filename = filename.replace(' ', '_').lower()
55
56 # Delete old file if pdf_key already exists and is different
57 if instance.pdf_key and instance.pdf_key != filename:
58 try:
59 default_storage.delete(instance.pdf_key)
60 except Exception:
61 pass # Ignore errors when deleting old files
62
63 # Save to S3 (or local storage in development)
64 path = default_storage.save(filename, pdf_file)
65 instance.pdf_key = path
66
67 if commit:
68 instance.save()
69 return instance
70
71
72 @admin.register(Conflict)
73 class ConflictAdmin(admin.ModelAdmin):
74 list_display = ['name', 'start_year', 'end_year', 'casualty_count', 'order']
75 list_editable = ['order']
76 search_fields = ['name']
77
78
79 class PersonAwardInline(admin.TabularInline):
80 """Inline for editing awards on Person admin page"""
81 model = PersonAward
82 extra = 1
83 autocomplete_fields = ['award']
84 fields = ['award', 'count', 'date_awarded', 'citation']
85
86
87 @admin.register(Person)
88 class PersonAdmin(admin.ModelAdmin):
89 form = PersonAdminForm
90 list_display = ['display_name', 'class_year', 'conflict', 'rank', 'get_death_date_display', 'has_pdf', 'has_description', 'contribution_count', 'award_count']
91 list_filter = ['conflict', 'class_year', 'rank', 'death_date_precision']
92 search_fields = ['first_name', 'last_name', 'unit']
93 autocomplete_fields = ['conflict']
94 inlines = [PersonAwardInline]
95
96 fieldsets = (
97 ('Name', {
98 'fields': ('first_name', 'middle_name', 'last_name', 'suffix')
99 }),
100 ('VMI Information', {
101 'fields': ('class_year', 'class_letter')
102 }),
103 ('Military Information', {
104 'fields': ('conflict', 'rank', 'unit')
105 }),
106 ('Death Information', {
107 'fields': ('date_of_death', 'death_date_precision'),
108 'description': 'For partial dates: Use January 1st for year-only, and the 1st of the month for month-year dates.'
109 }),
110 ('Death Details', {
111 'fields': ('death_description',),
112 'classes': ('wide',), # Makes the text field wider
113 }),
114 ('Memorial Content', {
115 'fields': ('pdf_file', 'pdf_key'),
116 'description': 'Upload a PDF or view the current S3 key'
117 }),
118 )
119
120 readonly_fields = ['pdf_key']
121
122 def get_death_date_display(self, obj):
123 """Display death date in list view"""
124 return obj.death_date_display or 'Unknown'
125 get_death_date_display.short_description = 'Date of Death'
126 get_death_date_display.admin_order_field = 'date_of_death'
127
128 def has_pdf(self, obj):
129 return bool(obj.pdf_key)
130 has_pdf.boolean = True
131 has_pdf.short_description = 'Has PDF'
132
133 def has_description(self, obj):
134 return bool(obj.death_description)
135 has_description.boolean = True
136 has_description.short_description = 'Has Description'
137
138 def contribution_count(self, obj):
139 """Show number of contributions"""
140 total = obj.contributions.count()
141 approved = obj.contributions.filter(status='approved').count()
142 pending = obj.contributions.filter(status='pending').count()
143
144 if pending > 0:
145 return format_html(
146 '<span style="color: green;">{}</span> / '
147 '<span style="color: orange;">{}</span> / '
148 '<span>{}</span>',
149 approved, pending, total
150 )
151 return f"{approved} / {total}"
152 contribution_count.short_description = 'Contributions (A/P/T)'
153
154 def award_count(self, obj):
155 """Show number of awards"""
156 return obj.person_awards.count()
157 award_count.short_description = 'Awards'
158
159
160 @admin.register(Award)
161 class AwardAdmin(admin.ModelAdmin):
162 list_display = ['name', 'recipient_count', 'order']
163 list_editable = ['order']
164 search_fields = ['name', 'short_description']
165 ordering = ['order', 'name']
166
167 fieldsets = (
168 ('Award Information', {
169 'fields': ('name', 'short_description', 'long_description')
170 }),
171 ('Display', {
172 'fields': ('image_filename', 'order')
173 }),
174 )
175
176
177 @admin.register(Contribution)
178 class ContributionAdmin(admin.ModelAdmin):
179 list_display = [
180 'id', 'person_link', 'contributor_email', 'content_type',
181 'status_badge', 'submitted_at', 'reviewed_by'
182 ]
183 list_filter = ['status', 'content_type', 'submitted_at', 'reviewed_at']
184 search_fields = [
185 'person__first_name', 'person__last_name',
186 'contributor_email', 'content_text'
187 ]
188 readonly_fields = [
189 'person', 'contributor_email', 'content_type', 'content_text',
190 'image_preview', 'submitted_at', 'reviewed_at', 'reviewed_by'
191 ]
192
193 fieldsets = (
194 ('Contribution Info', {
195 'fields': ('person', 'contributor_email', 'submitted_at')
196 }),
197 ('Content', {
198 'fields': ('content_type', 'content_text', 'image_preview')
199 }),
200 ('Review Status', {
201 'fields': ('status', 'reviewed_at', 'reviewed_by', 'rejection_reason')
202 })
203 )
204
205 actions = ['approve_contributions', 'reject_contributions']
206
207 def person_link(self, obj):
208 """Link to person in admin"""
209 url = reverse('admin:memorial_person_change', args=[obj.person.id])
210 return format_html('<a href="{}">{}</a>', url, obj.person.full_display_name)
211 person_link.short_description = 'Person'
212
213 def status_badge(self, obj):
214 """Color-coded status badge"""
215 colors = {
216 'pending': '#FFA500',
217 'approved': '#28a745',
218 'rejected': '#dc3545'
219 }
220 return format_html(
221 '<span style="background-color: {}; color: white; padding: 3px 10px; '
222 'border-radius: 3px; font-weight: bold;">{}</span>',
223 colors.get(obj.status, '#666'),
224 obj.get_status_display()
225 )
226 status_badge.short_description = 'Status'
227
228 def image_preview(self, obj):
229 """Preview of uploaded image"""
230 if obj.content_image:
231 return mark_safe(
232 f'<img src="{obj.content_image.url}" '
233 f'style="max-width: 300px; max-height: 300px;" />'
234 )
235 return "No image"
236 image_preview.short_description = 'Image Preview'
237
238 def approve_contributions(self, request, queryset):
239 """Bulk approve action"""
240 count = 0
241 for contribution in queryset.filter(status='pending'):
242 contribution.approve(request.user)
243 count += 1
244 self.message_user(request, f"{count} contributions approved.")
245 approve_contributions.short_description = "Approve selected contributions"
246
247 def reject_contributions(self, request, queryset):
248 """Bulk reject action"""
249 count = 0
250 for contribution in queryset.filter(status='pending'):
251 contribution.reject(request.user, "Bulk rejection")
252 count += 1
253 self.message_user(request, f"{count} contributions rejected.")
254 reject_contributions.short_description = "Reject selected contributions"
255
256 def has_add_permission(self, request):
257 """Prevent manual addition through admin"""
258 return False