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
@@ -0,0 +1,75 @@
# Generated by Django 5.2.16 on 2026-11-02 19:44
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pages', '0028_alter_blogpostpartner_options_and_more'),
]
operations = [
migrations.CreateModel(
name='MapPerson',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('qualifications', models.CharField(blank=True, max_length=100)),
('role', models.CharField(blank=True, max_length=200)),
('institution', models.CharField(blank=True, max_length=300)),
('biography', models.TextField(blank=True)),
('photo', models.ImageField(blank=True, upload_to='maps/people/')),
('profile_url', models.URLField(blank=True)),
],
options={
'verbose_name_plural': 'Map people',
'ordering': ['name', 'pk'],
},
),
migrations.CreateModel(
name='PeopleMap',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('description', models.TextField(blank=True)),
('style', models.CharField(choices=[('aurora', 'Aurora — dark ocean'), ('paper', 'Atlas — light'), ('classic', 'Classic — original blue / turquoise palette')], default='aurora', max_length=20)),
('background_image', models.ImageField(blank=True, help_text='Optional original map artwork. Must use an equirectangular world projection.', upload_to='maps/')),
('is_active', models.BooleanField(default=True)),
],
),
migrations.CreateModel(
name='MapPlacement',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('page_path', models.CharField(help_text='Local page path, e.g. /about/, /projects/project-slug/, or any custom page. No domain or query string.', max_length=400)),
('position', models.CharField(choices=[('top', 'Before page content'), ('bottom', 'After page content')], default='bottom', max_length=10)),
('order', models.PositiveIntegerField(default=0)),
('is_active', models.BooleanField(default=True)),
('people_map', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='placements', to='pages.peoplemap')),
],
options={
'ordering': ['order', 'pk'],
'constraints': [models.UniqueConstraint(fields=('people_map', 'page_path', 'position'), name='unique_map_placement')],
},
),
migrations.CreateModel(
name='MapMembership',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('location_method', models.CharField(choices=[('city', 'Country and city'), ('coordinates', 'Coordinates')], default='city', max_length=12)),
('country', models.CharField(blank=True, max_length=100)),
('city', models.CharField(blank=True, max_length=100)),
('latitude', models.FloatField(blank=True, null=True)),
('longitude', models.FloatField(blank=True, null=True)),
('order', models.PositiveIntegerField(default=0)),
('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='map_memberships', to='pages.mapperson')),
('people_map', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='pages.peoplemap')),
],
options={
'ordering': ['order', 'pk'],
'constraints': [models.UniqueConstraint(fields=('people_map', 'person'), name='unique_map_person')],
},
),
]