feat: add configurable people maps

Add independent map profiles, offline city lookup, bundled world artwork
and collision-free portrait placement with optional person details.

Deploy with updated requirements, migrations 0029-0031 and collectstatic.
This commit is contained in:
mohamad
2026-11-03 04:41:30 +08:00
parent 6fd236f518
commit d4ec85add4
22 changed files with 725 additions and 4 deletions
+53
View File
@@ -1,11 +1,14 @@
from django.contrib import admin
from django.http import FileResponse, Http404, HttpResponseRedirect
from django.urls import path, reverse
from django.templatetags.static import static
from django.utils.html import format_html_join
from django.utils.html import format_html
from apps.core.admin_video import video_admin_preview
from apps.core.video import VIDEO_ADMIN_FIELDSET
from .forms import MapMembershipAdminForm
from .models import (
AboutSection,
AboutSectionItem,
@@ -20,6 +23,10 @@ from .models import (
HomepageSection,
HomepageSectionItem,
PageVideo,
PeopleMap,
MapPerson,
MapMembership,
MapPlacement,
)
@@ -319,3 +326,49 @@ class PageVideoAdmin(admin.ModelAdmin):
@admin.display(description="Preview")
def video_preview(self, obj):
return video_admin_preview(obj)
class MapMembershipInline(admin.StackedInline):
model = MapMembership
form = MapMembershipAdminForm
extra = 0
autocomplete_fields = ['person']
class MapPlacementInline(admin.TabularInline):
model = MapPlacement
extra = 0
@admin.register(PeopleMap)
class PeopleMapAdmin(admin.ModelAdmin):
list_display = ['title', 'style', 'background_style', 'is_active']
list_filter = ['style', 'background_style', 'is_active']
search_fields = ['title']
inlines = [MapMembershipInline, MapPlacementInline]
readonly_fields = ['background_gallery']
fields = ['title', 'description', 'style', 'background_style', 'background_gallery', 'background_image', 'show_people_details', 'is_active']
@admin.display(description="Included world maps")
def background_gallery(self, obj):
return format_html_join('',
'<figure style="display:inline-block;width:240px;margin:8px;vertical-align:top">'
'<img src="{}" alt="{}" style="width:240px;height:120px;object-fit:contain;background:#0c182a;border-radius:8px" />'
'<figcaption style="margin-top:8px">{}</figcaption></figure>',
((static(PeopleMap(background_style=key).background_static_path), label, label)
for key, label in PeopleMap.BACKGROUND_CHOICES))
@admin.register(MapPerson)
class MapPersonAdmin(admin.ModelAdmin):
list_display = ['name', 'role', 'institution']
search_fields = ['name', 'institution', 'role']
@admin.register(MapPlacement)
class MapPlacementAdmin(admin.ModelAdmin):
list_display = ['people_map', 'page_path', 'position', 'order', 'is_active']
search_fields = ['page_path', 'people_map__title']
autocomplete_fields = ['people_map']
list_filter = ['position', 'is_active']