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:
@@ -470,3 +470,141 @@ def active_sig_seat(room):
|
||||
if seat.significator_id is None:
|
||||
return seat
|
||||
return None
|
||||
|
||||
|
||||
# ── Astrological reference tables (seeded, never user-edited) ─────────────────
|
||||
|
||||
class Sign(models.Model):
|
||||
FIRE = 'Fire'
|
||||
EARTH = 'Earth'
|
||||
AIR = 'Air'
|
||||
WATER = 'Water'
|
||||
ELEMENT_CHOICES = [(e, e) for e in (FIRE, EARTH, AIR, WATER)]
|
||||
|
||||
CARDINAL = 'Cardinal'
|
||||
FIXED = 'Fixed'
|
||||
MUTABLE = 'Mutable'
|
||||
MODALITY_CHOICES = [(m, m) for m in (CARDINAL, FIXED, MUTABLE)]
|
||||
|
||||
name = models.CharField(max_length=20, unique=True)
|
||||
symbol = models.CharField(max_length=5) # ♈ ♉ … ♓
|
||||
element = models.CharField(max_length=5, choices=ELEMENT_CHOICES)
|
||||
modality = models.CharField(max_length=8, choices=MODALITY_CHOICES)
|
||||
order = models.PositiveSmallIntegerField(unique=True) # 0–11, Aries first
|
||||
start_degree = models.FloatField() # 0, 30, 60 … 330
|
||||
|
||||
class Meta:
|
||||
ordering = ['order']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Planet(models.Model):
|
||||
name = models.CharField(max_length=20, unique=True)
|
||||
symbol = models.CharField(max_length=5) # ☉ ☽ ☿ ♀ ♂ ♃ ♄ ♅ ♆ ♇
|
||||
order = models.PositiveSmallIntegerField(unique=True) # 0–9, Sun first
|
||||
|
||||
class Meta:
|
||||
ordering = ['order']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class AspectType(models.Model):
|
||||
name = models.CharField(max_length=20, unique=True)
|
||||
symbol = models.CharField(max_length=5) # ☌ ⚹ □ △ ☍
|
||||
angle = models.PositiveSmallIntegerField() # 0, 60, 90, 120, 180
|
||||
orb = models.FloatField() # max allowed orb in degrees
|
||||
|
||||
class Meta:
|
||||
ordering = ['angle']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class HouseLabel(models.Model):
|
||||
"""Life-area label for each of the 12 astrological houses (distinctions)."""
|
||||
|
||||
number = models.PositiveSmallIntegerField(unique=True) # 1–12
|
||||
name = models.CharField(max_length=30)
|
||||
keywords = models.CharField(max_length=100, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['number']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.number}: {self.name}"
|
||||
|
||||
|
||||
# ── Character ─────────────────────────────────────────────────────────────────
|
||||
|
||||
class Character(models.Model):
|
||||
"""A gamer's player-character for one seat in one game session.
|
||||
|
||||
Lifecycle:
|
||||
- Created (draft) when gamer opens PICK SKY overlay.
|
||||
- confirmed_at set on confirm → locked.
|
||||
- retired_at set on retirement → archived (seat may hold a new Character).
|
||||
|
||||
Active character for a seat: confirmed_at__isnull=False, retired_at__isnull=True.
|
||||
"""
|
||||
|
||||
PORPHYRY = 'O'
|
||||
PLACIDUS = 'P'
|
||||
KOCH = 'K'
|
||||
WHOLE = 'W'
|
||||
HOUSE_SYSTEM_CHOICES = [
|
||||
(PORPHYRY, 'Porphyry'),
|
||||
(PLACIDUS, 'Placidus'),
|
||||
(KOCH, 'Koch'),
|
||||
(WHOLE, 'Whole Sign'),
|
||||
]
|
||||
|
||||
# ── seat relationship ─────────────────────────────────────────────────
|
||||
seat = models.ForeignKey(
|
||||
TableSeat, on_delete=models.CASCADE, related_name='characters',
|
||||
)
|
||||
|
||||
# ── significator (set at PICK SKY) ────────────────────────────────────
|
||||
significator = models.ForeignKey(
|
||||
TarotCard, null=True, blank=True,
|
||||
on_delete=models.SET_NULL, related_name='character_significators',
|
||||
)
|
||||
|
||||
# ── natus input (what the gamer entered) ─────────────────────────────
|
||||
birth_dt = models.DateTimeField(null=True, blank=True) # UTC
|
||||
birth_lat = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
||||
birth_lon = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
||||
birth_place = models.CharField(max_length=200, blank=True) # display string only
|
||||
house_system = models.CharField(
|
||||
max_length=1, choices=HOUSE_SYSTEM_CHOICES, default=PORPHYRY,
|
||||
)
|
||||
|
||||
# ── computed natus snapshot (full PySwiss response) ───────────────────
|
||||
chart_data = models.JSONField(null=True, blank=True)
|
||||
|
||||
# ── celtic cross spread (added at PICK SEA) ───────────────────────────
|
||||
celtic_cross = models.JSONField(null=True, blank=True)
|
||||
|
||||
# ── lifecycle ─────────────────────────────────────────────────────────
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
confirmed_at = models.DateTimeField(null=True, blank=True)
|
||||
retired_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
status = 'confirmed' if self.confirmed_at else 'draft'
|
||||
return f"Character(seat={self.seat_id}, {status})"
|
||||
|
||||
@property
|
||||
def is_confirmed(self):
|
||||
return self.confirmed_at is not None
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return self.confirmed_at is not None and self.retired_at is None
|
||||
|
||||
Reference in New Issue
Block a user