PICK SKY overlay: D3 natal wheel, Character model, PySwiss aspects+tz
PySwiss: - calculate_aspects() in calc.py (conjunction/sextile/square/trine/opposition with orbs) - /api/tz/ endpoint (timezonefinder lat/lon → IANA timezone) - aspects included in /api/chart/ response - timezonefinder==8.2.2 added to requirements - 14 new unit tests (test_calc.py) + 12 new integration tests (TimezoneApiTest, aspect fields) Main app: - Sign, Planet, AspectType, HouseLabel reference models + seeded migrations (0032–0033) - Character model with birth_dt/lat/lon/place, house_system, chart_data, celtic_cross, confirmed_at/retired_at lifecycle (migration 0034) - natus_preview proxy view: calls PySwiss /api/chart/ + optional /api/tz/ auto-resolution, computes planet-in-house distinctions, returns enriched JSON - natus_save view: find-or-create draft Character, confirmed_at on action='confirm' - natus-wheel.js: D3 v7 SVG natal wheel (elements pie, signs, houses, planets, aspects, ASC/MC axes); NatusWheel.draw() / redraw() / clear() - _natus_overlay.html: Nominatim place autocomplete (debounced 400ms), geolocation button with reverse-geocode city name, live chart preview (debounced 300ms), tz auto-fill, NVM / SAVE SKY footer; html.natus-open class toggle pattern - _natus.scss: Gaussian backdrop+modal, two-column form|wheel layout, suggestion dropdown, portrait collapse at 600px, landscape sidebar z-index sink - room.html: include overlay when table_status == SKY_SELECT Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Integration tests for the PySwiss chart calculation API.
|
||||
|
||||
These tests drive the TDD implementation of GET /api/chart/.
|
||||
These tests drive the TDD implementation of GET /api/chart/ and GET /api/tz/.
|
||||
They verify the HTTP contract using Django's test client.
|
||||
|
||||
Run:
|
||||
@@ -18,6 +18,11 @@ from django.test import TestCase
|
||||
J2000 = '2000-01-01T12:00:00Z'
|
||||
LONDON = {'lat': 51.5074, 'lon': -0.1278}
|
||||
|
||||
# Well-known coordinates with unambiguous timezone results
|
||||
NEW_YORK = {'lat': 40.7128, 'lon': -74.0060} # America/New_York
|
||||
TOKYO = {'lat': 35.6762, 'lon': 139.6503} # Asia/Tokyo
|
||||
REYKJAVIK = {'lat': 64.1355, 'lon': -21.8954} # Atlantic/Reykjavik
|
||||
|
||||
|
||||
class ChartApiTest(TestCase):
|
||||
"""GET /api/chart/ — calculate a natal chart from datetime + coordinates."""
|
||||
@@ -124,3 +129,87 @@ class ChartApiTest(TestCase):
|
||||
"""House system override is superuser-only; plain requests get 403."""
|
||||
response = self._get({'dt': J2000, **LONDON, 'house_system': 'P'})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
# ── aspects ───────────────────────────────────────────────────────────
|
||||
|
||||
def test_chart_returns_aspects_list(self):
|
||||
data = self._get({'dt': J2000, **LONDON}).json()
|
||||
self.assertIn('aspects', data)
|
||||
self.assertIsInstance(data['aspects'], list)
|
||||
|
||||
def test_each_aspect_has_required_fields(self):
|
||||
data = self._get({'dt': J2000, **LONDON}).json()
|
||||
for aspect in data['aspects']:
|
||||
with self.subTest(aspect=aspect):
|
||||
self.assertIn('planet1', aspect)
|
||||
self.assertIn('planet2', aspect)
|
||||
self.assertIn('type', aspect)
|
||||
self.assertIn('angle', aspect)
|
||||
self.assertIn('orb', aspect)
|
||||
|
||||
def test_sun_saturn_trine_present_at_j2000(self):
|
||||
"""Sun ~280.37° (Capricorn) and Saturn ~40.73° (Taurus) are ~120.36° apart — Trine."""
|
||||
data = self._get({'dt': J2000, **LONDON}).json()
|
||||
pairs = {(a['planet1'], a['planet2'], a['type']) for a in data['aspects']}
|
||||
self.assertIn(('Sun', 'Saturn', 'Trine'), pairs)
|
||||
|
||||
|
||||
class TimezoneApiTest(TestCase):
|
||||
"""GET /api/tz/ — resolve IANA timezone from lat/lon coordinates."""
|
||||
|
||||
def _get(self, params):
|
||||
return self.client.get('/api/tz/', params)
|
||||
|
||||
# ── guards ────────────────────────────────────────────────────────────
|
||||
|
||||
def test_returns_400_if_lat_missing(self):
|
||||
response = self._get({'lon': -74.0060})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_returns_400_if_lon_missing(self):
|
||||
response = self._get({'lat': 40.7128})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_returns_400_for_invalid_lat(self):
|
||||
response = self._get({'lat': 'abc', 'lon': -74.0060})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_returns_400_for_out_of_range_lat(self):
|
||||
response = self._get({'lat': 999, 'lon': -74.0060})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_returns_400_for_out_of_range_lon(self):
|
||||
response = self._get({'lat': 40.7128, 'lon': 999})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
# ── response shape ────────────────────────────────────────────────────
|
||||
|
||||
def test_returns_200_for_valid_coords(self):
|
||||
response = self._get(NEW_YORK)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_response_is_json(self):
|
||||
response = self._get(NEW_YORK)
|
||||
self.assertIn('application/json', response['Content-Type'])
|
||||
|
||||
def test_response_contains_timezone_key(self):
|
||||
data = self._get(NEW_YORK).json()
|
||||
self.assertIn('timezone', data)
|
||||
|
||||
def test_timezone_is_a_string(self):
|
||||
data = self._get(NEW_YORK).json()
|
||||
self.assertIsInstance(data['timezone'], str)
|
||||
|
||||
# ── correctness ───────────────────────────────────────────────────────
|
||||
|
||||
def test_new_york_timezone(self):
|
||||
data = self._get(NEW_YORK).json()
|
||||
self.assertEqual(data['timezone'], 'America/New_York')
|
||||
|
||||
def test_tokyo_timezone(self):
|
||||
data = self._get(TOKYO).json()
|
||||
self.assertEqual(data['timezone'], 'Asia/Tokyo')
|
||||
|
||||
def test_reykjavik_timezone(self):
|
||||
data = self._get(REYKJAVIK).json()
|
||||
self.assertEqual(data['timezone'], 'Atlantic/Reykjavik')
|
||||
|
||||
Reference in New Issue
Block a user