sky wheel: element contributor display; sign + house tooltips — TDD
sky_save now re-fetches from PySwiss server-side on save so stored chart_data always carries enriched element format (contributors/stellia/ parades). New sky/data endpoint serves fresh PySwiss data to the My Sky applet on load, replacing the stale inline json_script approach. natus-wheel.js: sign ring slices (data-sign-name) and house ring slices (data-house) now have click handlers with _activateSign/_activateHouse; em-dash fallback added for classic elements with empty contributor lists. Action URLs sky/preview, sky/save, sky/data lose trailing slashes. Jasmine: T12 sign tooltip, T13 house tooltip, T14 enriched element contributor display (symbols, Stellium/Parade formations, em-dash fallback). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
"""Integration tests for the My Sky dashboard views.
|
||||
|
||||
sky_view — GET /dashboard/sky/ → renders sky template
|
||||
sky_preview — GET /dashboard/sky/preview/ → proxies to PySwiss (no DB write)
|
||||
sky_save — POST /dashboard/sky/save/ → saves natal data to User model
|
||||
sky_preview — GET /dashboard/sky/preview → proxies to PySwiss (no DB write)
|
||||
sky_save — POST /dashboard/sky/save → saves natal data to User model
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -113,14 +113,13 @@ class SkySaveTest(TestCase):
|
||||
self.assertEqual(response.status_code, 405)
|
||||
|
||||
def test_saves_sky_fields_to_user(self):
|
||||
chart = {"planets": {}, "houses": {}, "elements": {}}
|
||||
payload = {
|
||||
"birth_dt": "1990-06-15T08:30:00",
|
||||
"birth_lat": 51.5074,
|
||||
"birth_lon": -0.1278,
|
||||
"birth_place": "London, UK",
|
||||
"house_system": "O",
|
||||
"chart_data": chart,
|
||||
"chart_data": {},
|
||||
}
|
||||
response = self._post(payload)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -131,7 +130,6 @@ class SkySaveTest(TestCase):
|
||||
self.assertAlmostEqual(float(self.user.sky_birth_lon), -0.1278, places=3)
|
||||
self.assertEqual(self.user.sky_birth_place, "London, UK")
|
||||
self.assertEqual(self.user.sky_house_system, "O")
|
||||
self.assertEqual(self.user.sky_chart_data, chart)
|
||||
|
||||
def test_invalid_json_returns_400(self):
|
||||
response = self.client.post(
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import json
|
||||
import lxml.html
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from django.contrib.messages import get_messages
|
||||
from django.test import override_settings, TestCase
|
||||
@@ -482,3 +484,117 @@ class WalletAppletTest(TestCase):
|
||||
def test_wallet_applet_has_manage_link(self):
|
||||
[link] = self.parsed.cssselect("#id_applet_wallet a.wallet-manage-link")
|
||||
self.assertEqual(link.get("href"), "/dashboard/wallet/")
|
||||
|
||||
|
||||
ENRICHED_CHART = {
|
||||
"planets": {"Sun": {"lon": 10.0, "sign": "Aries", "house": 1, "degree": 10.0}},
|
||||
"houses": {"cusps": [float(i * 30) for i in range(12)]},
|
||||
"elements": {
|
||||
"Fire": {"count": 3, "contributors": ["Sun", "Mars", "Jupiter"]},
|
||||
"Stone": {"count": 1, "contributors": ["Venus"]},
|
||||
"Air": {"count": 2, "contributors": ["Mercury", "Uranus"]},
|
||||
"Water": {"count": 0, "contributors": []},
|
||||
"Time": {"count": 1, "stellia": ["Saturn"]},
|
||||
"Space": {"count": 1, "parades": ["Neptune"]},
|
||||
},
|
||||
"distinctions": [],
|
||||
"timezone": "UTC",
|
||||
}
|
||||
|
||||
BIRTH_PAYLOAD = {
|
||||
"birth_dt": "1990-06-15T12:00:00Z",
|
||||
"birth_lat": 51.5,
|
||||
"birth_lon": -0.1,
|
||||
"birth_place": "London",
|
||||
"house_system": "O",
|
||||
"chart_data": {"stale": True},
|
||||
}
|
||||
|
||||
|
||||
@override_settings(PYSWISS_URL="http://pyswiss-test")
|
||||
class SkySaveViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(
|
||||
email="disco@test.io",
|
||||
sky_birth_lat=51.5,
|
||||
sky_birth_lon=-0.1,
|
||||
sky_birth_place="London",
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def _post(self, payload=None):
|
||||
return self.client.post(
|
||||
"/dashboard/sky/save",
|
||||
data=json.dumps(payload or BIRTH_PAYLOAD),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
@patch("apps.dashboard.views.http_requests.get")
|
||||
def test_save_fetches_enriched_data_from_pyswiss(self, mock_get):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.raise_for_status.return_value = None
|
||||
mock_resp.json.return_value = dict(ENRICHED_CHART)
|
||||
mock_get.return_value = mock_resp
|
||||
|
||||
response = self._post()
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.user.refresh_from_db()
|
||||
saved = self.user.sky_chart_data
|
||||
self.assertIsNotNone(saved)
|
||||
fire = saved["elements"]["Fire"]
|
||||
self.assertIn("contributors", fire)
|
||||
self.assertIn("Sun", fire["contributors"])
|
||||
|
||||
@patch("apps.dashboard.views.http_requests.get")
|
||||
def test_save_falls_back_to_client_data_if_pyswiss_unreachable(self, mock_get):
|
||||
mock_get.side_effect = Exception("connection refused")
|
||||
client_chart = {"planets": {}, "elements": {"Fire": 2}}
|
||||
payload = dict(BIRTH_PAYLOAD, chart_data=client_chart)
|
||||
|
||||
response = self._post(payload)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.user.refresh_from_db()
|
||||
self.assertEqual(self.user.sky_chart_data, client_chart)
|
||||
|
||||
|
||||
@override_settings(PYSWISS_URL="http://pyswiss-test")
|
||||
class SkyNatusDataViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(
|
||||
email="disco@test.io",
|
||||
sky_birth_lat=51.5,
|
||||
sky_birth_lon=-0.1,
|
||||
sky_birth_place="London",
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
@patch("apps.dashboard.views.http_requests.get")
|
||||
def test_returns_enriched_chart_data(self, mock_get):
|
||||
from datetime import datetime, timezone as dt_timezone
|
||||
self.user.sky_birth_dt = datetime(1990, 6, 15, 12, 0, tzinfo=dt_timezone.utc)
|
||||
self.user.sky_house_system = "O"
|
||||
self.user.save(update_fields=["sky_birth_dt", "sky_house_system"])
|
||||
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.raise_for_status.return_value = None
|
||||
mock_resp.json.return_value = dict(ENRICHED_CHART)
|
||||
mock_get.return_value = mock_resp
|
||||
|
||||
response = self.client.get("/dashboard/sky/data")
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.content)
|
||||
self.assertIn("elements", data)
|
||||
fire = data["elements"]["Fire"]
|
||||
self.assertIn("contributors", fire)
|
||||
|
||||
def test_returns_404_if_no_birth_data_saved(self):
|
||||
response = self.client.get("/dashboard/sky/data")
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_requires_login(self):
|
||||
self.client.logout()
|
||||
response = self.client.get("/dashboard/sky/data")
|
||||
self.assertRedirects(response, "/?next=/dashboard/sky/data", fetch_redirect_response=False)
|
||||
|
||||
@@ -15,6 +15,7 @@ urlpatterns = [
|
||||
path('wallet/save-payment-method', views.save_payment_method, name='save_payment_method'),
|
||||
path('kit-bag/', views.kit_bag, name='kit_bag'),
|
||||
path('sky/', views.sky_view, name='sky'),
|
||||
path('sky/preview/', views.sky_preview, name='sky_preview'),
|
||||
path('sky/save/', views.sky_save, name='sky_save'),
|
||||
path('sky/preview', views.sky_preview, name='sky_preview'),
|
||||
path('sky/save', views.sky_save, name='sky_save'),
|
||||
path('sky/data', views.sky_natus_data, name='sky_natus_data'),
|
||||
]
|
||||
|
||||
@@ -301,8 +301,8 @@ def _sky_natus_preview(request):
|
||||
@login_required(login_url="/")
|
||||
def sky_view(request):
|
||||
return render(request, "apps/dashboard/sky.html", {
|
||||
"preview_url": request.build_absolute_uri("/dashboard/sky/preview/"),
|
||||
"save_url": request.build_absolute_uri("/dashboard/sky/save/"),
|
||||
"preview_url": request.build_absolute_uri("/dashboard/sky/preview"),
|
||||
"save_url": request.build_absolute_uri("/dashboard/sky/save"),
|
||||
"saved_sky": request.user.sky_chart_data,
|
||||
"saved_birth_dt": request.user.sky_birth_dt,
|
||||
"saved_birth_place": request.user.sky_birth_place,
|
||||
@@ -327,21 +327,72 @@ def sky_save(request):
|
||||
|
||||
user = request.user
|
||||
birth_dt_str = body.get('birth_dt', '')
|
||||
birth_dt_utc = None
|
||||
if birth_dt_str:
|
||||
try:
|
||||
naive = datetime.fromisoformat(birth_dt_str.replace('Z', '+00:00'))
|
||||
user.sky_birth_dt = naive if naive.tzinfo else naive.replace(
|
||||
tzinfo=zoneinfo.ZoneInfo('UTC')
|
||||
)
|
||||
birth_dt_utc = user.sky_birth_dt.astimezone(zoneinfo.ZoneInfo('UTC'))
|
||||
except ValueError:
|
||||
user.sky_birth_dt = None
|
||||
user.sky_birth_lat = body.get('birth_lat')
|
||||
user.sky_birth_lon = body.get('birth_lon')
|
||||
user.sky_birth_place = body.get('birth_place', '')
|
||||
user.sky_house_system = body.get('house_system', 'O')
|
||||
user.sky_chart_data = body.get('chart_data')
|
||||
|
||||
lat_str = body.get('birth_lat')
|
||||
lon_str = body.get('birth_lon')
|
||||
if birth_dt_utc and lat_str is not None and lon_str is not None:
|
||||
try:
|
||||
dt_iso = birth_dt_utc.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
resp = http_requests.get(
|
||||
settings.PYSWISS_URL + '/api/chart/',
|
||||
params={'dt': dt_iso, 'lat': str(lat_str), 'lon': str(lon_str)},
|
||||
timeout=5,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
enriched = resp.json()
|
||||
if 'elements' in enriched and 'Earth' in enriched['elements']:
|
||||
enriched['elements']['Stone'] = enriched['elements'].pop('Earth')
|
||||
user.sky_chart_data = enriched
|
||||
except Exception:
|
||||
user.sky_chart_data = body.get('chart_data')
|
||||
else:
|
||||
user.sky_chart_data = body.get('chart_data')
|
||||
|
||||
user.save(update_fields=[
|
||||
'sky_birth_dt', 'sky_birth_lat', 'sky_birth_lon',
|
||||
'sky_birth_place', 'sky_house_system', 'sky_chart_data',
|
||||
])
|
||||
return JsonResponse({"saved": True})
|
||||
|
||||
|
||||
@login_required(login_url="/")
|
||||
def sky_natus_data(request):
|
||||
user = request.user
|
||||
if not user.sky_birth_lat or not user.sky_birth_lon or not user.sky_birth_dt:
|
||||
return HttpResponse(status=404)
|
||||
|
||||
try:
|
||||
utc_dt = user.sky_birth_dt.astimezone(zoneinfo.ZoneInfo('UTC'))
|
||||
dt_iso = utc_dt.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
resp = http_requests.get(
|
||||
settings.PYSWISS_URL + '/api/chart/',
|
||||
params={
|
||||
'dt': dt_iso,
|
||||
'lat': str(user.sky_birth_lat),
|
||||
'lon': str(user.sky_birth_lon),
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
except Exception:
|
||||
return HttpResponse(status=502)
|
||||
|
||||
data = resp.json()
|
||||
if 'elements' in data and 'Earth' in data['elements']:
|
||||
data['elements']['Stone'] = data['elements'].pop('Earth')
|
||||
data['distinctions'] = _compute_distinctions(data['planets'], data['houses'])
|
||||
return JsonResponse(data)
|
||||
|
||||
Reference in New Issue
Block a user