Files
mohamad d4ec85add4 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.
2026-11-03 04:41:47 +08:00

142 lines
7.8 KiB
Python

from django.core.exceptions import ValidationError
from django.template import Context, Template
from django.test import RequestFactory, TestCase
from apps.pages.maps import resolve_city
from apps.pages.models import MapMembership, MapPerson, MapPlacement, PeopleMap
class PeopleMapTests(TestCase):
def setUp(self):
self.world = PeopleMap.objects.create(title='Our global network')
self.person = MapPerson.objects.create(name='<script>Test</script>', institution='Institute')
def test_offline_city_and_country_resolution(self):
lat, lon = resolve_city('CA', 'Vancouver')
self.assertAlmostEqual(lat, 49.24966, places=2)
self.assertAlmostEqual(lon, -123.11934, places=2)
self.assertEqual(resolve_city('Canada', 'Vancouver'), (lat, lon))
def test_invalid_or_ambiguous_city_is_rejected(self):
for country, city in [('Canada', 'Not a real city'), ('Unknown country', 'Paris'), ('US', 'Springfield')]:
with self.subTest(country=country, city=city), self.assertRaises(ValidationError):
resolve_city(country, city)
def test_membership_saves_resolved_coordinates(self):
member = MapMembership.objects.create(people_map=self.world, person=self.person, country='CA', city='Vancouver')
self.assertLess(member.longitude, -120)
member.city = 'Toronto'
member.save()
member.refresh_from_db()
self.assertGreater(member.longitude, -80)
def test_explicit_coordinates_and_invalid_ranges(self):
member = MapMembership(people_map=self.world, person=self.person, location_method='coordinates', latitude=0, longitude=0)
member.full_clean()
for latitude, longitude in [(91, 0), (0, -181), (None, 0), (float('nan'), 0), (0, float('inf'))]:
member.latitude, member.longitude = latitude, longitude
with self.subTest(latitude=latitude, longitude=longitude), self.assertRaises(ValidationError):
member.full_clean()
def test_different_maps_can_have_different_members(self):
second = PeopleMap.objects.create(title='Other map')
MapMembership.objects.create(people_map=self.world, person=self.person, location_method='coordinates', latitude=0, longitude=0)
self.assertEqual(second.memberships.count(), 0)
with self.assertRaises(ValidationError):
MapMembership.objects.create(people_map=self.world, person=self.person, location_method='coordinates', latitude=1, longitude=1)
def test_placement_path_validation(self):
for path in ['https://example.com/', '//example.com/', '/about/?a=1', '/about/#x']:
with self.subTest(path=path), self.assertRaises(ValidationError):
MapPlacement(people_map=self.world, page_path=path).full_clean()
def render_maps(self, path, position='bottom'):
request = RequestFactory().get(path)
return Template('{% load people_maps %}{% people_maps position %}').render(Context({'request': request, 'position': position}))
def test_placement_filters_and_escaped_person_data(self):
MapPlacement.objects.create(people_map=self.world, page_path='/about/')
MapMembership.objects.create(people_map=self.world, person=self.person, location_method='coordinates', latitude=0, longitude=0)
html = self.render_maps('/about/')
self.assertIn('Our global network', html)
self.assertIn('&lt;script&gt;', html)
self.assertNotIn('<script>Test', html)
self.assertNotIn('Our global network', self.render_maps('/contact/'))
self.assertNotIn('Our global network', self.render_maps('/about/', 'top'))
self.world.is_active = False
self.world.save()
self.assertNotIn('Our global network', self.render_maps('/about/'))
def test_placement_supports_any_project_or_page_path(self):
for path in ['/', '/about/', '/my-page/', '/projects/alpha/', '/projects/alpha/beta/']:
MapPlacement.objects.create(people_map=self.world, page_path=path, position='top')
self.assertIn('Our global network', self.render_maps(path, 'top'))
def test_admin_map_inline_resolves_city(self):
from django.contrib.auth import get_user_model
from django.urls import reverse
user = get_user_model().objects.create_superuser(username='mapadmin', password='test-password', email='map@example.com')
self.client.force_login(user)
url = reverse('admin:pages_peoplemap_change', args=[self.world.pk])
self.assertEqual(self.client.get(url).status_code, 200)
response = self.client.post(url, {
'title': self.world.title, 'style': 'classic', 'background_style': 'atlas', 'is_active': 'on',
'memberships-TOTAL_FORMS': '1', 'memberships-INITIAL_FORMS': '0',
'memberships-MIN_NUM_FORMS': '0', 'memberships-MAX_NUM_FORMS': '1000',
'memberships-0-person': str(self.person.pk), 'memberships-0-location_method': 'city',
'memberships-0-country': 'CA', 'memberships-0-city': 'Vancouver', 'memberships-0-order': '0',
'placements-TOTAL_FORMS': '1', 'placements-INITIAL_FORMS': '0',
'placements-MIN_NUM_FORMS': '0', 'placements-MAX_NUM_FORMS': '1000',
'placements-0-page_path': '/about/', 'placements-0-position': 'bottom',
'placements-0-order': '0', 'placements-0-is_active': 'on', '_save': 'Save',
})
self.assertEqual(response.status_code, 302)
member = self.world.memberships.get()
self.assertAlmostEqual(member.latitude, 49.25, places=1)
self.assertIn('Our global network', self.client.get('/about/').content.decode())
def test_all_builtin_backgrounds_are_bundled_and_rendered(self):
from django.contrib.staticfiles import finders
MapPlacement.objects.create(people_map=self.world, page_path='/about/')
for key, label in PeopleMap.BACKGROUND_CHOICES:
self.world.background_style = key
self.world.full_clean()
self.world.save()
self.assertTrue(finders.find(self.world.background_static_path), label)
self.assertIn(self.world.background_static_path, self.render_maps('/about/'))
def test_uploaded_background_overrides_preset(self):
MapPlacement.objects.create(people_map=self.world, page_path='/about/')
self.world.background_image = 'maps/custom-world.png'
self.world.background_style = 'atlas'
self.world.save()
html = self.render_maps('/about/')
self.assertIn('/media/maps/custom-world.png', html)
self.assertNotIn('src="/static/images/maps/world-atlas.svg"', html)
def test_people_details_can_be_hidden_without_hiding_map_or_leaving_dead_links(self):
placement = MapPlacement.objects.create(people_map=self.world, page_path='/about/')
member = MapMembership.objects.create(people_map=self.world, person=self.person,
location_method='coordinates', latitude=0, longitude=0)
target = f'map-{placement.pk}-person-{member.pk}'
html = self.render_maps('/about/')
self.assertIn('people-map-directory', html)
self.assertIn(f'href="#{target}"', html)
self.world.show_people_details = False
self.world.save()
html = self.render_maps('/about/')
self.assertNotIn('people-map-directory', html)
self.assertNotIn(target, html)
self.assertNotIn('Institute', html)
self.assertIn('people-map-pin', html)
self.assertIn('data-lat=', html)
self.assertIn('&lt;script&gt;Test&lt;/script&gt;', html)
def test_canonical_city_name_takes_priority_over_another_towns_alias(self):
lat, lon = resolve_city('IR', 'Tehran')
self.assertAlmostEqual(lat, 35.69, places=1)
self.assertAlmostEqual(lon, 51.42, places=1)
def test_city_alias_remains_supported(self):
self.assertEqual(resolve_city('GB', 'Londres'), resolve_city('GB', 'London'))