Compare commits

...

4 Commits

Author SHA1 Message Date
Disco DeDisco
2b4f20c0e8 collapse migrations: 41 epic + 20 lyric + 12 applets + others → fresh initials + 4 themed seeds
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
- Delete all incremental migration files across all apps; regenerate 0001_initial.py
  per app via makemigrations (schema unchanged, no model edits)
- applets/0003_seed_applets.py: all 20 Applet rows in one migration
- epic/0003_seed_fiorentine_deck.py: Fiorentine Minchiate DeckVariant + 78 cards
- epic/0004_seed_earthman_deck.py: Earthman DeckVariant + 50 major + 56 minor/middle
  arcana (106 cards); stray PENTACLES courts absent — clean from the start
- epic/0005_seed_astro_reference_tables.py: 12 signs, 10 planets, 9 aspect types
  (incl. Semisquare + Sesquiquadrate), 12 house labels
- 706 ITs green on fresh DB

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 21:06:23 -04:00
Disco DeDisco
e2c9dc4e8a Earthman deck: delete stray Pentacles courts; fix tarot_fan suit order
- Migration 0041: delete Maid/Jack/Queen/King of Pentacles (suit=PENTACLES,
  arcana=MINOR, numbers 11-14) from Earthman deck — duplicates of the correct
  CROWNS courts that already exist; escaped migration 0024's rename sweep
- tarot_fan view: _suit_order now uses Earthman suit names (BRANDS/GRAILS/BLADES/CROWNS)
  in correct display order; old Fiorentine names kept as fallback

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 20:48:12 -04:00
Disco DeDisco
a724479e60 natus wheel: Semisquare & Sesquiquadrate; Ott orb pair detection; intensity sort
- pyswiss ASPECTS: uncomment Semisquare (45°, 4°) & Sesquiquadrate (135°, 4°) in angle order; update test known-types + max-orbs
- ALL_ASPECTS replaces MAJOR_ASPECTS (client-side angle detection now covers all 9 types)
- Ott's Orb system: PLANET_ORB per body (Sun/Moon=10, Mer/Ven/Mar=8, Jup/Sat=6, Ura/Nep/Plu=4); allowed orb = avg of two bodies
- _angleAspectWith uses per-planet Ott orb as detection threshold
- ASPECT_AV values + _sortAspects: intensity = A.V. × (1 − orb/ottOrb); aspects exceeding Ott orb filtered out; ties: separating first → partner importance → aspect importance

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 20:16:47 -04:00
Disco DeDisco
4b2e89c088 some basic palette tooltip style refinements 2026-04-27 16:43:14 -04:00
95 changed files with 1041 additions and 3559 deletions

View File

@@ -20,15 +20,15 @@ SIGN_ELEMENT = {
} }
ASPECTS = [ ASPECTS = [
('Conjunction', 0, 8.0), ('Conjunction', 0, 8.0),
('Semisextile', 30, 4.0), ('Semisextile', 30, 4.0),
('Sextile', 60, 6.0), ('Semisquare', 45, 4.0),
('Square', 90, 8.0), ('Sextile', 60, 6.0),
('Trine', 120, 8.0), ('Square', 90, 8.0),
('Quincunx', 150, 5.0), ('Trine', 120, 8.0),
('Opposition', 180, 10.0), ('Sesquiquadrate', 135, 4.0),
# ('Semisquare', 45, 4.0), ('Quincunx', 150, 5.0),
# ('Sesquiquadrate', 135, 4.0), ('Opposition', 180, 10.0),
] ]
PLANET_CODES = { PLANET_CODES = {

View File

@@ -233,8 +233,8 @@ class CalculateAspectsTest(SimpleTestCase):
def test_each_aspect_type_is_a_known_name(self): def test_each_aspect_type_is_a_known_name(self):
known = { known = {
'Conjunction', 'Semisextile', 'Sextile', 'Square', 'Conjunction', 'Semisextile', 'Semisquare', 'Sextile', 'Square',
'Trine', 'Quincunx', 'Opposition', 'Trine', 'Sesquiquadrate', 'Quincunx', 'Opposition',
} }
for aspect in self.aspects: for aspect in self.aspects:
with self.subTest(aspect=aspect): with self.subTest(aspect=aspect):
@@ -305,13 +305,15 @@ class CalculateAspectsTest(SimpleTestCase):
def test_orb_is_within_allowed_maximum(self): def test_orb_is_within_allowed_maximum(self):
max_orbs = { max_orbs = {
'Conjunction': 8.0, 'Conjunction': 8.0,
'Semisextile': 4.0, 'Semisextile': 4.0,
'Sextile': 6.0, 'Semisquare': 4.0,
'Square': 8.0, 'Sextile': 6.0,
'Trine': 8.0, 'Square': 8.0,
'Quincunx': 5.0, 'Trine': 8.0,
'Opposition': 10.0, 'Sesquiquadrate': 4.0,
'Quincunx': 5.0,
'Opposition': 10.0,
} }
for aspect in self.aspects: for aspect in self.aspects:
with self.subTest(aspect=aspect): with self.subTest(aspect=aspect):

View File

@@ -1,13 +1,14 @@
import django.db.models.deletion # Generated by Django 6.0 on 2026-04-28 00:59
from django.conf import settings import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
@@ -17,7 +18,7 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', models.SlugField(unique=True)), ('slug', models.SlugField(unique=True)),
('name', models.CharField(max_length=100)), ('name', models.CharField(max_length=100)),
('context', models.CharField(choices=[('dashboard', 'Dashboard'), ('gameboard', 'Gameboard')], default='dashboard', max_length=20)), ('context', models.CharField(choices=[('dashboard', 'Dashboard'), ('gameboard', 'Gameboard'), ('wallet', 'Wallet'), ('billboard', 'Billboard')], default='dashboard', max_length=20)),
('default_visible', models.BooleanField(default=True)), ('default_visible', models.BooleanField(default=True)),
('grid_cols', models.PositiveSmallIntegerField(default=12)), ('grid_cols', models.PositiveSmallIntegerField(default=12)),
('grid_rows', models.PositiveSmallIntegerField(default=3)), ('grid_rows', models.PositiveSmallIntegerField(default=3)),
@@ -29,8 +30,6 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('visible', models.BooleanField(default=True)), ('visible', models.BooleanField(default=True)),
('applet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='applets.applet')), ('applet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='applets.applet')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_applets', to=settings.AUTH_USER_MODEL)),
], ],
options={'unique_together': {('user', 'applet')}},
), ),
] ]

View File

@@ -0,0 +1,27 @@
# Generated by Django 6.0 on 2026-04-28 00:59
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('applets', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='userapplet',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_applets', to=settings.AUTH_USER_MODEL),
),
migrations.AlterUniqueTogether(
name='userapplet',
unique_together={('user', 'applet')},
),
]

View File

@@ -1,29 +0,0 @@
from django.db import migrations
def seed_applets(apps, schema_editor):
Applet = apps.get_model('applets', 'Applet')
for slug, name, cols, rows, context in [
('wallet', 'Wallet', 12, 3, 'dashboard'),
('new-list', 'New List', 9, 3, 'dashboard'),
('my-lists', 'My Lists', 3, 3, 'dashboard'),
('username', 'Username', 6, 3, 'dashboard'),
('palette', 'Palette', 6, 3, 'dashboard'),
('new-game', 'New Game', 4, 2, 'gameboard'),
('my-games', 'My Games', 4, 4, 'gameboard'),
('game-kit', 'Game Kit', 4, 2, 'gameboard'),
]:
Applet.objects.get_or_create(
slug=slug,
defaults={'name': name, 'grid_cols': cols, 'grid_rows': rows, 'context': context},
)
class Migration(migrations.Migration):
dependencies = [
('applets', '0001_initial')
]
operations = [
migrations.RunPython(seed_applets, migrations.RunPython.noop)
]

View File

@@ -0,0 +1,47 @@
"""Seed all Applet rows."""
from django.db import migrations
APPLETS = [
# (slug, name, context, default_visible, grid_cols, grid_rows)
('wallet', 'Wallet', 'dashboard', True, 12, 3),
('new-post', 'New Post', 'billboard', True, 9, 3),
('my-posts', 'My Posts', 'billboard', True, 3, 3),
('username', 'Username', 'dashboard', True, 6, 3),
('palette', 'Palette', 'dashboard', True, 6, 3),
('new-game', 'New Game', 'gameboard', True, 4, 3),
('my-games', 'My Games', 'gameboard', True, 4, 4),
('game-kit', 'Game Kit', 'gameboard', True, 4, 3),
('wallet-balances', 'Wallet Balances', 'wallet', True, 3, 3),
('wallet-tokens', 'Wallet Tokens', 'wallet', True, 3, 3),
('wallet-payment', 'Payment Methods', 'wallet', True, 6, 3),
('billboard-my-scrolls', 'My Scrolls', 'billboard', True, 4, 3),
('billboard-my-contacts', 'Contacts', 'billboard', True, 4, 3),
('billboard-most-recent', 'Most Recent', 'billboard', True, 8, 6),
('gk-trinkets', 'Trinkets', 'game-kit', True, 3, 3),
('gk-tokens', 'Tokens', 'game-kit', True, 3, 3),
('gk-decks', 'Card Decks', 'game-kit', True, 3, 3),
('gk-dice', 'Dice Sets', 'game-kit', True, 3, 3),
('my-sky', 'My Sky', 'dashboard', True, 6, 6),
('billboard-notes', 'My Notes', 'billboard', True, 4, 4),
]
def seed(apps, schema_editor):
Applet = apps.get_model('applets', 'Applet')
for slug, name, context, default_visible, grid_cols, grid_rows in APPLETS:
Applet.objects.create(
slug=slug, name=name, context=context,
default_visible=default_visible,
grid_cols=grid_cols, grid_rows=grid_rows,
)
class Migration(migrations.Migration):
dependencies = [
('applets', '0002_initial'),
]
operations = [
migrations.RunPython(seed, migrations.RunPython.noop),
]

View File

@@ -1,37 +0,0 @@
from django.db import migrations, models
def seed_wallet_applets(apps, schema_editor):
Applet = apps.get_model('applets', 'Applet')
for slug, name, cols, rows in [
('wallet-balances', 'Wallet Balances', 3, 3),
('wallet-tokens', 'Wallet Tokens', 3, 3),
('wallet-payment', 'Payment Methods', 6, 2),
]:
Applet.objects.get_or_create(
slug=slug,
defaults={'name': name, 'grid_cols': cols, 'grid_rows': rows, 'context': 'wallet'},
)
class Migration(migrations.Migration):
dependencies = [
('applets', '0002_seed_applets'),
]
operations = [
migrations.AlterField(
model_name='applet',
name='context',
field=models.CharField(
choices=[
('dashboard', 'Dashboard'),
('gameboard', 'Gameboard'),
('wallet', 'Wallet'),
],
default='dashboard',
max_length=20,
),
),
migrations.RunPython(seed_wallet_applets, migrations.RunPython.noop),
]

View File

@@ -1,24 +0,0 @@
from django.db import migrations
def rename_list_slugs(apps, schema_editor):
Applet = apps.get_model('applets', 'Applet')
Applet.objects.filter(slug='new-list').update(slug='new-note', name='New Note')
Applet.objects.filter(slug='my-lists').update(slug='my-notes', name='My Notes')
def reverse_rename_list_slugs(apps, schema_editor):
Applet = apps.get_model('applets', 'Applet')
Applet.objects.filter(slug='new-note').update(slug='new-list', name='New List')
Applet.objects.filter(slug='my-notes').update(slug='my-lists', name='My Lists')
class Migration(migrations.Migration):
dependencies = [
('applets', '0003_wallet_applets'),
]
operations = [
migrations.RunPython(rename_list_slugs, reverse_rename_list_slugs),
]

View File

@@ -1,24 +0,0 @@
from django.db import migrations
def increase_gameboard_applet_heights(apps, schema_editor):
Applet = apps.get_model('applets', 'Applet')
Applet.objects.filter(slug__in=['new-game', 'game-kit', 'wallet-payment']).update(grid_rows=3)
def revert_gameboard_applet_heights(apps, schema_editor):
Applet = apps.get_model('applets', 'Applet')
Applet.objects.filter(slug__in=['new-game', 'game-kit', 'wallet-payment']).update(grid_rows=2)
class Migration(migrations.Migration):
dependencies = [
('applets', '0004_rename_list_applet_slugs')
]
operations = [
migrations.RunPython(
increase_gameboard_applet_heights,
revert_gameboard_applet_heights,
)
]

View File

@@ -1,48 +0,0 @@
from django.db import migrations, models
def seed_billboard_applets(apps, schema_editor):
Applet = apps.get_model("applets", "Applet")
for slug, name, cols, rows in [
("billboard-my-scrolls", "My Scrolls", 4, 3),
("billboard-my-contacts", "Contacts", 4, 3),
("billboard-most-recent", "Most Recent", 8, 6),
]:
Applet.objects.get_or_create(
slug=slug,
defaults={"name": name, "grid_cols": cols, "grid_rows": rows, "context": "billboard"},
)
def remove_billboard_applets(apps, schema_editor):
Applet = apps.get_model("applets", "Applet")
Applet.objects.filter(slug__in=[
"billboard-my-scrolls",
"billboard-my-contacts",
"billboard-most-recent",
]).delete()
class Migration(migrations.Migration):
dependencies = [
("applets", "0005_gameboard_applet_heights"),
]
operations = [
migrations.AlterField(
model_name="applet",
name="context",
field=models.CharField(
choices=[
("dashboard", "Dashboard"),
("gameboard", "Gameboard"),
("wallet", "Wallet"),
("billboard", "Billboard"),
],
default="dashboard",
max_length=20,
),
),
migrations.RunPython(seed_billboard_applets, remove_billboard_applets),
]

View File

@@ -1,29 +0,0 @@
from django.db import migrations
def fix_billboard_applets(apps, schema_editor):
Applet = apps.get_model("applets", "Applet")
# billboard-scroll belongs only to the billscroll page template, not the grid
Applet.objects.filter(slug="billboard-scroll").delete()
# Rename "My Contacts" → "Contacts"
Applet.objects.filter(slug="billboard-my-contacts").update(name="Contacts")
def reverse_fix_billboard_applets(apps, schema_editor):
Applet = apps.get_model("applets", "Applet")
Applet.objects.get_or_create(
slug="billboard-scroll",
defaults={"name": "Billscroll", "grid_cols": 12, "grid_rows": 6, "context": "billboard"},
)
Applet.objects.filter(slug="billboard-my-contacts").update(name="My Contacts")
class Migration(migrations.Migration):
dependencies = [
("applets", "0006_billboard_applets"),
]
operations = [
migrations.RunPython(fix_billboard_applets, reverse_fix_billboard_applets),
]

View File

@@ -1,25 +0,0 @@
from django.db import migrations
def seed_game_kit_applets(apps, schema_editor):
Applet = apps.get_model('applets', 'Applet')
for slug, name in [
('gk-trinkets', 'Trinkets'),
('gk-tokens', 'Tokens'),
('gk-decks', 'Card Decks'),
('gk-dice', 'Dice Sets'),
]:
Applet.objects.get_or_create(
slug=slug,
defaults={'name': name, 'grid_cols': 3, 'grid_rows': 3, 'context': 'game-kit'},
)
class Migration(migrations.Migration):
dependencies = [
('applets', '0007_fix_billboard_applets'),
]
operations = [
migrations.RunPython(seed_game_kit_applets, migrations.RunPython.noop)
]

View File

@@ -1,24 +0,0 @@
from django.db import migrations
def seed_my_sky_applet(apps, schema_editor):
Applet = apps.get_model('applets', 'Applet')
Applet.objects.get_or_create(
slug='my-sky',
defaults={
'name': 'My Sky',
'grid_cols': 6,
'grid_rows': 6,
'context': 'dashboard',
},
)
class Migration(migrations.Migration):
dependencies = [
('applets', '0008_game_kit_applets'),
]
operations = [
migrations.RunPython(seed_my_sky_applet, migrations.RunPython.noop)
]

View File

@@ -1,25 +0,0 @@
from django.db import migrations
def seed_recognition_applet(apps, schema_editor):
Applet = apps.get_model("applets", "Applet")
Applet.objects.get_or_create(
slug="billboard-recognition",
defaults={
"name": "Recognition",
"grid_cols": 4,
"grid_rows": 4,
"context": "billboard",
},
)
class Migration(migrations.Migration):
dependencies = [
("applets", "0009_my_sky_applet"),
]
operations = [
migrations.RunPython(seed_recognition_applet, migrations.RunPython.noop),
]

View File

@@ -1,24 +0,0 @@
from django.db import migrations
def rename_note_applets_to_post(apps, schema_editor):
Applet = apps.get_model('applets', 'Applet')
Applet.objects.filter(slug='new-note').update(slug='new-post', name='New Post', context='billboard')
Applet.objects.filter(slug='my-notes').update(slug='my-posts', name='My Posts', context='billboard')
def reverse_rename_note_applets_to_post(apps, schema_editor):
Applet = apps.get_model('applets', 'Applet')
Applet.objects.filter(slug='new-post').update(slug='new-note', name='New Note', context='dashboard')
Applet.objects.filter(slug='my-posts').update(slug='my-notes', name='My Notes', context='dashboard')
class Migration(migrations.Migration):
dependencies = [
('applets', '0010_recognition_applet'),
]
operations = [
migrations.RunPython(rename_note_applets_to_post, reverse_rename_note_applets_to_post),
]

View File

@@ -1,31 +0,0 @@
from django.db import migrations
def rename_recognition_applet_to_notes(apps, schema_editor):
Applet = apps.get_model('applets', 'Applet')
Applet.objects.filter(slug='billboard-recognition').update(
slug='billboard-notes',
name='My Notes',
)
def reverse_rename_recognition_applet_to_notes(apps, schema_editor):
Applet = apps.get_model('applets', 'Applet')
Applet.objects.filter(slug='billboard-notes').update(
slug='billboard-recognition',
name='Recognition',
)
class Migration(migrations.Migration):
dependencies = [
('applets', '0011_rename_note_applets_to_post'),
]
operations = [
migrations.RunPython(
rename_recognition_applet_to_notes,
reverse_rename_recognition_applet_to_notes,
),
]

View File

@@ -1,8 +1,6 @@
# Generated by Django 6.0 on 2026-02-23 04:30 # Generated by Django 6.0 on 2026-04-28 00:59
import django.db.models.deletion
import uuid import uuid
from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@@ -11,28 +9,23 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='List', name='Line',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='lists', to=settings.AUTH_USER_MODEL)),
('shared_with', models.ManyToManyField(blank=True, related_name='shared_lists', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Item',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.TextField(default='')), ('text', models.TextField(default='')),
('list', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='dashboard.list')),
], ],
options={ options={
'ordering': ('id',), 'ordering': ('id',),
'unique_together': {('list', 'text')},
}, },
), ),
migrations.CreateModel(
name='Post',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
],
),
] ]

View File

@@ -1,4 +1,4 @@
# Generated by Django 6.0 on 2026-04-23 05:42 # Generated by Django 6.0 on 2026-04-28 00:59
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings
@@ -7,25 +7,31 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True
dependencies = [ dependencies = [
('dashboard', '0004_rename_note_to_post'), ('dashboard', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AddField(
model_name='line',
name='post',
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='dashboard.post'),
),
migrations.AlterField(
model_name='post', model_name='post',
name='owner', name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='posts', to=settings.AUTH_USER_MODEL), field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='posts', to=settings.AUTH_USER_MODEL),
), ),
migrations.AlterField( migrations.AddField(
model_name='post', model_name='post',
name='shared_with', name='shared_with',
field=models.ManyToManyField(blank=True, related_name='shared_posts', to=settings.AUTH_USER_MODEL), field=models.ManyToManyField(blank=True, related_name='shared_posts', to=settings.AUTH_USER_MODEL),
), ),
migrations.AddField(
model_name='line',
name='post',
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='dashboard.post'),
),
migrations.AlterUniqueTogether(
name='line',
unique_together={('post', 'text')},
),
] ]

View File

@@ -1,20 +0,0 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0001_initial'),
]
operations = [
migrations.RenameModel(
old_name='List',
new_name='Note',
),
migrations.RenameField(
model_name='Item',
old_name='list',
new_name='note',
),
]

View File

@@ -1,26 +0,0 @@
# Generated by Django 6.0 on 2026-03-12 19:30
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0002_rename_list_to_note'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='note',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notes', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='note',
name='shared_with',
field=models.ManyToManyField(blank=True, related_name='shared_notes', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -1,18 +0,0 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0003_alter_note_owner_alter_note_shared_with'),
]
operations = [
migrations.RenameModel('Note', 'Post'),
migrations.RenameModel('Item', 'Line'),
migrations.RenameField('Line', 'note', 'post'),
migrations.AlterUniqueTogether(
name='line',
unique_together={('post', 'text')},
),
]

View File

@@ -1,7 +1,5 @@
# Generated by Django 6.0 on 2026-03-19 18:34 # Generated by Django 6.0 on 2026-04-28 00:59
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@@ -10,8 +8,6 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('epic', '0006_table_status_and_table_seat'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
@@ -19,14 +15,32 @@ class Migration(migrations.Migration):
name='GameEvent', name='GameEvent',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('verb', models.CharField(choices=[('room_created', 'Room created'), ('slot_reserved', 'Gate slot reserved'), ('slot_filled', 'Gate slot filled'), ('slot_returned', 'Gate slot returned'), ('slot_released', 'Gate slot released'), ('invite_sent', 'Invite sent'), ('role_select_started', 'Role select started'), ('role_selected', 'Role selected'), ('roles_revealed', 'Roles revealed')], max_length=30)), ('verb', models.CharField(choices=[('room_created', 'Room created'), ('slot_reserved', 'Gate slot reserved'), ('slot_filled', 'Gate slot filled'), ('slot_returned', 'Gate slot returned'), ('slot_released', 'Gate slot released'), ('invite_sent', 'Invite sent'), ('role_select_started', 'Role select started'), ('role_selected', 'Role selected'), ('roles_revealed', 'Roles revealed'), ('sig_ready', 'Sig claim staked'), ('sig_unready', 'Sig claim withdrawn')], max_length=30)),
('data', models.JSONField(default=dict)), ('data', models.JSONField(default=dict)),
('timestamp', models.DateTimeField(auto_now_add=True)), ('timestamp', models.DateTimeField(auto_now_add=True)),
('actor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='game_events', to=settings.AUTH_USER_MODEL)),
('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='events', to='epic.room')),
], ],
options={ options={
'ordering': ['timestamp'], 'ordering': ['timestamp'],
}, },
), ),
migrations.CreateModel(
name='Note',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', models.SlugField(max_length=60)),
('earned_at', models.DateTimeField()),
('palette', models.CharField(blank=True, max_length=60, null=True)),
],
options={
'ordering': ['earned_at'],
},
),
migrations.CreateModel(
name='ScrollPosition',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('position', models.PositiveIntegerField(default=0)),
('updated_at', models.DateTimeField(auto_now=True)),
],
),
] ]

View File

@@ -0,0 +1,52 @@
# Generated by Django 6.0 on 2026-04-28 00:59
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('drama', '0001_initial'),
('epic', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='gameevent',
name='actor',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='game_events', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='gameevent',
name='room',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='events', to='epic.room'),
),
migrations.AddField(
model_name='note',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='scrollposition',
name='room',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scroll_positions', to='epic.room'),
),
migrations.AddField(
model_name='scrollposition',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scroll_positions', to=settings.AUTH_USER_MODEL),
),
migrations.AlterUniqueTogether(
name='note',
unique_together={('user', 'slug')},
),
migrations.AlterUniqueTogether(
name='scrollposition',
unique_together={('user', 'room')},
),
]

View File

@@ -1,30 +0,0 @@
# Generated by Django 6.0 on 2026-03-24 21:36
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('drama', '0001_initial'),
('epic', '0006_table_status_and_table_seat'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ScrollPosition',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('position', models.PositiveIntegerField(default=0)),
('updated_at', models.DateTimeField(auto_now=True)),
('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scroll_positions', to='epic.room')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scroll_positions', to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'room')},
},
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 6.0 on 2026-04-12 23:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('drama', '0002_scrollposition'),
]
operations = [
migrations.AlterField(
model_name='gameevent',
name='verb',
field=models.CharField(choices=[('room_created', 'Room created'), ('slot_reserved', 'Gate slot reserved'), ('slot_filled', 'Gate slot filled'), ('slot_returned', 'Gate slot returned'), ('slot_released', 'Gate slot released'), ('invite_sent', 'Invite sent'), ('role_select_started', 'Role select started'), ('role_selected', 'Role selected'), ('roles_revealed', 'Roles revealed'), ('sig_ready', 'Sig claim staked'), ('sig_unready', 'Sig claim withdrawn')], max_length=30),
),
]

View File

@@ -1,30 +0,0 @@
# Generated by Django 6.0 on 2026-04-22 06:11
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('drama', '0003_alter_gameevent_verb'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Recognition',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', models.SlugField(max_length=60)),
('earned_at', models.DateTimeField()),
('palette', models.CharField(blank=True, max_length=60, null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recognitions', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['earned_at'],
'unique_together': {('user', 'slug')},
},
),
]

View File

@@ -1,24 +0,0 @@
import django.conf
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('drama', '0004_recognition'),
migrations.swappable_dependency(django.conf.settings.AUTH_USER_MODEL),
]
operations = [
migrations.RenameModel('Recognition', 'Note'),
migrations.AlterField(
model_name='note',
name='user',
field=models.ForeignKey(
django.conf.settings.AUTH_USER_MODEL,
on_delete=django.db.models.deletion.CASCADE,
related_name='notes',
),
),
]

View File

@@ -1,8 +1,7 @@
# Generated by Django 6.0 on 2026-03-12 19:46 # Generated by Django 6.0 on 2026-04-28 00:59
import django.db.models.deletion import datetime
import uuid import uuid
from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@@ -11,22 +10,50 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Room', name='AspectType',
fields=[ fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)), ('name', models.CharField(max_length=20, unique=True)),
('visibility', models.CharField(choices=[('PRIVATE', 'Private'), ('PUBLIC', 'Public'), ('INVITE ONLY', 'Invite Only')], default='PRIVATE', max_length=20)), ('symbol', models.CharField(max_length=5)),
('gate_status', models.CharField(choices=[('GATHERING', 'Gathering'), ('OPEN', 'Open'), ('RENEWAL_DUE', 'Renewal Due')], default='GATHERING', max_length=20)), ('angle', models.PositiveSmallIntegerField()),
('renewal_period', models.DurationField(blank=True, null=True)), ('orb', models.FloatField()),
],
options={
'ordering': ['angle'],
},
),
migrations.CreateModel(
name='Character',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('birth_dt', models.DateTimeField(blank=True, null=True)),
('birth_lat', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
('birth_lon', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
('birth_place', models.CharField(blank=True, max_length=200)),
('house_system', models.CharField(choices=[('O', 'Porphyry'), ('P', 'Placidus'), ('K', 'Koch'), ('W', 'Whole Sign')], default='O', max_length=1)),
('chart_data', models.JSONField(blank=True, null=True)),
('celtic_cross', models.JSONField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)), ('created_at', models.DateTimeField(auto_now_add=True)),
('board_state', models.JSONField(default=dict)), ('confirmed_at', models.DateTimeField(blank=True, null=True)),
('seed_count', models.IntegerField(default=12)), ('retired_at', models.DateTimeField(blank=True, null=True)),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owned_rooms', to=settings.AUTH_USER_MODEL)), ],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='DeckVariant',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(unique=True)),
('card_count', models.IntegerField()),
('description', models.TextField(blank=True)),
('is_default', models.BooleanField(default=False)),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
@@ -37,9 +64,129 @@ class Migration(migrations.Migration):
('status', models.CharField(choices=[('EMPTY', 'Empty'), ('RESERVED', 'Reserved'), ('FILLED', 'Filled')], default='EMPTY', max_length=10)), ('status', models.CharField(choices=[('EMPTY', 'Empty'), ('RESERVED', 'Reserved'), ('FILLED', 'Filled')], default='EMPTY', max_length=10)),
('reserved_at', models.DateTimeField(blank=True, null=True)), ('reserved_at', models.DateTimeField(blank=True, null=True)),
('filled_at', models.DateTimeField(blank=True, null=True)), ('filled_at', models.DateTimeField(blank=True, null=True)),
('funded_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='funded_slots', to=settings.AUTH_USER_MODEL)), ('debited_token_type', models.CharField(blank=True, max_length=8, null=True)),
('gamer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='gate_slots', to=settings.AUTH_USER_MODEL)), ('debited_token_expires_at', models.DateTimeField(blank=True, null=True)),
('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gate_slots', to='epic.room')), ],
),
migrations.CreateModel(
name='HouseLabel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('number', models.PositiveSmallIntegerField(unique=True)),
('name', models.CharField(max_length=30)),
('keywords', models.CharField(blank=True, max_length=100)),
],
options={
'ordering': ['number'],
},
),
migrations.CreateModel(
name='Planet',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20, unique=True)),
('symbol', models.CharField(max_length=5)),
('order', models.PositiveSmallIntegerField(unique=True)),
],
options={
'ordering': ['order'],
},
),
migrations.CreateModel(
name='Room',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=200)),
('visibility', models.CharField(choices=[('PRIVATE', 'Private'), ('PUBLIC', 'Public'), ('INVITE ONLY', 'Invite Only')], default='PRIVATE', max_length=20)),
('gate_status', models.CharField(choices=[('GATHERING', 'GATHERING GAMERS'), ('OPEN', 'Open'), ('RENEWAL_DUE', 'Renewal Due')], default='GATHERING', max_length=20)),
('table_status', models.CharField(blank=True, choices=[('ROLE_SELECT', 'Role Select'), ('SIG_SELECT', 'Significator Select'), ('SKY_SELECT', 'Sky Select'), ('IN_GAME', 'In Game')], max_length=20, null=True)),
('sig_select_started_at', models.DateTimeField(blank=True, null=True)),
('renewal_period', models.DurationField(blank=True, default=datetime.timedelta(days=7), null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('board_state', models.JSONField(default=dict)),
('seed_count', models.IntegerField(default=12)),
],
),
migrations.CreateModel(
name='RoomInvite',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('invitee_email', models.EmailField(max_length=254)),
('status', models.CharField(choices=[('PENDING', 'Pending'), ('ACCEPTED', 'Accepted'), ('DECLINED', 'Declined')], default='PENDING', max_length=10)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='Sign',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20, unique=True)),
('symbol', models.CharField(max_length=5)),
('element', models.CharField(choices=[('Fire', 'Fire'), ('Earth', 'Earth'), ('Air', 'Air'), ('Water', 'Water')], max_length=5)),
('modality', models.CharField(choices=[('Cardinal', 'Cardinal'), ('Fixed', 'Fixed'), ('Mutable', 'Mutable')], max_length=8)),
('order', models.PositiveSmallIntegerField(unique=True)),
('start_degree', models.FloatField()),
],
options={
'ordering': ['order'],
},
),
migrations.CreateModel(
name='SigReservation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('role', models.CharField(max_length=2)),
('polarity', models.CharField(choices=[('levity', 'Levity'), ('gravity', 'Gravity')], max_length=7)),
('reserved_at', models.DateTimeField(auto_now_add=True)),
('ready', models.BooleanField(default=False)),
('countdown_remaining', models.IntegerField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='TableSeat',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slot_number', models.IntegerField()),
('role', models.CharField(blank=True, choices=[('PC', 'Player'), ('BC', 'Builder'), ('SC', 'Shepherd'), ('AC', 'Alchemist'), ('NC', 'Narrator'), ('EC', 'Economist')], max_length=2, null=True)),
('role_revealed', models.BooleanField(default=False)),
('seat_position', models.IntegerField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='TarotCard',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('arcana', models.CharField(choices=[('MAJOR', 'Major Arcana'), ('MINOR', 'Minor Arcana'), ('MIDDLE', 'Middle Arcana')], max_length=6)),
('suit', models.CharField(blank=True, choices=[('WANDS', 'Wands'), ('CUPS', 'Cups'), ('SWORDS', 'Swords'), ('PENTACLES', 'Pentacles'), ('CROWNS', 'Crowns'), ('BRANDS', 'Brands'), ('GRAILS', 'Grails'), ('BLADES', 'Blades')], max_length=10, null=True)),
('icon', models.CharField(blank=True, default='', max_length=50)),
('number', models.IntegerField()),
('slug', models.SlugField(max_length=120)),
('correspondence', models.CharField(blank=True, max_length=200)),
('group', models.CharField(blank=True, max_length=100)),
('reversal', models.CharField(blank=True, default='', max_length=200)),
('levity_qualifier', models.CharField(blank=True, default='', max_length=100)),
('gravity_qualifier', models.CharField(blank=True, default='', max_length=100)),
('levity_emanation', models.CharField(blank=True, default='', max_length=200)),
('gravity_emanation', models.CharField(blank=True, default='', max_length=200)),
('levity_reversal', models.CharField(blank=True, default='', max_length=200)),
('gravity_reversal', models.CharField(blank=True, default='', max_length=200)),
('mechanisms', models.JSONField(default=list)),
('articulations', models.JSONField(default=list)),
('keywords_upright', models.JSONField(default=list)),
('keywords_reversed', models.JSONField(default=list)),
('cautions', models.JSONField(default=list)),
],
options={
'ordering': ['deck_variant', 'arcana', 'suit', 'number'],
},
),
migrations.CreateModel(
name='TarotDeck',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('drawn_card_ids', models.JSONField(default=list)),
('created_at', models.DateTimeField(auto_now_add=True)),
], ],
), ),
] ]

View File

@@ -1,19 +0,0 @@
# Generated by Django 6.0 on 2026-03-13 20:32
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='room',
name='renewal_period',
field=models.DurationField(blank=True, default=datetime.timedelta(days=7), null=True),
),
]

View File

@@ -0,0 +1,120 @@
# Generated by Django 6.0 on 2026-04-28 00:59
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('epic', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='gateslot',
name='funded_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='funded_slots', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='gateslot',
name='gamer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='gate_slots', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='room',
name='owner',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owned_rooms', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='gateslot',
name='room',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gate_slots', to='epic.room'),
),
migrations.AddField(
model_name='roominvite',
name='inviter',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_invites', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='roominvite',
name='room',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites', to='epic.room'),
),
migrations.AddField(
model_name='sigreservation',
name='gamer',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sig_reservations', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='sigreservation',
name='room',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sig_reservations', to='epic.room'),
),
migrations.AddField(
model_name='tableseat',
name='gamer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='table_seats', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='tableseat',
name='room',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='table_seats', to='epic.room'),
),
migrations.AddField(
model_name='sigreservation',
name='seat',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sig_reservation', to='epic.tableseat'),
),
migrations.AddField(
model_name='character',
name='seat',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='characters', to='epic.tableseat'),
),
migrations.AddField(
model_name='tarotcard',
name='deck_variant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='cards', to='epic.deckvariant'),
),
migrations.AddField(
model_name='tableseat',
name='significator',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='significator_seats', to='epic.tarotcard'),
),
migrations.AddField(
model_name='sigreservation',
name='card',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sig_reservations', to='epic.tarotcard'),
),
migrations.AddField(
model_name='character',
name='significator',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='character_significators', to='epic.tarotcard'),
),
migrations.AddField(
model_name='tarotdeck',
name='deck_variant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='active_decks', to='epic.deckvariant'),
),
migrations.AddField(
model_name='tarotdeck',
name='room',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='tarot_deck', to='epic.room'),
),
migrations.AlterUniqueTogether(
name='tarotcard',
unique_together={('deck_variant', 'slug')},
),
migrations.AddConstraint(
model_name='sigreservation',
constraint=models.UniqueConstraint(fields=('room', 'gamer'), name='one_sig_reservation_per_gamer_per_room'),
),
migrations.AddConstraint(
model_name='sigreservation',
constraint=models.UniqueConstraint(fields=('room', 'card', 'polarity'), name='one_reservation_per_card_per_polarity_per_room'),
),
]

View File

@@ -1,27 +0,0 @@
# Generated by Django 6.0 on 2026-03-13 22:19
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0002_alter_room_renewal_period'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='RoomInvite',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('invitee_email', models.EmailField(max_length=254)),
('status', models.CharField(choices=[('PENDING', 'Pending'), ('ACCEPTED', 'Accepted'), ('DECLINED', 'Declined')], default='PENDING', max_length=10)),
('created_at', models.DateTimeField(auto_now_add=True)),
('inviter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_invites', to=settings.AUTH_USER_MODEL)),
('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites', to='epic.room')),
],
),
]

View File

@@ -0,0 +1,121 @@
"""Seed the Fiorentine Minchiate deck (78 cards)."""
from django.db import migrations
# (arcana, suit, number, name, slug, correspondence, group, icon, keywords_upright, keywords_reversed)
FIORENTINE_CARDS = [
("MAJOR","",0,"The Fool","the-fool","","","",["beginnings","spontaneity","freedom"],["recklessness","naivety","risk"]),
("MAJOR","",1,"The Magician","the-magician","","","",["willpower","skill","resourcefulness"],["manipulation","untapped potential","deceit"]),
("MAJOR","",2,"The High Priestess","the-high-priestess","","","",["intuition","mystery","inner knowledge"],["secrets","disconnection","withdrawal"]),
("MAJOR","",3,"The Empress","the-empress","","","",["fertility","abundance","nurturing"],["dependence","smothering","creative block"]),
("MAJOR","",4,"The Emperor","the-emperor","","","",["authority","structure","stability"],["rigidity","domination","inflexibility"]),
("MAJOR","",5,"The Hierophant","the-hierophant","","","",["tradition","conformity","institutions"],["rebellion","unconventionality","challenge"]),
("MAJOR","",6,"The Lovers","the-lovers","","","",["love","harmony","choice"],["disharmony","imbalance","misalignment"]),
("MAJOR","",7,"The Chariot","the-chariot","","","",["control","willpower","victory"],["aggression","lack of direction","defeat"]),
("MAJOR","",8,"Strength","strength","","","",["courage","patience","compassion"],["self-doubt","weakness","insecurity"]),
("MAJOR","",9,"The Hermit","the-hermit","","","",["introspection","guidance","solitude"],["isolation","loneliness","withdrawal"]),
("MAJOR","",10,"Wheel of Fortune","wheel-of-fortune","","","",["change","cycles","fate"],["bad luck","resistance","clinging to control"]),
("MAJOR","",11,"Justice","justice","","","",["fairness","truth","cause and effect"],["injustice","dishonesty","avoidance"]),
("MAJOR","",12,"The Hanged Man","the-hanged-man","","","",["pause","surrender","new perspective"],["stalling","resistance","indecision"]),
("MAJOR","",13,"Death","death","","","",["endings","transition","transformation"],["fear of change","stagnation","resistance"]),
("MAJOR","",14,"Temperance","temperance","","","",["balance","patience","moderation"],["imbalance","excess","lack of harmony"]),
("MAJOR","",15,"The Devil","the-devil","","","",["bondage","materialism","shadow self"],["detachment","freedom","releasing control"]),
("MAJOR","",16,"The Tower","the-tower","","","",["sudden change","upheaval","revelation"],["avoidance","fear of change","delaying disaster"]),
("MAJOR","",17,"The Star","the-star","","","",["hope","renewal","inspiration"],["despair","insecurity","hopelessness"]),
("MAJOR","",18,"The Moon","the-moon","","","",["illusion","fear","the unconscious"],["confusion","misinterpretation","clarity"]),
("MAJOR","",19,"The Sun","the-sun","","","",["positivity","success","vitality"],["negativity","depression","sadness"]),
("MAJOR","",20,"Judgement","judgement","","","",["reflection","reckoning","absolution"],["self-doubt","lack of self-awareness","loathing"]),
("MAJOR","",21,"The World","the-world","","","",["completion","integration","accomplishment"],["incompletion","no closure","shortcuts"]),
("MINOR","CUPS",1,"Ace of Cups","ace-of-cups","","","",["new feelings","intuition","opportunity"],["blocked creativity","emptiness","hesitation"]),
("MINOR","CUPS",2,"Two of Cups","two-of-cups","","","",["partnership","unity","celebration"],["imbalance","broken bonds","misalignment"]),
("MINOR","CUPS",3,"Three of Cups","three-of-cups","","","",["creativity","community","abundance"],["independence","isolation","looking inward"]),
("MINOR","CUPS",4,"Four of Cups","four-of-cups","","","",["contemplation","apathy","reevaluation"],["withdrawal","boredom","seeking motivation"]),
("MINOR","CUPS",5,"Five of Cups","five-of-cups","","","",["loss","grief","disappointment"],["acceptance","moving on","forgiveness"]),
("MINOR","CUPS",6,"Six of Cups","six-of-cups","","","",["nostalgia","reunion","joy"],["living in the past","naivety","unrealistic"]),
("MINOR","CUPS",7,"Seven of Cups","seven-of-cups","","","",["illusion","fantasy","wishful thinking"],["alignment","clarity","sobriety"]),
("MINOR","CUPS",8,"Eight of Cups","eight-of-cups","","","",["disappointment","abandonment","walking away"],["hopelessness","aimlessness","stagnation"]),
("MINOR","CUPS",9,"Nine of Cups","nine-of-cups","","","",["contentment","fulfilment","satisfaction"],["inner happiness","materialism","indulgence"]),
("MINOR","CUPS",10,"Ten of Cups","ten-of-cups","","","",["divine love","bliss","fulfilment"],["inner happiness","alignment","personal values"]),
("MINOR","CUPS",11,"Page of Cups","page-of-cups","","","",["sensitivity","creativity","intuition"],["insecurity","emotional immaturity","creative blocks"]),
("MINOR","CUPS",12,"Knight of Cups","knight-of-cups","","","",["compassion","romanticism","diplomacy"],["moodiness","emotional manipulation","deception"]),
("MINOR","CUPS",13,"Queen of Cups","queen-of-cups","","","",["compassion","empathy","nurturing"],["emotional insecurity","over-giving","neglect"]),
("MINOR","CUPS",14,"King of Cups","king-of-cups","","","",["emotional maturity","diplomacy","wisdom"],["manipulation","moodiness","coldness"]),
("MINOR","PENTACLES",1,"Ace of Pentacles","ace-of-pentacles","","","",["opportunity","new venture","manifestation"],["lost opportunity","lack of planning","scarcity"]),
("MINOR","PENTACLES",2,"Two of Pentacles","two-of-pentacles","","","",["juggling resources","flexibility","fun"],["imbalance","disorganisation","overwhelm"]),
("MINOR","PENTACLES",3,"Three of Pentacles","three-of-pentacles","","","",["teamwork","building","apprenticeship"],["lack of teamwork","disharmony","misalignment"]),
("MINOR","PENTACLES",4,"Four of Pentacles","four-of-pentacles","","","",["stability","security","conservation"],["greed","stinginess","possessiveness"]),
("MINOR","PENTACLES",5,"Five of Pentacles","five-of-pentacles","","","",["isolation","insecurity","worry"],["recovery from loss","overcoming hardship"]),
("MINOR","PENTACLES",6,"Six of Pentacles","six-of-pentacles","","","",["generosity","charity","community"],["strings attached","power dynamics","inequality"]),
("MINOR","PENTACLES",7,"Seven of Pentacles","seven-of-pentacles","","","",["hard work","perseverance","diligence"],["lack of reward","laziness","low quality"]),
("MINOR","PENTACLES",8,"Eight of Pentacles","eight-of-pentacles","","","",["apprenticeship","education","skill"],["perfectionism","misdirected activity","misuse"]),
("MINOR","PENTACLES",9,"Nine of Pentacles","nine-of-pentacles","","","",["abundance","luxury","self-sufficiency"],["overindulgence","superficiality","materialism"]),
("MINOR","PENTACLES",10,"Ten of Pentacles","ten-of-pentacles","","","",["wealth","financial security","achievement"],["financial failure","greed","lost success"]),
("MINOR","PENTACLES",11,"Page of Pentacles","page-of-pentacles","","","",["ambition","diligence","management"],["underhandedness","greediness","unethical"]),
("MINOR","PENTACLES",12,"Knight of Pentacles","knight-of-pentacles","","","",["hard work","productivity","routine"],["laziness","obsession with work","burnout"]),
("MINOR","PENTACLES",13,"Queen of Pentacles","queen-of-pentacles","","","",["nurturing","practical","abundance"],["financial dependence","smothering","insecurity"]),
("MINOR","PENTACLES",14,"King of Pentacles","king-of-pentacles","","","",["abundance","prosperity","security"],["greed","indulgence","sensual obsession"]),
("MINOR","SWORDS",1,"Ace of Swords","ace-of-swords","","","",["raw power","breakthrough","clarity"],["confusion","brutality","mental chaos"]),
("MINOR","SWORDS",2,"Two of Swords","two-of-swords","","","",["difficult choices","stalemate","truce"],["indecision","lies","confusion"]),
("MINOR","SWORDS",3,"Three of Swords","three-of-swords","","","",["heartbreak","sorrow","grief"],["recovery","forgiveness","moving on"]),
("MINOR","SWORDS",4,"Four of Swords","four-of-swords","","","",["rest","restoration","retreat"],["restlessness","burnout","illness"]),
("MINOR","SWORDS",5,"Five of Swords","five-of-swords","","","",["defeat","change","transition"],["resistance to change","inability to move"]),
("MINOR","SWORDS",6,"Six of Swords","six-of-swords","","","",["victory","success","ambition"],["an eye for an eye","dishonour","manipulation"]),
("MINOR","SWORDS",7,"Seven of Swords","seven-of-swords","","","",["deception","trickery","tactics"],["imposter syndrome","coming clean","rethinking"]),
("MINOR","SWORDS",8,"Eight of Swords","eight-of-swords","","","",["restriction","isolation","imprisonment"],["self-limiting beliefs","inner critic","opening up"]),
("MINOR","SWORDS",9,"Nine of Swords","nine-of-swords","","","",["anxiety","worry","fear"],["recovery from anxiety","inner turmoil","secrets"]),
("MINOR","SWORDS",10,"Ten of Swords","ten-of-swords","","","",["ruin","painful endings","loss"],["recovery","regeneration","resisting an end"]),
("MINOR","SWORDS",11,"Page of Swords","page-of-swords","","","",["new ideas","mental agility","curiosity"],["manipulation","all talk no action","ruthlessness"]),
("MINOR","SWORDS",12,"Knight of Swords","knight-of-swords","","","",["action","impulsiveness","ambition"],["no direction","disregard for consequences"]),
("MINOR","SWORDS",13,"Queen of Swords","queen-of-swords","","","",["clarity","directness","structure"],["coldness","cruelty","manipulation"]),
("MINOR","SWORDS",14,"King of Swords","king-of-swords","","","",["mental clarity","truth","authority"],["abuse of power","manipulation","coldness"]),
("MINOR","WANDS",1,"Ace of Wands","ace-of-wands","","","",["inspiration","new venture","spark"],["delays","lack of motivation","false start"]),
("MINOR","WANDS",2,"Two of Wands","two-of-wands","","","",["planning","progress","decisions"],["impatience","lack of planning","hesitation"]),
("MINOR","WANDS",3,"Three of Wands","three-of-wands","","","",["expansion","foresight","enterprise"],["obstacles","lack of foresight","delays"]),
("MINOR","WANDS",4,"Four of Wands","four-of-wands","","","",["celebration","harmony","homecoming"],["lack of support","transience","home conflicts"]),
("MINOR","WANDS",5,"Five of Wands","five-of-wands","","","",["conflict","competition","tension"],["avoiding conflict","compromise","truce"]),
("MINOR","WANDS",6,"Six of Wands","six-of-wands","","","",["victory","recognition","progress"],["excess pride","lack of recognition","fall"]),
("MINOR","WANDS",7,"Seven of Wands","seven-of-wands","","","",["challenge","courage","competition"],["anxiety","giving up","overwhelmed"]),
("MINOR","WANDS",8,"Eight of Wands","eight-of-wands","","","",["rapid action","adventure","change"],["haste","scattered energy","delays"]),
("MINOR","WANDS",9,"Nine of Wands","nine-of-wands","","","",["resilience","persistence","last stand"],["exhaustion","giving up","surrender"]),
("MINOR","WANDS",10,"Ten of Wands","ten-of-wands","","","",["completion","celebration","travel"],["burdens","oppression","carrying too much"]),
("MINOR","WANDS",11,"Page of Wands","page-of-wands","","","",["exploration","enthusiasm","adventure"],["hasty decisions","scattered energy","immaturity"]),
("MINOR","WANDS",12,"Knight of Wands","knight-of-wands","","","",["energy","passion","adventure"],["scattered energy","frustration","aggression"]),
("MINOR","WANDS",13,"Queen of Wands","queen-of-wands","","","",["confidence","independence","courage"],["selfishness","jealousy","insecurity"]),
("MINOR","WANDS",14,"King of Wands","king-of-wands","","","",["big picture","leadership","vision"],["impulsiveness","haste","overconfidence"]),
]
def seed(apps, schema_editor):
DeckVariant = apps.get_model('epic', 'DeckVariant')
TarotCard = apps.get_model('epic', 'TarotCard')
deck = DeckVariant.objects.create(
name='Fiorentine Minchiate',
slug='fiorentine-minchiate',
card_count=78,
is_default=False,
)
for arcana, suit, number, name, slug, correspondence, group, icon, kw_up, kw_rev in FIORENTINE_CARDS:
TarotCard.objects.create(
deck_variant=deck,
arcana=arcana,
suit=suit or None,
number=number,
name=name,
slug=slug,
correspondence=correspondence,
group=group,
icon=icon,
keywords_upright=kw_up,
keywords_reversed=kw_rev,
)
class Migration(migrations.Migration):
dependencies = [
('epic', '0002_initial'),
]
operations = [
migrations.RunPython(seed, migrations.RunPython.noop),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 6.0 on 2026-03-15 00:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0003_roominvite'),
]
operations = [
migrations.AlterField(
model_name='room',
name='gate_status',
field=models.CharField(choices=[('GATHERING', 'GATHERING GAMERS'), ('OPEN', 'Open'), ('RENEWAL_DUE', 'Renewal Due')], default='GATHERING', max_length=20),
),
]

View File

@@ -0,0 +1,184 @@
"""Seed the Earthman deck: DeckVariant + 50 major + 56 minor/middle arcana (106 cards total)."""
from django.db import migrations
# Major arcana
# (number, name, slug, group, correspondence, icon,
# reversal, levity_qualifier, gravity_qualifier,
# levity_emanation, gravity_emanation, levity_reversal, gravity_reversal,
# mechanisms, articulations, keywords_upright, keywords_reversed, cautions)
EM_MAJOR = [
(0,"The Nomad","the-nomad","","The Fool / Il Matto","fa-hat-cowboy-side","","","","","","","",[], [],["beginnings","spontaneity","freedom"],["recklessness","naivety","risk"],[]),
(1,"The Schizo","the-schizo","Pope/Horseman","Magician","fa-hat-wizard","Territoriality","Enlightened","Engraven","","","","",[], [],["willpower","skill","resourcefulness"],["manipulation","untapped potential","deceit"],["This card will reverse into <span class=\"card-ref\">I. The Pervert</span> when it comes under dominion of <span class=\"card-ref\">II. The Occultist</span>, which in turn reverses into <span class=\"card-ref\">II. Pestilence</span>.","This card will reverse into <span class=\"card-ref\">I. The Paranoiac</span> when it comes under dominion of <span class=\"card-ref\">III. The Despot</span>, which in turn reverses into <span class=\"card-ref\">III. War</span>.","This card will reverse into <span class=\"card-ref\">I. The Neurotic</span> when it comes under dominion of <span class=\"card-ref\">IV. The Capitalist</span>, which in turn reverses into <span class=\"card-ref\">IV. Famine</span>.","This card will reverse into <span class=\"card-ref\">I. The Suicidal</span> when it comes under dominion of <span class=\"card-ref\">V. The Fascist</span>, which in turn reverses into <span class=\"card-ref\">V. Death</span>."]),
(2,"The Occultist","the-occultist","Pope/Horseman","High Priestess","fa-hand-dots","Territoriality","Enlightened","Engraven","","","","",[], [],["intuition","mystery","inner knowledge"],["secrets","disconnection","withdrawal"],[]),
(3,"The Despot","the-despot","Pope/Horseman","Empress","fa-hand-dots","Despotism","Enlightened","Engraven","","","","",[], [],["fertility","abundance","nurturing"],["dependence","smothering","creative block"],[]),
(4,"The Capitalist","the-capitalist","Pope/Horseman","Emperor","fa-hand-dots","Capitalism","Enlightened","Engraven","","","","",[], [],["authority","structure","stability"],["rigidity","domination","inflexibility"],[]),
(5,"The Fascist","the-fascist","Pope/Horseman","Hierophant","fa-hand-dots","Fascism","Enlightened","Engraven","","","","",[], [],["tradition","conformity","institutions"],["rebellion","unconventionality","challenge"],[]),
(6,"Controlled Folly","controlled-folly","Implicit Virtues","Fortitude","fa-hand-dots","","","","","","","",[], [],["courage","patience","compassion"],["self-doubt","weakness","insecurity"],[]),
(7,"Not Doing","not-doing","Implicit Virtues","Temperance","fa-hand-dots","","","","","","","",[], [],["fairness","truth","cause and effect"],["injustice","dishonesty","avoidance"],[]),
(8,"Losing Self-Importance","losing-self-importance","Implicit Virtues","Justice","fa-hand-dots","","","","","","","",[], [],["balance","patience","moderation"],["imbalance","excess","lack of harmony"],[]),
(9,"Erasing Personal History","erasing-personal-history","Implicit Virtues","Prudence","fa-hand-dots","","","","","","","",[], [],[],[],[]),
(10,"Space","space-em","Elements","Chariot","fa-hand-dots","Nexus","Kinetic","Potential","","","","",[], [],[],[],[]),
(11,"Time","time-em","Elements","Hermit","fa-hand-dots","Tempo","Kinetic","Potential","","","","",[], [],[],[],[]),
(12,"Stone","stone-em","Elements","Earth","fa-hand-dots","Ossum","Kinetic","Potential","","","","",[], [],[],[],[]),
(13,"Fire","fire-em","Elements","Fire","fa-hand-dots","Ardor","Kinetic","Potential","","","","",[], [],[],[],[]),
(14,"Water","water-em","Elements","Water","fa-hand-dots","Humor","Kinetic","Potential","","","","",[], [],[],[],[]),
(15,"Air","air-em","Elements","Air","fa-hand-dots","Pneuma","Kinetic","Potential","","","","",[], [],[],[],[]),
(16,"Disco Inferno","disco-inferno","Realms","Devil","fa-hand-dots","Shame","Deasil","Widdershins","","","","",[], [],["sudden change","upheaval","revelation"],["avoidance","fear of change","delaying disaster"],[]),
(17,"Torre Terrestre","torre-terrestre","Realms","Tower","fa-hand-dots","Guilt","Deasil","Widdershins","","","","",[], [],[],[],[]),
(18,"Fantasia Celestia","fantasia-celestia","Realms","Wheel of Fortune","fa-hand-dots","Anxiety","Deasil","Widdershins","","","","",[], [],[],[],[]),
(19,"Stalking","stalking","Explicit Virtues","Charity","fa-hand-dots","","","","","","","",[], [],["love","harmony","choice"],["disharmony","imbalance","misalignment"],[]),
(20,"Dreaming","dreaming","Explicit Virtues","Faith","fa-hand-dots","","","","","","","",[], [],[],[],[]),
(21,"Intent","intent","Explicit Virtues","Hope","fa-hand-dots","","","","","","","",[], [],[],[],[]),
(22,"Aries","aries","Zodiac Signs & Houses","Aries","fa-hand-dots","House of Self","Precessional","Recessional","","","","",[], [],[],[],[]),
(23,"Taurus","taurus","Zodiac Signs & Houses","Taurus","fa-hand-dots","House of Worth","Precessional","Recessional","","","","",[], [],[],[],[]),
(24,"Gemini","gemini","Zodiac Signs & Houses","Gemini","fa-hand-dots","House of Education","Precessional","Recessional","","","","",[], [],[],[],[]),
(25,"Cancer","cancer","Zodiac Signs & Houses","Cancer","fa-hand-dots","House of Family","Precessional","Recessional","","","","",[], [],[],[],[]),
(26,"Leo","leo","Zodiac Signs & Houses","Leo","fa-hand-dots","House of Creation","Precessional","Recessional","","","","",[], [],[],[],[]),
(27,"Virgo","virgo","Zodiac Signs & Houses","Virgo","fa-hand-dots","House of Ritual","Precessional","Recessional","","","","",[], [],[],[],[]),
(28,"Libra","libra","Zodiac Signs & Houses","Libra","fa-hand-dots","House of Cooperation","Precessional","Recessional","","","","",[], [],[],[],[]),
(29,"Scorpio","scorpio","Zodiac Signs & Houses","Scorpio","fa-hand-dots","House of Regeneration","Precessional","Recessional","","","","",[], [],[],[],[]),
(30,"Sagittarius","sagittarius","Zodiac Signs & Houses","Sagittarius","fa-hand-dots","House of Enterprise","Precessional","Recessional","","","","",[], [],[],[],[]),
(31,"Capricorn","capricorn","Zodiac Signs & Houses","Capricorn","fa-hand-dots","House of Career","Precessional","Recessional","","","","",[], [],[],[],[]),
(32,"Aquarius","aquarius","Zodiac Signs & Houses","Aquarius","fa-hand-dots","House of Reward","Precessional","Recessional","","","","",[], [],[],[],[]),
(33,"Pisces","pisces","Zodiac Signs & Houses","Pisces","fa-hand-dots","House of Reprisal","Precessional","Recessional","","","","",[], [],[],[],[]),
(34,"Animal Powers","animal-powers","Lunars","Hanged Man","fa-hand-dots","Patrilineage","Centrifugal","Centripetal","","","","",[], [],["pause","surrender","new perspective"],["stalling","resistance","indecision"],[]),
(35,"Seeded Earth","seeded-earth","Lunars","Death","fa-hand-dots","Matrilineage","Centrifugal","Centripetal","","","","",[], [],["endings","transition","transformation"],["fear of change","stagnation","resistance"],[]),
(36,"The Twins of Pluto","twins-of-pluto","Lunars","Pluto","fa-hand-dots","","Prograde","Retrograde","","","","",[], [],[],[],[]),
(37,"Neptune","neptune","Planets","Neptune","fa-hand-dots","","Prograde","Retrograde","","","","",[], [],[],[],[]),
(38,"Uranus","uranus","Planets","Uranus","fa-hand-dots","","Prograde","Retrograde","","","","",[], [],[],[],[]),
(39,"Saturn","saturn","Planets","Saturn","fa-hand-dots","","Prograde","Retrograde","","","","",[], [],[],[],[]),
(40,"Jupiter","jupiter","Planets","Jupiter","fa-hand-dots","","Prograde","Retrograde","","","","",[], [],[],[],[]),
(41,"The Asteroid Belt","the-asteroid-belt","Planets","","","Ouroboros","Prograde","Retrograde","","","","",[], [],[],[],[]),
(42,"Mars","mars","Planets","Mars","fa-hand-dots","","Prograde","Retrograde","","","","",[], [],[],[],[]),
(43,"Venus","venus","Planets","Venus","fa-hand-dots","","Prograde","Retrograde","","","","",[], [],[],[],[]),
(44,"Mercury","mercury","Planets","Mercury","fa-hand-dots","","Prograde","Retrograde","","","","",[], [],[],[],[]),
(45,"The Polestar","the-polestar","Inner Rings","Star","fa-hand-dots","","Precessional","Recessional","","","","",[], [],["hope","renewal","inspiration"],["despair","insecurity","hopelessness"],[]),
(46,"The Antichthon","the-antichthon","Inner Rings","Moon","fa-hand-dots","","Waxing","Waning","","","","",[], [],["illusion","fear","the unconscious"],["confusion","misinterpretation","clarity"],[]),
(47,"The Corestar","the-corestar","Inner Rings","Sun","fa-hand-dots","","Inclining","Declining","","","","",[], [],["positivity","success","vitality"],["negativity","depression","sadness"],[]),
(48,"Father Sky / Mother Sea","father-sky-mother-sea","","World","fa-hand-dots","","","","Father Sky","Mother Sea","The Storm","The Flood",[], [],["completion","integration","accomplishment"],["incompletion","no closure","shortcuts"],[]),
(49,"The Effulgent Mould of Man / The Devouring Eagle","effulgent-mould-devouring-eagle","","Trumpets","fa-hand-dots","","","","The Effulgent Mould of Man","The Devouring Eagle","","",[], [],["reflection","reckoning","absolution"],["self-doubt","lack of self-awareness","loathing"],[]),
]
# Minor/Middle arcana
# (arcana, suit, number, name, slug, keywords_upright, keywords_reversed)
EM_MINOR = [
("MINOR","BRANDS",1,"Ace of Brands","ace-of-wands-em",["inspiration","new venture","spark"],["delays","lack of motivation","false start"]),
("MINOR","BRANDS",2,"Two of Brands","two-of-wands-em",["planning","progress","decisions"],["impatience","lack of planning","hesitation"]),
("MINOR","BRANDS",3,"Three of Brands","three-of-wands-em",["expansion","foresight","enterprise"],["obstacles","lack of foresight","delays"]),
("MINOR","BRANDS",4,"Four of Brands","four-of-wands-em",["celebration","harmony","homecoming"],["lack of support","transience","home conflicts"]),
("MINOR","BRANDS",5,"Five of Brands","five-of-wands-em",["conflict","competition","tension"],["avoiding conflict","compromise","truce"]),
("MINOR","BRANDS",6,"Six of Brands","six-of-wands-em",["victory","recognition","progress"],["excess pride","lack of recognition","fall"]),
("MINOR","BRANDS",7,"Seven of Brands","seven-of-wands-em",["challenge","courage","competition"],["anxiety","giving up","overwhelmed"]),
("MINOR","BRANDS",8,"Eight of Brands","eight-of-wands-em",["rapid action","adventure","change"],["haste","scattered energy","delays"]),
("MINOR","BRANDS",9,"Nine of Brands","nine-of-wands-em",["resilience","persistence","last stand"],["exhaustion","giving up","surrender"]),
("MINOR","BRANDS",10,"Ten of Brands","ten-of-wands-em",["completion","celebration","travel"],["burdens","oppression","carrying too much"]),
("MIDDLE","BRANDS",11,"Maid of Brands","maid-of-wands-em",["exploration","enthusiasm","adventure"],["hasty decisions","scattered energy","immaturity"]),
("MIDDLE","BRANDS",12,"Jack of Brands","jack-of-wands-em",["energy","passion","adventure"],["scattered energy","frustration","aggression"]),
("MIDDLE","BRANDS",13,"Queen of Brands","queen-of-wands-em",["confidence","independence","courage"],["selfishness","jealousy","insecurity"]),
("MIDDLE","BRANDS",14,"King of Brands","king-of-wands-em",["big picture","leadership","vision"],["impulsiveness","haste","overconfidence"]),
("MINOR","GRAILS",1,"Ace of Grails","ace-of-cups-em",["new feelings","intuition","opportunity"],["blocked creativity","emptiness","hesitation"]),
("MINOR","GRAILS",2,"Two of Grails","two-of-cups-em",["partnership","unity","celebration"],["imbalance","broken bonds","misalignment"]),
("MINOR","GRAILS",3,"Three of Grails","three-of-cups-em",["creativity","community","abundance"],["independence","isolation","looking inward"]),
("MINOR","GRAILS",4,"Four of Grails","four-of-cups-em",["contemplation","apathy","reevaluation"],["withdrawal","boredom","seeking motivation"]),
("MINOR","GRAILS",5,"Five of Grails","five-of-cups-em",["loss","grief","disappointment"],["acceptance","moving on","forgiveness"]),
("MINOR","GRAILS",6,"Six of Grails","six-of-cups-em",["nostalgia","reunion","joy"],["living in the past","naivety","unrealistic"]),
("MINOR","GRAILS",7,"Seven of Grails","seven-of-cups-em",["illusion","fantasy","wishful thinking"],["alignment","clarity","sobriety"]),
("MINOR","GRAILS",8,"Eight of Grails","eight-of-cups-em",["disappointment","abandonment","walking away"],["hopelessness","aimlessness","stagnation"]),
("MINOR","GRAILS",9,"Nine of Grails","nine-of-cups-em",["contentment","fulfilment","satisfaction"],["inner happiness","materialism","indulgence"]),
("MINOR","GRAILS",10,"Ten of Grails","ten-of-cups-em",["divine love","bliss","fulfilment"],["inner happiness","alignment","personal values"]),
("MIDDLE","GRAILS",11,"Maid of Grails","maid-of-cups-em",["sensitivity","creativity","intuition"],["insecurity","emotional immaturity","creative blocks"]),
("MIDDLE","GRAILS",12,"Jack of Grails","jack-of-cups-em",["compassion","romanticism","diplomacy"],["moodiness","emotional manipulation","deception"]),
("MIDDLE","GRAILS",13,"Queen of Grails","queen-of-cups-em",["compassion","empathy","nurturing"],["emotional insecurity","over-giving","neglect"]),
("MIDDLE","GRAILS",14,"King of Grails","king-of-cups-em",["emotional maturity","diplomacy","wisdom"],["manipulation","moodiness","coldness"]),
("MINOR","BLADES",1,"Ace of Blades","ace-of-swords-em",["raw power","breakthrough","clarity"],["confusion","brutality","mental chaos"]),
("MINOR","BLADES",2,"Two of Blades","two-of-swords-em",["difficult choices","stalemate","truce"],["indecision","lies","confusion"]),
("MINOR","BLADES",3,"Three of Blades","three-of-swords-em",["heartbreak","sorrow","grief"],["recovery","forgiveness","moving on"]),
("MINOR","BLADES",4,"Four of Blades","four-of-swords-em",["rest","restoration","retreat"],["restlessness","burnout","illness"]),
("MINOR","BLADES",5,"Five of Blades","five-of-swords-em",["defeat","change","transition"],["resistance to change","inability to move"]),
("MINOR","BLADES",6,"Six of Blades","six-of-swords-em",["victory","success","ambition"],["an eye for an eye","dishonour","manipulation"]),
("MINOR","BLADES",7,"Seven of Blades","seven-of-swords-em",["deception","trickery","tactics"],["imposter syndrome","coming clean","rethinking"]),
("MINOR","BLADES",8,"Eight of Blades","eight-of-swords-em",["restriction","isolation","imprisonment"],["self-limiting beliefs","inner critic","opening up"]),
("MINOR","BLADES",9,"Nine of Blades","nine-of-swords-em",["anxiety","worry","fear"],["recovery from anxiety","inner turmoil","secrets"]),
("MINOR","BLADES",10,"Ten of Blades","ten-of-swords-em",["ruin","painful endings","loss"],["recovery","regeneration","resisting an end"]),
("MIDDLE","BLADES",11,"Maid of Blades","maid-of-swords-em",["new ideas","mental agility","curiosity"],["manipulation","all talk no action","ruthlessness"]),
("MIDDLE","BLADES",12,"Jack of Blades","jack-of-swords-em",["action","impulsiveness","ambition"],["no direction","disregard for consequences"]),
("MIDDLE","BLADES",13,"Queen of Blades","queen-of-swords-em",["clarity","directness","structure"],["coldness","cruelty","manipulation"]),
("MIDDLE","BLADES",14,"King of Blades","king-of-swords-em",["mental clarity","truth","authority"],["abuse of power","manipulation","coldness"]),
("MINOR","CROWNS",1,"Ace of Crowns","ace-of-crowns-em",["opportunity","new venture","manifestation"],["lost opportunity","lack of planning","scarcity"]),
("MINOR","CROWNS",2,"Two of Crowns","two-of-crowns-em",["juggling resources","flexibility","fun"],["imbalance","disorganisation","overwhelm"]),
("MINOR","CROWNS",3,"Three of Crowns","three-of-crowns-em",["teamwork","building","apprenticeship"],["lack of teamwork","disharmony","misalignment"]),
("MINOR","CROWNS",4,"Four of Crowns","four-of-crowns-em",["stability","security","conservation"],["greed","stinginess","possessiveness"]),
("MINOR","CROWNS",5,"Five of Crowns","five-of-crowns-em",["isolation","insecurity","worry"],["recovery from loss","overcoming hardship"]),
("MINOR","CROWNS",6,"Six of Crowns","six-of-crowns-em",["generosity","charity","community"],["strings attached","power dynamics","inequality"]),
("MINOR","CROWNS",7,"Seven of Crowns","seven-of-crowns-em",["hard work","perseverance","diligence"],["lack of reward","laziness","low quality"]),
("MINOR","CROWNS",8,"Eight of Crowns","eight-of-crowns-em",["apprenticeship","education","skill"],["perfectionism","misdirected activity","misuse"]),
("MINOR","CROWNS",9,"Nine of Crowns","nine-of-crowns-em",["abundance","luxury","self-sufficiency"],["overindulgence","superficiality","materialism"]),
("MINOR","CROWNS",10,"Ten of Crowns","ten-of-crowns-em",["wealth","financial security","achievement"],["financial failure","greed","lost success"]),
("MIDDLE","CROWNS",11,"Maid of Crowns","maid-of-crowns-em",["ambition","diligence","management"],["underhandedness","greediness","unethical"]),
("MIDDLE","CROWNS",12,"Jack of Crowns","jack-of-crowns-em",["hard work","productivity","routine"],["laziness","obsession with work","burnout"]),
("MIDDLE","CROWNS",13,"Queen of Crowns","queen-of-crowns-em",["nurturing","practical","abundance"],["financial dependence","smothering","insecurity"]),
("MIDDLE","CROWNS",14,"King of Crowns","king-of-crowns-em",["abundance","prosperity","security"],["greed","indulgence","sensual obsession"]),
]
def seed(apps, schema_editor):
DeckVariant = apps.get_model('epic', 'DeckVariant')
TarotCard = apps.get_model('epic', 'TarotCard')
deck = DeckVariant.objects.create(
name='Earthman',
slug='earthman',
card_count=106,
is_default=True,
)
for (number, name, slug, group, correspondence, icon,
reversal, levity_qualifier, gravity_qualifier,
levity_emanation, gravity_emanation, levity_reversal, gravity_reversal,
mechanisms, articulations, kw_up, kw_rev, cautions) in EM_MAJOR:
TarotCard.objects.create(
deck_variant=deck,
arcana='MAJOR',
suit=None,
number=number,
name=name,
slug=slug,
group=group,
correspondence=correspondence,
icon=icon,
reversal=reversal,
levity_qualifier=levity_qualifier,
gravity_qualifier=gravity_qualifier,
levity_emanation=levity_emanation,
gravity_emanation=gravity_emanation,
levity_reversal=levity_reversal,
gravity_reversal=gravity_reversal,
mechanisms=mechanisms,
articulations=articulations,
keywords_upright=kw_up,
keywords_reversed=kw_rev,
cautions=cautions,
)
for arcana, suit, number, name, slug, kw_up, kw_rev in EM_MINOR:
TarotCard.objects.create(
deck_variant=deck,
arcana=arcana,
suit=suit,
number=number,
name=name,
slug=slug,
keywords_upright=kw_up,
keywords_reversed=kw_rev,
)
class Migration(migrations.Migration):
dependencies = [
('epic', '0003_seed_fiorentine_deck'),
]
operations = [
migrations.RunPython(seed, migrations.RunPython.noop),
]

View File

@@ -1,21 +0,0 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0004_alter_room_gate_status'),
]
operations = [
migrations.AddField(
model_name='gateslot',
name='debited_token_type',
field=models.CharField(max_length=8, null=True, blank=True),
),
migrations.AddField(
model_name='gateslot',
name='debited_token_expires_at',
field=models.DateTimeField(null=True, blank=True),
),
]

View File

@@ -0,0 +1,92 @@
"""Seed Sign, Planet, AspectType, and HouseLabel reference tables."""
from django.db import migrations
# (order, name, symbol, element, modality, start_degree)
SIGNS = [
(0, 'Aries', '', 'Fire', 'Cardinal', 0.0),
(1, 'Taurus', '', 'Earth', 'Fixed', 30.0),
(2, 'Gemini', '', 'Air', 'Mutable', 60.0),
(3, 'Cancer', '', 'Water', 'Cardinal', 90.0),
(4, 'Leo', '', 'Fire', 'Fixed', 120.0),
(5, 'Virgo', '', 'Earth', 'Mutable', 150.0),
(6, 'Libra', '', 'Air', 'Cardinal', 180.0),
(7, 'Scorpio', '', 'Water', 'Fixed', 210.0),
(8, 'Sagittarius', '', 'Fire', 'Mutable', 240.0),
(9, 'Capricorn', '', 'Earth', 'Cardinal', 270.0),
(10, 'Aquarius', '', 'Air', 'Fixed', 300.0),
(11, 'Pisces', '', 'Water', 'Mutable', 330.0),
]
# (order, name, symbol)
PLANETS = [
(0, 'Sun', ''),
(1, 'Moon', ''),
(2, 'Mercury', ''),
(3, 'Venus', ''),
(4, 'Mars', ''),
(5, 'Jupiter', ''),
(6, 'Saturn', ''),
(7, 'Uranus', ''),
(8, 'Neptune', ''),
(9, 'Pluto', ''),
]
# (name, symbol, angle, orb) — mirrors ASPECTS in pyswiss/apps/charts/calc.py
ASPECT_TYPES = [
('Conjunction', '', 0, 8.0),
('Semisextile', '', 30, 4.0),
('Semisquare', '', 45, 4.0),
('Sextile', '', 60, 6.0),
('Square', '', 90, 8.0),
('Trine', '', 120, 8.0),
('Sesquiquadrate', '', 135, 4.0),
('Quincunx', '', 150, 5.0),
('Opposition', '', 180, 10.0),
]
# (number, name, keywords)
HOUSE_LABELS = [
(1, 'Self', 'identity, appearance, first impressions'),
(2, 'Worth', 'possessions, values, finances'),
(3, 'Education', 'communication, siblings, short journeys'),
(4, 'Family', 'home, roots, ancestry'),
(5, 'Creation', 'creativity, romance, children, pleasure'),
(6, 'Ritual', 'service, health, daily routines'),
(7, 'Cooperation', 'partnerships, marriage, open enemies'),
(8, 'Regeneration', 'transformation, shared resources, death'),
(9, 'Enterprise', 'philosophy, travel, higher learning'),
(10, 'Career', 'public life, reputation, authority'),
(11, 'Reward', 'friends, groups, aspirations'),
(12, 'Reprisal', 'hidden matters, karma, self-undoing'),
]
def seed(apps, schema_editor):
Sign = apps.get_model('epic', 'Sign')
Planet = apps.get_model('epic', 'Planet')
AspectType = apps.get_model('epic', 'AspectType')
HouseLabel = apps.get_model('epic', 'HouseLabel')
for order, name, symbol, element, modality, start_degree in SIGNS:
Sign.objects.create(order=order, name=name, symbol=symbol,
element=element, modality=modality, start_degree=start_degree)
for order, name, symbol in PLANETS:
Planet.objects.create(order=order, name=name, symbol=symbol)
for name, symbol, angle, orb in ASPECT_TYPES:
AspectType.objects.create(name=name, symbol=symbol, angle=angle, orb=orb)
for number, name, keywords in HOUSE_LABELS:
HouseLabel.objects.create(number=number, name=name, keywords=keywords)
class Migration(migrations.Migration):
dependencies = [
('epic', '0004_seed_earthman_deck'),
]
operations = [
migrations.RunPython(seed, migrations.RunPython.noop),
]

View File

@@ -1,33 +0,0 @@
# Generated by Django 6.0 on 2026-03-17 00:14
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0005_gateslot_debited_token_fields'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='room',
name='table_status',
field=models.CharField(blank=True, choices=[('ROLE_SELECT', 'Role Select'), ('SIG_SELECT', 'Significator Select'), ('IN_GAME', 'In Game')], max_length=20, null=True),
),
migrations.CreateModel(
name='TableSeat',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slot_number', models.IntegerField()),
('role', models.CharField(blank=True, choices=[('PC', 'Player'), ('BC', 'Builder'), ('SC', 'Shepherd'), ('AC', 'Alchemist'), ('NC', 'Narrator'), ('EC', 'Economist')], max_length=2, null=True)),
('role_revealed', models.BooleanField(default=False)),
('seat_position', models.IntegerField(blank=True, null=True)),
('gamer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='table_seats', to=settings.AUTH_USER_MODEL)),
('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='table_seats', to='epic.room')),
],
),
]

View File

@@ -1,39 +0,0 @@
# Generated by Django 6.0 on 2026-03-24 23:33
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0006_table_status_and_table_seat'),
]
operations = [
migrations.CreateModel(
name='TarotCard',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
('arcana', models.CharField(choices=[('MAJOR', 'Major Arcana'), ('MINOR', 'Minor Arcana')], max_length=5)),
('suit', models.CharField(blank=True, choices=[('WANDS', 'Wands'), ('CUPS', 'Cups'), ('SWORDS', 'Swords'), ('PENTACLES', 'Pentacles')], max_length=10, null=True)),
('number', models.IntegerField()),
('slug', models.SlugField(unique=True)),
('keywords_upright', models.JSONField(default=list)),
('keywords_reversed', models.JSONField(default=list)),
],
options={
'ordering': ['arcana', 'suit', 'number'],
},
),
migrations.CreateModel(
name='TarotDeck',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('drawn_card_ids', models.JSONField(default=list)),
('created_at', models.DateTimeField(auto_now_add=True)),
('room', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='tarot_deck', to='epic.room')),
],
),
]

View File

@@ -1,164 +0,0 @@
from django.db import migrations
MAJOR_ARCANA = [
(0, "The Fool", "the-fool", ["beginnings", "spontaneity", "freedom"], ["recklessness", "naivety", "risk"]),
(1, "The Magician", "the-magician", ["willpower", "skill", "resourcefulness"], ["manipulation", "untapped potential", "deceit"]),
(2, "The High Priestess", "the-high-priestess", ["intuition", "mystery", "inner knowledge"], ["secrets", "disconnection", "withdrawal"]),
(3, "The Empress", "the-empress", ["fertility", "abundance", "nurturing"], ["dependence", "smothering", "creative block"]),
(4, "The Emperor", "the-emperor", ["authority", "structure", "stability"], ["rigidity", "domination", "inflexibility"]),
(5, "The Hierophant", "the-hierophant", ["tradition", "conformity", "institutions"], ["rebellion", "unconventionality", "challenge"]),
(6, "The Lovers", "the-lovers", ["love", "harmony", "choice"], ["disharmony", "imbalance", "misalignment"]),
(7, "The Chariot", "the-chariot", ["control", "willpower", "victory"], ["aggression", "lack of direction", "defeat"]),
(8, "Strength", "strength", ["courage", "patience", "compassion"], ["self-doubt", "weakness", "insecurity"]),
(9, "The Hermit", "the-hermit", ["introspection", "guidance", "solitude"], ["isolation", "loneliness", "withdrawal"]),
(10, "Wheel of Fortune", "wheel-of-fortune", ["change", "cycles", "fate"], ["bad luck", "resistance", "clinging to control"]),
(11, "Justice", "justice", ["fairness", "truth", "cause and effect"], ["injustice", "dishonesty", "avoidance"]),
(12, "The Hanged Man", "the-hanged-man", ["pause", "surrender", "new perspective"], ["stalling", "resistance", "indecision"]),
(13, "Death", "death", ["endings", "transition", "transformation"], ["fear of change", "stagnation", "resistance"]),
(14, "Temperance", "temperance", ["balance", "patience", "moderation"], ["imbalance", "excess", "lack of harmony"]),
(15, "The Devil", "the-devil", ["bondage", "materialism", "shadow self"], ["detachment", "freedom", "releasing control"]),
(16, "The Tower", "the-tower", ["sudden change", "upheaval", "revelation"], ["avoidance", "fear of change", "delaying disaster"]),
(17, "The Star", "the-star", ["hope", "renewal", "inspiration"], ["despair", "insecurity", "hopelessness"]),
(18, "The Moon", "the-moon", ["illusion", "fear", "the unconscious"], ["confusion", "misinterpretation", "clarity"]),
(19, "The Sun", "the-sun", ["positivity", "success", "vitality"], ["negativity", "depression", "sadness"]),
(20, "Judgement", "judgement", ["reflection", "reckoning", "absolution"], ["self-doubt", "lack of self-awareness", "loathing"]),
(21, "The World", "the-world", ["completion", "integration", "accomplishment"], ["incompletion", "no closure", "shortcuts"]),
]
MINOR_SUITS = [
("WANDS", "wands"),
("CUPS", "cups"),
("SWORDS", "swords"),
("PENTACLES", "pentacles"),
]
MINOR_NAMES = [
(1, "Ace", "ace"),
(2, "Two", "two"),
(3, "Three", "three"),
(4, "Four", "four"),
(5, "Five", "five"),
(6, "Six", "six"),
(7, "Seven", "seven"),
(8, "Eight", "eight"),
(9, "Nine", "nine"),
(10, "Ten", "ten"),
(11, "Page", "page"),
(12, "Knight", "knight"),
(13, "Queen", "queen"),
(14, "King", "king"),
]
# Keywords: [suit][number-1] → (upright_list, reversed_list)
MINOR_KEYWORDS = {
"WANDS": [
(["inspiration", "new venture", "spark"], ["delays", "lack of motivation", "false start"]),
(["planning", "progress", "decisions"], ["impatience", "lack of planning", "hesitation"]),
(["expansion", "foresight", "enterprise"], ["obstacles", "lack of foresight", "delays"]),
(["celebration", "harmony", "homecoming"], ["lack of support", "transience", "home conflicts"]),
(["conflict", "competition", "tension"], ["avoiding conflict", "compromise", "truce"]),
(["victory", "recognition", "progress"], ["excess pride", "lack of recognition", "fall"]),
(["challenge", "courage", "competition"], ["anxiety", "giving up", "overwhelmed"]),
(["rapid action", "adventure", "change"], ["haste", "scattered energy", "delays"]),
(["resilience", "persistence", "last stand"], ["exhaustion", "giving up", "surrender"]),
(["completion", "celebration", "travel"], ["burdens", "oppression", "carrying too much"]),
(["exploration", "enthusiasm", "adventure"], ["hasty decisions", "scattered energy", "immaturity"]),
(["energy", "passion", "adventure"], ["scattered energy", "frustration", "aggression"]),
(["confidence", "independence", "courage"], ["selfishness", "jealousy", "insecurity"]),
(["big picture", "leadership", "vision"], ["impulsiveness", "haste", "overconfidence"]),
],
"CUPS": [
(["new feelings", "intuition", "opportunity"], ["blocked creativity", "emptiness", "hesitation"]),
(["partnership", "unity", "celebration"], ["imbalance", "broken bonds", "misalignment"]),
(["creativity", "community", "abundance"], ["independence", "isolation", "looking inward"]),
(["contemplation", "apathy", "reevaluation"], ["withdrawal", "boredom", "seeking motivation"]),
(["loss", "grief", "disappointment"], ["acceptance", "moving on", "forgiveness"]),
(["nostalgia", "reunion", "joy"], ["living in the past", "naivety", "unrealistic"]),
(["illusion", "fantasy", "wishful thinking"], ["alignment", "clarity", "sobriety"]),
(["disappointment", "abandonment", "walking away"], ["hopelessness", "aimlessness", "stagnation"]),
(["contentment", "fulfilment", "satisfaction"], ["inner happiness", "materialism", "indulgence"]),
(["divine love", "bliss", "fulfilment"], ["inner happiness", "alignment", "personal values"]),
(["sensitivity", "creativity", "intuition"], ["insecurity", "emotional immaturity", "creative blocks"]),
(["compassion", "romanticism", "diplomacy"], ["moodiness", "emotional manipulation", "deception"]),
(["compassion", "empathy", "nurturing"], ["emotional insecurity", "over-giving", "neglect"]),
(["emotional maturity", "diplomacy", "wisdom"], ["manipulation", "moodiness", "coldness"]),
],
"SWORDS": [
(["raw power", "breakthrough", "clarity"], ["confusion", "brutality", "mental chaos"]),
(["difficult choices", "stalemate", "truce"], ["indecision", "lies", "confusion"]),
(["heartbreak", "sorrow", "grief"], ["recovery", "forgiveness", "moving on"]),
(["rest", "restoration", "retreat"], ["restlessness", "burnout", "illness"]),
(["defeat", "change", "transition"], ["resistance to change", "inability to move"]),
(["victory", "success", "ambition"], ["an eye for an eye", "dishonour", "manipulation"]),
(["deception", "trickery", "tactics"], ["imposter syndrome", "coming clean", "rethinking"]),
(["restriction", "isolation", "imprisonment"], ["self-limiting beliefs", "inner critic", "opening up"]),
(["anxiety", "worry", "fear"], ["recovery from anxiety", "inner turmoil", "secrets"]),
(["ruin", "painful endings", "loss"], ["recovery", "regeneration", "resisting an end"]),
(["new ideas", "mental agility", "curiosity"], ["manipulation", "all talk no action", "ruthlessness"]),
(["action", "impulsiveness", "ambition"], ["no direction", "disregard for consequences"]),
(["clarity", "directness", "structure"], ["coldness", "cruelty", "manipulation"]),
(["mental clarity", "truth", "authority"], ["abuse of power", "manipulation", "coldness"]),
],
"PENTACLES": [
(["opportunity", "new venture", "manifestation"], ["lost opportunity", "lack of planning", "scarcity"]),
(["juggling resources", "flexibility", "fun"], ["imbalance", "disorganisation", "overwhelm"]),
(["teamwork", "building", "apprenticeship"], ["lack of teamwork", "disharmony", "misalignment"]),
(["stability", "security", "conservation"], ["greed", "stinginess", "possessiveness"]),
(["isolation", "insecurity", "worry"], ["recovery from loss", "overcoming hardship"]),
(["generosity", "charity", "community"], ["strings attached", "power dynamics", "inequality"]),
(["hard work", "perseverance", "diligence"], ["lack of reward", "laziness", "low quality"]),
(["apprenticeship", "education", "skill"], ["perfectionism", "misdirected activity", "misuse"]),
(["abundance", "luxury", "self-sufficiency"], ["overindulgence", "superficiality", "materialism"]),
(["wealth", "financial security", "achievement"], ["financial failure", "greed", "lost success"]),
(["ambition", "diligence", "management"], ["underhandedness", "greediness", "unethical"]),
(["hard work", "productivity", "routine"], ["laziness", "obsession with work", "burnout"]),
(["nurturing", "practical", "abundance"], ["financial dependence", "smothering", "insecurity"]),
(["abundance", "prosperity", "security"], ["greed", "indulgence", "sensual obsession"]),
],
}
def seed_tarot_cards(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
# Major Arcana
for number, name, slug, upright, reversed_ in MAJOR_ARCANA:
TarotCard.objects.create(
name=name,
arcana="MAJOR",
suit=None,
number=number,
slug=slug,
keywords_upright=upright,
keywords_reversed=reversed_,
)
# Minor Arcana
for suit_code, suit_slug in MINOR_SUITS:
for number, rank_name, rank_slug in MINOR_NAMES:
upright, reversed_ = MINOR_KEYWORDS[suit_code][number - 1]
TarotCard.objects.create(
name=f"{rank_name} of {suit_code.capitalize()}",
arcana="MINOR",
suit=suit_code,
number=number,
slug=f"{rank_slug}-of-{suit_slug}",
keywords_upright=upright,
keywords_reversed=reversed_,
)
def unseed_tarot_cards(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
TarotCard.objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
("epic", "0007_tarotcard_tarotdeck"),
]
operations = [
migrations.RunPython(seed_tarot_cards, reverse_code=unseed_tarot_cards),
]

View File

@@ -1,68 +0,0 @@
# Generated by Django 6.0 on 2026-03-25 00:48
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0008_seed_tarot_cards'),
]
operations = [
migrations.CreateModel(
name='DeckVariant',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(unique=True)),
('card_count', models.IntegerField()),
('description', models.TextField(blank=True)),
('is_default', models.BooleanField(default=False)),
],
),
migrations.AlterModelOptions(
name='tarotcard',
options={'ordering': ['deck_variant', 'arcana', 'suit', 'number']},
),
migrations.AddField(
model_name='tarotcard',
name='correspondence',
field=models.CharField(blank=True, max_length=200),
),
migrations.AddField(
model_name='tarotcard',
name='group',
field=models.CharField(blank=True, max_length=100),
),
migrations.AlterField(
model_name='tarotcard',
name='name',
field=models.CharField(max_length=200),
),
migrations.AlterField(
model_name='tarotcard',
name='slug',
field=models.SlugField(max_length=120),
),
migrations.AlterField(
model_name='tarotcard',
name='suit',
field=models.CharField(blank=True, choices=[('WANDS', 'Wands'), ('CUPS', 'Cups'), ('SWORDS', 'Swords'), ('PENTACLES', 'Pentacles'), ('COINS', 'Coins')], max_length=10, null=True),
),
migrations.AddField(
model_name='tarotcard',
name='deck_variant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='cards', to='epic.deckvariant'),
),
migrations.AddField(
model_name='tarotdeck',
name='deck_variant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='active_decks', to='epic.deckvariant'),
),
migrations.AlterUniqueTogether(
name='tarotcard',
unique_together={('deck_variant', 'slug')},
),
]

View File

@@ -1,202 +0,0 @@
"""
Data migration:
1. Create DeckVariant records (Fiorentine Minchiate + Earthman).
2. Backfill the 78 existing TarotCards → Fiorentine Minchiate.
3. Seed all 108 Earthman cards (52 major + 56 minor).
"""
from django.db import migrations
# ── Earthman Major Arcana (52 cards, numbers 051) ──────────────────────────
# (name, slug, group, correspondence)
EARTHMAN_MAJOR = [
# ── The Schiz ──────────────────────────────────────────────────────────
(0, "The Schiz", "the-schiz", "", "The Fool / Il Matto"),
# ── The Popes ──────────────────────────────────────────────────────────
(1, "Pope I: President", "pope-i-president", "The Popes", "The Magician / Il Bagatto"),
(2, "Pope II: Tsar", "pope-ii-tsar", "The Popes", "The Popess / La Papessa"),
(3, "Pope III: Chairman", "pope-iii-chairman", "The Popes", "The Empress / L'Imperatrice"),
(4, "Pope IV: Emperor", "pope-iv-emperor", "The Popes", "The Emperor / L'Imperatore"),
(5, "Pope V: Chancellor", "pope-v-chancellor", "The Popes", "The Pope / Il Papa"),
# ── The Virtues, Implicit (cardinal / acquired) ────────────────────────
(6, "Virtue VI: Controlled Folly", "virtue-vi-controlled-folly", "The Virtues, Implicit", "Fortitude / La Fortezza"),
(7, "Virtue VII: Not-Doing", "virtue-vii-not-doing", "The Virtues, Implicit", "Justice / La Giustizia"),
(8, "Virtue VIII: Losing Self-Importance","virtue-viii-losing-self-importance","The Virtues, Implicit", "Temperance / La Temperanza"),
(9, "Virtue IX: Erasing Personal History","virtue-ix-erasing-personal-history","The Virtues, Implicit", "Prudence / La Prudenza"),
# ── Wheel ──────────────────────────────────────────────────────────────
(10, "Wheel of Fortune", "wheel-of-fortune-em", "", "La Ruota della Fortuna"),
# ── Solo cards ─────────────────────────────────────────────────────────
(11, "The Junkboat", "the-junkboat", "", "The Chariot / Il Carro"),
(12, "The Junkman", "the-junkman", "", "The Hanged Man / L'Appeso"),
(13, "Death", "death-em", "", "La Morte"),
(14, "The Traitor", "the-traitor", "", "The Devil / Il Diavolo"),
(15, "Disco Inferno", "disco-inferno", "", "The Tower / La Torre"),
(16, "Torre Terrestre", "torre-terrestre", "", "Purgatorio"),
(17, "Fantasia Celestia", "fantasia-celestia", "", "Paradiso"),
# ── The Virtues, Explicit (theological / infused) ─────────────────────
(18, "Virtue XVIII: Stalking", "virtue-xviii-stalking", "The Virtues, Explicit", "Love / Charity / La Carità"),
(19, "Virtue XIX: Intent", "virtue-xix-intent", "The Virtues, Explicit", "Hope / La Speranza"),
(20, "Virtue XX: Dreaming", "virtue-xx-dreaming", "The Virtues, Explicit", "Faith / La Fede"),
# ── The Elements, Classical ────────────────────────────────────────────
(21, "Element XXI: Fire", "element-xxi-fire", "The Elements, Classical", "Ardor [Ar]"),
(22, "Element XXII: Earth", "element-xxii-earth", "The Elements, Classical", "Ossum [Om]"),
(23, "Element XXIII: Air", "element-xxiii-air", "The Elements, Classical", "Pneuma [Pn]"),
(24, "Element XXIV: Water", "element-xxiv-water", "The Elements, Classical", "Humor [Hm]"),
# ── The Zodiac ─────────────────────────────────────────────────────────
(25, "Zodiac XXV: Aries", "zodiac-xxv-aries", "The Zodiac", "The Ram"),
(26, "Zodiac XXVI: Taurus", "zodiac-xxvi-taurus", "The Zodiac", "The Bull"),
(27, "Zodiac XXVII: Gemini", "zodiac-xxvii-gemini", "The Zodiac", "The Twins"),
(28, "Zodiac XXVIII: Cancer", "zodiac-xxviii-cancer", "The Zodiac", "The Crab"),
(29, "Zodiac XXIX: Leo", "zodiac-xxix-leo", "The Zodiac", "The Lion"),
(30, "Zodiac XXX: Virgo", "zodiac-xxx-virgo", "The Zodiac", "The Maiden"),
(31, "Zodiac XXXI: Libra", "zodiac-xxxi-libra", "The Zodiac", "The Scales"),
(32, "Zodiac XXXII: Scorpio", "zodiac-xxxii-scorpio", "The Zodiac", "The Scorpion"),
(33, "Zodiac XXXIII: Sagittarius", "zodiac-xxxiii-sagittarius", "The Zodiac", "The Archer"),
(34, "Zodiac XXXIV: Capricorn", "zodiac-xxxiv-capricorn", "The Zodiac", "The Sea-Goat"),
(35, "Zodiac XXXV: Aquarius", "zodiac-xxxv-aquarius", "The Zodiac", "The Water-Bearer"),
(36, "Zodiac XXXVI: Pisces", "zodiac-xxxvi-pisces", "The Zodiac", "The Fish"),
# ── The Elements, Absolute ─────────────────────────────────────────────
(37, "Element XXXVII: Time", "element-xxxvii-time", "The Elements, Absolute", "Tempo [Tp]"),
(38, "Element XXXVIII: Space", "element-xxxviii-space", "The Elements, Absolute", "Nexus [Nx]"),
# ── The Wanderers ──────────────────────────────────────────────────────
(39, "Wanderer XXXIX: The Polestar", "wanderer-xxxix-polestar", "The Wanderers", "The Star / Le Stelle"),
(40, "Wanderer XL: The Antichthon", "wanderer-xl-antichthon", "The Wanderers", "The Moon / La Luna"),
(41, "Wanderer XLI: The Corestar", "wanderer-xli-corestar", "The Wanderers", "The Sun / Il Sole"),
(42, "Wanderer XLII: Mercury", "wanderer-xlii-mercury", "The Wanderers", "Mercurio"),
(43, "Wanderer XLIII: Venus", "wanderer-xliii-venus", "The Wanderers", "Venere"),
(44, "Wanderer XLIV: Mars", "wanderer-xliv-mars", "The Wanderers", "Marte"),
(45, "Wanderer XLV: Jupiter", "wanderer-xlv-jupiter", "The Wanderers", "Giove"),
(46, "Wanderer XLVI: Saturn", "wanderer-xlvi-saturn", "The Wanderers", "Saturno"),
(47, "Wanderer XLVII: Uranus", "wanderer-xlvii-uranus", "The Wanderers", "Urano"),
(48, "Wanderer XLVIII: Neptune", "wanderer-xlviii-neptune", "The Wanderers", "Nettuno"),
(49, "Wanderer XLIX: The King & Queen of Hades", "wanderer-xlix-king-queen-hades", "The Wanderers", "The Binary / Plutone-Proserpina"),
# ── Finale ─────────────────────────────────────────────────────────────
(50, "The Eagle", "the-eagle", "", "Judgement / L'Angelo"),
(51, "Divine Calculus", "divine-calculus", "", "The World / Il Mondo"),
]
# ── Earthman Minor Arcana ────────────────────────────────────────────────────
# 4 suits × 14 cards. Suits: WANDS / CUPS / SWORDS / COINS
# Court cards: Jack (11) / Cavalier (12) / Queen (13) / King (14)
EARTHMAN_SUITS = [
("WANDS", "wands", "Ardor [Ar] — Fire"),
("CUPS", "cups", "Humor [Hm] — Water"),
("SWORDS","swords","Pneuma [Pn] — Air"),
("COINS", "coins", "Ossum [Om] — Stone"),
]
EARTHMAN_RANKS = [
(1, "Ace", "ace"),
(2, "2", "two"),
(3, "3", "three"),
(4, "4", "four"),
(5, "5", "five"),
(6, "6", "six"),
(7, "7", "seven"),
(8, "8", "eight"),
(9, "9", "nine"),
(10, "10", "ten"),
(11, "Jack", "jack"),
(12, "Cavalier", "cavalier"),
(13, "Queen", "queen"),
(14, "King", "king"),
]
def forward(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
# ── 1. Create DeckVariant records ────────────────────────────────────
fiorentine = DeckVariant.objects.create(
name="Fiorentine Minchiate",
slug="fiorentine-minchiate",
card_count=78,
description="Standard 78-card Minchiate deck. Alt / lite play mode.",
is_default=False,
)
earthman = DeckVariant.objects.create(
name="Earthman Deck",
slug="earthman",
card_count=108,
description=(
"Primary 108-card Earthman deck. "
"52 Major Arcana (The Schiz through Divine Calculus) "
"+ 56 Minor Arcana across Wands, Cups, Swords, Coins."
),
is_default=True,
)
# ── 2. Backfill existing 78 Fiorentine cards ─────────────────────────
TarotCard.objects.filter(deck_variant__isnull=True).update(
deck_variant=fiorentine
)
# ── 3. Seed Earthman Major Arcana ────────────────────────────────────
for number, name, slug, group, correspondence in EARTHMAN_MAJOR:
TarotCard.objects.create(
deck_variant=earthman,
name=name,
arcana="MAJOR",
suit=None,
number=number,
slug=slug,
group=group,
correspondence=correspondence,
keywords_upright=[],
keywords_reversed=[],
)
# ── 4. Seed Earthman Minor Arcana ────────────────────────────────────
for suit_code, suit_slug, _element in EARTHMAN_SUITS:
for number, rank_name, rank_slug in EARTHMAN_RANKS:
name = f"{rank_name} of {suit_code.capitalize()}"
slug = f"{rank_slug}-of-{suit_slug}-em"
TarotCard.objects.create(
deck_variant=earthman,
name=name,
arcana="MINOR",
suit=suit_code,
number=number,
slug=slug,
group="",
correspondence="",
keywords_upright=[],
keywords_reversed=[],
)
def reverse(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
# Remove Earthman cards and clear FK from Fiorentine cards
earthman = DeckVariant.objects.filter(slug="earthman").first()
if earthman:
TarotCard.objects.filter(deck_variant=earthman).delete()
fiorentine = DeckVariant.objects.filter(slug="fiorentine-minchiate").first()
if fiorentine:
TarotCard.objects.filter(deck_variant=fiorentine).update(deck_variant=None)
DeckVariant.objects.filter(slug__in=["earthman", "fiorentine-minchiate"]).delete()
class Migration(migrations.Migration):
dependencies = [
("epic", "0009_deckvariant_alter_tarotcard_options_and_more"),
]
operations = [
migrations.RunPython(forward, reverse_code=reverse),
]

View File

@@ -1,82 +0,0 @@
"""
Data migration: rename Earthman court cards at positions 11 and 12.
Old naming (from 0010): Jack (11) / Cavalier (12)
New naming: Maid (11) / Jack (12)
Must rename 11 → Maid first so the "jack-of-*-em" slugs are free
before the 12s claim them.
"""
from django.db import migrations
SUITS = ["Wands", "Cups", "Swords", "Coins"]
def rename_court_cards(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
earthman = DeckVariant.objects.filter(slug="earthman").first()
if not earthman:
return
# Step 1: Jack (11) → Maid — frees up jack-of-*-em slugs
for suit in SUITS:
suit_slug = suit.lower()
TarotCard.objects.filter(
deck_variant=earthman, number=11, slug=f"jack-of-{suit_slug}-em"
).update(
name=f"Maid of {suit}",
slug=f"maid-of-{suit_slug}-em",
)
# Step 2: Cavalier (12) → Jack — takes the now-free jack-of-*-em slugs
for suit in SUITS:
suit_slug = suit.lower()
TarotCard.objects.filter(
deck_variant=earthman, number=12, slug=f"cavalier-of-{suit_slug}-em"
).update(
name=f"Jack of {suit}",
slug=f"jack-of-{suit_slug}-em",
)
def reverse_court_cards(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
earthman = DeckVariant.objects.filter(slug="earthman").first()
if not earthman:
return
# Step 1: Jack (12) → Cavalier — frees up jack-of-*-em slugs
for suit in SUITS:
suit_slug = suit.lower()
TarotCard.objects.filter(
deck_variant=earthman, number=12, slug=f"jack-of-{suit_slug}-em"
).update(
name=f"Cavalier of {suit}",
slug=f"cavalier-of-{suit_slug}-em",
)
# Step 2: Maid (11) → Jack
for suit in SUITS:
suit_slug = suit.lower()
TarotCard.objects.filter(
deck_variant=earthman, number=11, slug=f"maid-of-{suit_slug}-em"
).update(
name=f"Jack of {suit}",
slug=f"jack-of-{suit_slug}-em",
)
class Migration(migrations.Migration):
dependencies = [
("epic", "0010_seed_deck_variants_and_earthman"),
]
operations = [
migrations.RunPython(rename_court_cards, reverse_code=reverse_court_cards),
]

View File

@@ -1,162 +0,0 @@
"""
Data migration:
1. Rename grouped Earthman major arcana to use group-relative ordinals
(e.g. "Virtue VI: Controlled Folly""Implicit Virtue 1: Controlled Folly").
2. Spell out Earthman minor arcana pip names 210
(e.g. "2 of Wands""Two of Wands").
Corner ranks (Roman numerals of absolute card number) are a property on the model
and are unchanged — this only affects the stored name / slug fields.
"""
from django.db import migrations
# ── Major arcana: (new_name, new_slug) keyed by card number ─────────────────
MAJOR_RENAMES = {
# Implicit Virtues (cards 69)
6: ("Implicit Virtue 1: Controlled Folly", "implicit-virtue-1-controlled-folly"),
7: ("Implicit Virtue 2: Not-Doing", "implicit-virtue-2-not-doing"),
8: ("Implicit Virtue 3: Losing Self-Importance", "implicit-virtue-3-losing-self-importance"),
9: ("Implicit Virtue 4: Erasing Personal History", "implicit-virtue-4-erasing-personal-history"),
# Explicit Virtues (cards 1820)
18: ("Explicit Virtue 1: Stalking", "explicit-virtue-1-stalking"),
19: ("Explicit Virtue 2: Intent", "explicit-virtue-2-intent"),
20: ("Explicit Virtue 3: Dreaming", "explicit-virtue-3-dreaming"),
# Classical Elements (cards 2124)
21: ("Classical Element 1: Fire", "classical-element-1-fire"),
22: ("Classical Element 2: Earth", "classical-element-2-earth"),
23: ("Classical Element 3: Air", "classical-element-3-air"),
24: ("Classical Element 4: Water", "classical-element-4-water"),
# Zodiac (cards 2536)
25: ("Zodiac 1: Aries", "zodiac-1-aries"),
26: ("Zodiac 2: Taurus", "zodiac-2-taurus"),
27: ("Zodiac 3: Gemini", "zodiac-3-gemini"),
28: ("Zodiac 4: Cancer", "zodiac-4-cancer"),
29: ("Zodiac 5: Leo", "zodiac-5-leo"),
30: ("Zodiac 6: Virgo", "zodiac-6-virgo"),
31: ("Zodiac 7: Libra", "zodiac-7-libra"),
32: ("Zodiac 8: Scorpio", "zodiac-8-scorpio"),
33: ("Zodiac 9: Sagittarius", "zodiac-9-sagittarius"),
34: ("Zodiac 10: Capricorn", "zodiac-10-capricorn"),
35: ("Zodiac 11: Aquarius", "zodiac-11-aquarius"),
36: ("Zodiac 12: Pisces", "zodiac-12-pisces"),
# Absolute Elements (cards 3738)
37: ("Absolute Element 1: Time", "absolute-element-1-time"),
38: ("Absolute Element 2: Space", "absolute-element-2-space"),
# Wanderers (cards 3949)
39: ("Wanderer 1: The Polestar", "wanderer-1-polestar"),
40: ("Wanderer 2: The Antichthon", "wanderer-2-antichthon"),
41: ("Wanderer 3: The Corestar", "wanderer-3-corestar"),
42: ("Wanderer 4: Mercury", "wanderer-4-mercury"),
43: ("Wanderer 5: Venus", "wanderer-5-venus"),
44: ("Wanderer 6: Mars", "wanderer-6-mars"),
45: ("Wanderer 7: Jupiter", "wanderer-7-jupiter"),
46: ("Wanderer 8: Saturn", "wanderer-8-saturn"),
47: ("Wanderer 9: Uranus", "wanderer-9-uranus"),
48: ("Wanderer 10: Neptune", "wanderer-10-neptune"),
49: ("Wanderer 11: The King & Queen of Hades", "wanderer-11-king-queen-hades"),
}
# Original (name, slug) pairs for reversal
MAJOR_ORIGINALS = {
6: ("Virtue VI: Controlled Folly", "virtue-vi-controlled-folly"),
7: ("Virtue VII: Not-Doing", "virtue-vii-not-doing"),
8: ("Virtue VIII: Losing Self-Importance", "virtue-viii-losing-self-importance"),
9: ("Virtue IX: Erasing Personal History", "virtue-ix-erasing-personal-history"),
18: ("Virtue XVIII: Stalking", "virtue-xviii-stalking"),
19: ("Virtue XIX: Intent", "virtue-xix-intent"),
20: ("Virtue XX: Dreaming", "virtue-xx-dreaming"),
21: ("Element XXI: Fire", "element-xxi-fire"),
22: ("Element XXII: Earth", "element-xxii-earth"),
23: ("Element XXIII: Air", "element-xxiii-air"),
24: ("Element XXIV: Water", "element-xxiv-water"),
25: ("Zodiac XXV: Aries", "zodiac-xxv-aries"),
26: ("Zodiac XXVI: Taurus", "zodiac-xxvi-taurus"),
27: ("Zodiac XXVII: Gemini", "zodiac-xxvii-gemini"),
28: ("Zodiac XXVIII: Cancer", "zodiac-xxviii-cancer"),
29: ("Zodiac XXIX: Leo", "zodiac-xxix-leo"),
30: ("Zodiac XXX: Virgo", "zodiac-xxx-virgo"),
31: ("Zodiac XXXI: Libra", "zodiac-xxxi-libra"),
32: ("Zodiac XXXII: Scorpio", "zodiac-xxxii-scorpio"),
33: ("Zodiac XXXIII: Sagittarius", "zodiac-xxxiii-sagittarius"),
34: ("Zodiac XXXIV: Capricorn", "zodiac-xxxiv-capricorn"),
35: ("Zodiac XXXV: Aquarius", "zodiac-xxxv-aquarius"),
36: ("Zodiac XXXVI: Pisces", "zodiac-xxxvi-pisces"),
37: ("Element XXXVII: Time", "element-xxxvii-time"),
38: ("Element XXXVIII: Space", "element-xxxviii-space"),
39: ("Wanderer XXXIX: The Polestar", "wanderer-xxxix-polestar"),
40: ("Wanderer XL: The Antichthon", "wanderer-xl-antichthon"),
41: ("Wanderer XLI: The Corestar", "wanderer-xli-corestar"),
42: ("Wanderer XLII: Mercury", "wanderer-xlii-mercury"),
43: ("Wanderer XLIII: Venus", "wanderer-xliii-venus"),
44: ("Wanderer XLIV: Mars", "wanderer-xliv-mars"),
45: ("Wanderer XLV: Jupiter", "wanderer-xlv-jupiter"),
46: ("Wanderer XLVI: Saturn", "wanderer-xlvi-saturn"),
47: ("Wanderer XLVII: Uranus", "wanderer-xlvii-uranus"),
48: ("Wanderer XLVIII: Neptune", "wanderer-xlviii-neptune"),
49: ("Wanderer XLIX: The King & Queen of Hades", "wanderer-xlix-king-queen-hades"),
}
# Pip number → spelled-out word (slugs already use the word form, only name changes)
PIP_SPELLINGS = {
2: "Two", 3: "Three", 4: "Four", 5: "Five",
6: "Six", 7: "Seven", 8: "Eight", 9: "Nine", 10: "Ten",
}
SUITS = ["WANDS", "CUPS", "SWORDS", "COINS"]
def rename_forward(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
earthman = DeckVariant.objects.filter(slug="earthman").first()
if not earthman:
return
# 1. Rename grouped major arcana to group-relative ordinals
for number, (new_name, new_slug) in MAJOR_RENAMES.items():
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=number
).update(name=new_name, slug=new_slug)
# 2. Spell out pip names 210
for number, word in PIP_SPELLINGS.items():
for suit in SUITS:
TarotCard.objects.filter(
deck_variant=earthman, arcana="MINOR", suit=suit, number=number
).update(name=f"{word} of {suit.capitalize()}")
def rename_reverse(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
earthman = DeckVariant.objects.filter(slug="earthman").first()
if not earthman:
return
# 1. Restore original major arcana names
for number, (old_name, old_slug) in MAJOR_ORIGINALS.items():
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=number
).update(name=old_name, slug=old_slug)
# 2. Restore numeric pip names (slugs unchanged)
for number, _word in PIP_SPELLINGS.items():
for suit in SUITS:
TarotCard.objects.filter(
deck_variant=earthman, arcana="MINOR", suit=suit, number=number
).update(name=f"{number} of {suit.capitalize()}")
class Migration(migrations.Migration):
dependencies = [
("epic", "0011_rename_earthman_court_cards"),
]
operations = [
migrations.RunPython(rename_forward, reverse_code=rename_reverse),
]

View File

@@ -1,55 +0,0 @@
"""
Data migration: rename Earthman 4th-suit cards from COINS → PENTACLES.
Updates:
- suit field: "COINS""PENTACLES"
- name: "X of Coins""X of Pentacles"
- slug: "x-of-coins-em""x-of-pentacles-em"
"""
from django.db import migrations
def coins_to_pentacles(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
earthman = DeckVariant.objects.filter(slug="earthman").first()
if not earthman:
return
cards = TarotCard.objects.filter(deck_variant=earthman, suit="COINS")
for card in cards:
card.suit = "PENTACLES"
card.name = card.name.replace(" of Coins", " of Pentacles")
card.slug = card.slug.replace("-of-coins-em", "-of-pentacles-em")
card.save(update_fields=["suit", "name", "slug"])
def pentacles_to_coins(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
earthman = DeckVariant.objects.filter(slug="earthman").first()
if not earthman:
return
# Only reverse cards that came from Earthman (identified by -em slug suffix)
cards = TarotCard.objects.filter(
deck_variant=earthman, suit="PENTACLES", slug__endswith="-em"
)
for card in cards:
card.suit = "COINS"
card.name = card.name.replace(" of Pentacles", " of Coins")
card.slug = card.slug.replace("-of-pentacles-em", "-of-coins-em")
card.save(update_fields=["suit", "name", "slug"])
class Migration(migrations.Migration):
dependencies = [
("epic", "0012_rename_earthman_major_groups_and_pip_spellings"),
]
operations = [
migrations.RunPython(coins_to_pentacles, reverse_code=pentacles_to_coins),
]

View File

@@ -1,65 +0,0 @@
"""
Data migration: rename the five Pope cards to use Arabic group-relative ordinals,
matching the convention set for other grouped major arcana.
"Pope I: President""Pope 1: President"
"Pope II: Tsar""Pope 2: Tsar"
etc.
"""
from django.db import migrations
POPE_RENAMES = {
1: ("Pope 1: President", "pope-1-president"),
2: ("Pope 2: Tsar", "pope-2-tsar"),
3: ("Pope 3: Chairman", "pope-3-chairman"),
4: ("Pope 4: Emperor", "pope-4-emperor"),
5: ("Pope 5: Chancellor", "pope-5-chancellor"),
}
POPE_ORIGINALS = {
1: ("Pope I: President", "pope-i-president"),
2: ("Pope II: Tsar", "pope-ii-tsar"),
3: ("Pope III: Chairman", "pope-iii-chairman"),
4: ("Pope IV: Emperor", "pope-iv-emperor"),
5: ("Pope V: Chancellor", "pope-v-chancellor"),
}
def rename_forward(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
earthman = DeckVariant.objects.filter(slug="earthman").first()
if not earthman:
return
for number, (new_name, new_slug) in POPE_RENAMES.items():
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=number
).update(name=new_name, slug=new_slug)
def rename_reverse(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
earthman = DeckVariant.objects.filter(slug="earthman").first()
if not earthman:
return
for number, (old_name, old_slug) in POPE_ORIGINALS.items():
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=number
).update(name=old_name, slug=old_slug)
class Migration(migrations.Migration):
dependencies = [
("epic", "0013_earthman_coins_to_pentacles"),
]
operations = [
migrations.RunPython(rename_forward, reverse_code=rename_reverse),
]

View File

@@ -1,42 +0,0 @@
"""
Data migration: rename Earthman card 22 from "Classical Element 2: Earth"
to "Classical Element 2: Stone" (Stone = Ossum, the Earthman name for Earth).
"""
from django.db import migrations
def rename_forward(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
earthman = DeckVariant.objects.filter(slug="earthman").first()
if not earthman:
return
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=22
).update(name="Classical Element 2: Stone", slug="classical-element-2-stone")
def rename_reverse(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
earthman = DeckVariant.objects.filter(slug="earthman").first()
if not earthman:
return
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=22
).update(name="Classical Element 2: Earth", slug="classical-element-2-earth")
class Migration(migrations.Migration):
dependencies = [
("epic", "0014_rename_earthman_popes_arabic_ordinals"),
]
operations = [
migrations.RunPython(rename_forward, reverse_code=rename_reverse),
]

View File

@@ -1,63 +0,0 @@
"""
Data migration: reorder the five Pope cards.
New assignment (card number → title):
1 → Chancellor 2 → President 3 → Tsar 4 → Chairman 5 → Emperor
"""
from django.db import migrations
POPE_RENAMES = {
1: ("Pope 1: Chancellor", "pope-1-chancellor"),
2: ("Pope 2: President", "pope-2-president"),
3: ("Pope 3: Tsar", "pope-3-tsar"),
4: ("Pope 4: Chairman", "pope-4-chairman"),
5: ("Pope 5: Emperor", "pope-5-emperor"),
}
POPE_ORIGINALS = {
1: ("Pope 1: President", "pope-1-president"),
2: ("Pope 2: Tsar", "pope-2-tsar"),
3: ("Pope 3: Chairman", "pope-3-chairman"),
4: ("Pope 4: Emperor", "pope-4-emperor"),
5: ("Pope 5: Chancellor", "pope-5-chancellor"),
}
def rename_forward(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
earthman = DeckVariant.objects.filter(slug="earthman").first()
if not earthman:
return
for number, (new_name, new_slug) in POPE_RENAMES.items():
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=number
).update(name=new_name, slug=new_slug)
def rename_reverse(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
earthman = DeckVariant.objects.filter(slug="earthman").first()
if not earthman:
return
for number, (old_name, old_slug) in POPE_ORIGINALS.items():
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=number
).update(name=old_name, slug=old_slug)
class Migration(migrations.Migration):
dependencies = [
("epic", "0015_rename_classical_element_earth_to_stone"),
]
operations = [
migrations.RunPython(rename_forward, reverse_code=rename_reverse),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 6.0 on 2026-03-25 05:46
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0016_reorder_earthman_popes'),
]
operations = [
migrations.AddField(
model_name='tableseat',
name='significator',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='significator_seats', to='epic.tarotcard'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 6.0 on 2026-04-01 17:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0017_tableseat_significator_fk'),
]
operations = [
migrations.AlterField(
model_name='tarotcard',
name='suit',
field=models.CharField(blank=True, choices=[('WANDS', 'Wands'), ('CUPS', 'Cups'), ('SWORDS', 'Swords'), ('PENTACLES', 'Pentacles')], max_length=10, null=True),
),
]

View File

@@ -1,70 +0,0 @@
"""
Data migration: rename The Schiz (card 0) and the five Pope cards (cards 15)
in the Earthman deck.
0: "The Schiz""The Nomad"
1: "Pope 1: Chancellor""Pope 1: The Schizo"
2: "Pope 2: President""Pope 2: The Despot"
3: "Pope 3: Tsar""Pope 3: The Capitalist"
4: "Pope 4: Chairman""Pope 4: The Fascist"
5: "Pope 5: Emperor""Pope 5: The War Machine"
"""
from django.db import migrations
NEW_NAMES = {
0: ("The Nomad", "the-nomad"),
1: ("Pope 1: The Schizo", "pope-1-the-schizo"),
2: ("Pope 2: The Despot", "pope-2-the-despot"),
3: ("Pope 3: The Capitalist", "pope-3-the-capitalist"),
4: ("Pope 4: The Fascist", "pope-4-the-fascist"),
5: ("Pope 5: The War Machine","pope-5-the-war-machine"),
}
OLD_NAMES = {
0: ("The Schiz", "the-schiz"),
1: ("Pope 1: Chancellor", "pope-1-chancellor"),
2: ("Pope 2: President", "pope-2-president"),
3: ("Pope 3: Tsar", "pope-3-tsar"),
4: ("Pope 4: Chairman", "pope-4-chairman"),
5: ("Pope 5: Emperor", "pope-5-emperor"),
}
def rename_forward(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
earthman = DeckVariant.objects.filter(slug="earthman").first()
if not earthman:
return
for number, (new_name, new_slug) in NEW_NAMES.items():
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=number
).update(name=new_name, slug=new_slug)
def rename_reverse(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
earthman = DeckVariant.objects.filter(slug="earthman").first()
if not earthman:
return
for number, (old_name, old_slug) in OLD_NAMES.items():
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=number
).update(name=old_name, slug=old_slug)
class Migration(migrations.Migration):
dependencies = [
("epic", "0018_alter_tarotcard_suit"),
]
operations = [
migrations.RunPython(rename_forward, reverse_code=rename_reverse),
]

View File

@@ -1,63 +0,0 @@
"""
Data migration: rename Pope cards 25 in the Earthman deck.
2: "Pope 2: The Despot""Pope 2: The Occultist"
3: "Pope 3: The Capitalist""Pope 3: The Despot"
4: "Pope 4: The Fascist""Pope 4: The Capitalist"
5: "Pope 5: The War Machine""Pope 5: The Fascist"
"""
from django.db import migrations
NEW_NAMES = {
2: ("Pope 2: The Occultist", "pope-2-the-occultist"),
3: ("Pope 3: The Despot", "pope-3-the-despot"),
4: ("Pope 4: The Capitalist","pope-4-the-capitalist"),
5: ("Pope 5: The Fascist", "pope-5-the-fascist"),
}
OLD_NAMES = {
2: ("Pope 2: The Despot", "pope-2-the-despot"),
3: ("Pope 3: The Capitalist", "pope-3-the-capitalist"),
4: ("Pope 4: The Fascist", "pope-4-the-fascist"),
5: ("Pope 5: The War Machine", "pope-5-the-war-machine"),
}
def rename_forward(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
earthman = DeckVariant.objects.filter(slug="earthman").first()
if not earthman:
return
for number, (new_name, new_slug) in NEW_NAMES.items():
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=number
).update(name=new_name, slug=new_slug)
def rename_reverse(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
earthman = DeckVariant.objects.filter(slug="earthman").first()
if not earthman:
return
for number, (old_name, old_slug) in OLD_NAMES.items():
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=number
).update(name=old_name, slug=old_slug)
class Migration(migrations.Migration):
dependencies = [
("epic", "0019_rename_earthman_schiz_and_popes"),
]
operations = [
migrations.RunPython(rename_forward, reverse_code=rename_reverse),
]

View File

@@ -1,56 +0,0 @@
"""
Data migration: rename/update six Earthman Major Arcana cards.
13 name: "Death""King Death & the Cosmic Tree"
14 name: "The Traitor""The Great Hunt"
15 correspondence: "The Tower / La Torre""The House of the Devil / Inferno"
16 correspondence: "Purgatorio""The Tower / La Torre / Purgatorio"
50 name/slug: "The Eagle""The Mould of Man"
51 name/slug: "Divine Calculus""The Eagle"
"""
from django.db import migrations
FORWARD = {
13: dict(name="King Death & the Cosmic Tree", slug="king-death-and-the-cosmic-tree"),
14: dict(name="The Great Hunt", slug="the-great-hunt"),
15: dict(correspondence="The House of the Devil / Inferno"),
16: dict(correspondence="The Tower / La Torre / Purgatorio"),
50: dict(name="The Mould of Man", slug="the-mould-of-man"),
51: dict(name="The Eagle", slug="the-eagle"),
}
REVERSE = {
13: dict(name="Death", slug="death-em"),
14: dict(name="The Traitor", slug="the-traitor"),
15: dict(correspondence="The Tower / La Torre"),
16: dict(correspondence="Purgatorio"),
50: dict(name="The Eagle", slug="the-eagle"),
51: dict(name="Divine Calculus",slug="divine-calculus"),
}
def apply(changes):
def fn(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
earthman = DeckVariant.objects.filter(slug="earthman").first()
if not earthman:
return
# Process in sorted order so card 50 vacates "the-eagle" slug before card 51 claims it.
for number in sorted(changes):
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=number
).update(**changes[number])
return fn
class Migration(migrations.Migration):
dependencies = [
("epic", "0020_rename_earthman_pope_cards_2_5"),
]
operations = [
migrations.RunPython(apply(FORWARD), reverse_code=apply(REVERSE)),
]

View File

@@ -1,31 +0,0 @@
# Generated by Django 6.0 on 2026-04-06 00:02
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0021_rename_earthman_major_arcana_batch_2'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='SigReservation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('role', models.CharField(max_length=2)),
('polarity', models.CharField(choices=[('levity', 'Levity'), ('gravity', 'Gravity')], max_length=7)),
('reserved_at', models.DateTimeField(auto_now_add=True)),
('card', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sig_reservations', to='epic.tarotcard')),
('gamer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sig_reservations', to=settings.AUTH_USER_MODEL)),
('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sig_reservations', to='epic.room')),
],
options={
'constraints': [models.UniqueConstraint(fields=('room', 'gamer'), name='one_sig_reservation_per_gamer_per_room'), models.UniqueConstraint(fields=('room', 'card', 'polarity'), name='one_reservation_per_card_per_polarity_per_room')],
},
),
]

View File

@@ -1,28 +0,0 @@
# Generated by Django 6.0 on 2026-04-06 02:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0022_sig_reservation'),
]
operations = [
migrations.AddField(
model_name='tarotcard',
name='icon',
field=models.CharField(blank=True, default='', max_length=50),
),
migrations.AlterField(
model_name='tarotcard',
name='arcana',
field=models.CharField(choices=[('MAJOR', 'Major Arcana'), ('MINOR', 'Minor Arcana'), ('MIDDLE', 'Middle Arcana')], max_length=6),
),
migrations.AlterField(
model_name='tarotcard',
name='suit',
field=models.CharField(blank=True, choices=[('WANDS', 'Wands'), ('CUPS', 'Cups'), ('SWORDS', 'Swords'), ('PENTACLES', 'Pentacles'), ('CROWNS', 'Crowns')], max_length=10, null=True),
),
]

View File

@@ -1,46 +0,0 @@
"""
Data migration: rename Earthman 4th-suit cards from PENTACLES → CROWNS.
Updates for every Earthman card where suit="PENTACLES":
- suit: "PENTACLES""CROWNS"
- name: " of Pentacles"" of Crowns"
- slug: "pentacles""crowns"
"""
from django.db import migrations
def pentacles_to_crowns(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
earthman = DeckVariant.objects.filter(slug="earthman").first()
if not earthman:
return
for card in TarotCard.objects.filter(deck_variant=earthman, suit="PENTACLES"):
card.suit = "CROWNS"
card.name = card.name.replace(" of Pentacles", " of Crowns")
card.slug = card.slug.replace("pentacles", "crowns")
card.save(update_fields=["suit", "name", "slug"])
def crowns_to_pentacles(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
earthman = DeckVariant.objects.filter(slug="earthman").first()
if not earthman:
return
for card in TarotCard.objects.filter(deck_variant=earthman, suit="CROWNS"):
card.suit = "PENTACLES"
card.name = card.name.replace(" of Crowns", " of Pentacles")
card.slug = card.slug.replace("crowns", "pentacles")
card.save(update_fields=["suit", "name", "slug"])
class Migration(migrations.Migration):
dependencies = [
("epic", "0023_tarotcard_icon_alter_tarotcard_arcana_and_more"),
]
operations = [
migrations.RunPython(pentacles_to_crowns, reverse_code=crowns_to_pentacles),
]

View File

@@ -1,62 +0,0 @@
"""
Data migration: Earthman deck — court cards and major arcana icons.
1. Court cards (numbers 1114, all suits): arcana "MINOR""MIDDLE"
2. Major arcana icons (stored in TarotCard.icon):
0 (Nomad) → fa-hat-cowboy-side
1 (Schizo) → fa-hat-wizard
251 (rest) → fa-hand-dots
"""
from django.db import migrations
MAJOR_ICONS = {
0: "fa-hat-cowboy-side",
1: "fa-hat-wizard",
}
DEFAULT_MAJOR_ICON = "fa-hand-dots"
def forward(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
earthman = DeckVariant.objects.filter(slug="earthman").first()
if not earthman:
return
# Court cards → MIDDLE
TarotCard.objects.filter(
deck_variant=earthman, arcana="MINOR", number__in=[11, 12, 13, 14]
).update(arcana="MIDDLE")
# Major arcana icons
for card in TarotCard.objects.filter(deck_variant=earthman, arcana="MAJOR"):
card.icon = MAJOR_ICONS.get(card.number, DEFAULT_MAJOR_ICON)
card.save(update_fields=["icon"])
def backward(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
earthman = DeckVariant.objects.filter(slug="earthman").first()
if not earthman:
return
TarotCard.objects.filter(
deck_variant=earthman, arcana="MIDDLE", number__in=[11, 12, 13, 14]
).update(arcana="MINOR")
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR"
).update(icon="")
class Migration(migrations.Migration):
dependencies = [
("epic", "0024_earthman_pentacles_to_crowns"),
]
operations = [
migrations.RunPython(forward, reverse_code=backward),
]

View File

@@ -1,154 +0,0 @@
"""
Data migration — Earthman deck:
1. Rename three suit codes (and card names) for Earthman cards:
WANDS → BRANDS (Wands → Brands)
CUPS → GRAILS (Cups → Grails)
SWORDS → BLADES (Swords → Blades)
CROWNS stays CROWNS.
2. Copy keywords_upright / keywords_reversed from the Fiorentine Minchiate
deck to corresponding Earthman cards:
• Major: explicit number-to-number map based on card correspondences.
• Minor/Middle: same number, suit mapped (BRANDS→WANDS, GRAILS→CUPS,
BLADES→SWORDS, CROWNS→PENTACLES). Cards with no Fiorentine counterpart
stay with empty keyword lists.
"""
from django.db import migrations
# ── 1. Suit rename map ────────────────────────────────────────────────────────
SUIT_RENAMES = {
"WANDS": "BRANDS",
"CUPS": "GRAILS",
"SWORDS": "BLADES",
}
# ── 2. Major arcana: Earthman number → Fiorentine number ─────────────────────
# Cards without a Fiorentine counterpart are omitted (keywords stay empty).
MAJOR_KEYWORD_MAP = {
0: 0, # The Schiz → The Fool
1: 1, # Pope I (President) → The Magician
2: 2, # Pope II (Tsar) → The High Priestess
3: 3, # Pope III (Chairman) → The Empress
4: 4, # Pope IV (Emperor) → The Emperor
5: 5, # Pope V (Chancellor) → The Hierophant
6: 8, # Virtue VI (Controlled Folly) → Strength
7: 11, # Virtue VII (Not-Doing) → Justice
8: 14, # Virtue VIII (Losing Self-Importance) → Temperance
# 9: Prudence — no Fiorentine equivalent
10: 10, # Wheel of Fortune → Wheel of Fortune
11: 7, # The Junkboat → The Chariot
12: 12, # The Junkman → The Hanged Man
13: 13, # Death → Death
14: 15, # The Traitor → The Devil
15: 16, # Disco Inferno → The Tower
# 16: Torre Terrestre (Purgatory) — no equivalent
# 17: Fantasia Celestia (Paradise) — no equivalent
18: 6, # Virtue XVIII (Stalking) → The Lovers
# 19: Virtue XIX (Intent / Hope) — no equivalent
# 20: Virtue XX (Dreaming / Faith)— no equivalent
# 2138: Classical Elements + Zodiac — no equivalents
39: 17, # Wanderer XXXIX (Polestar) → The Star
40: 18, # Wanderer XL (Antichthon) → The Moon
41: 19, # Wanderer XLI (Corestar) → The Sun
# 4249: Planets + The Binary — no equivalents
50: 20, # The Eagle → Judgement
51: 21, # Divine Calculus → The World
}
# ── 3. Minor suit map: Earthman (post-rename) → Fiorentine ───────────────────
MINOR_SUIT_MAP = {
"BRANDS": "WANDS",
"GRAILS": "CUPS",
"BLADES": "SWORDS",
"CROWNS": "PENTACLES",
}
def forward(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
try:
earthman = DeckVariant.objects.get(slug="earthman")
fiorentine = DeckVariant.objects.get(slug="fiorentine-minchiate")
except DeckVariant.DoesNotExist:
return # decks not seeded — nothing to do
# ── Step 1: rename Earthman suit codes + card names ───────────────────────
for old_suit, new_suit in SUIT_RENAMES.items():
old_display = old_suit.capitalize() # e.g. "Wands"
new_display = new_suit.capitalize() # e.g. "Brands"
cards = TarotCard.objects.filter(deck_variant=earthman, suit=old_suit)
for card in cards:
card.name = card.name.replace(f" of {old_display}", f" of {new_display}")
card.suit = new_suit
card.save()
# ── Step 2: copy major arcana keywords ───────────────────────────────────
fio_major = {
card.number: card
for card in TarotCard.objects.filter(deck_variant=fiorentine, arcana="MAJOR")
}
for em_num, fio_num in MAJOR_KEYWORD_MAP.items():
fio_card = fio_major.get(fio_num)
if not fio_card:
continue
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=em_num
).update(
keywords_upright=fio_card.keywords_upright,
keywords_reversed=fio_card.keywords_reversed,
)
# ── Step 3: copy minor/middle arcana keywords ─────────────────────────────
for em_suit, fio_suit in MINOR_SUIT_MAP.items():
fio_by_number = {
card.number: card
for card in TarotCard.objects.filter(deck_variant=fiorentine, suit=fio_suit)
}
for em_card in TarotCard.objects.filter(deck_variant=earthman, suit=em_suit):
fio_card = fio_by_number.get(em_card.number)
if fio_card:
em_card.keywords_upright = fio_card.keywords_upright
em_card.keywords_reversed = fio_card.keywords_reversed
em_card.save()
def reverse(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
try:
earthman = DeckVariant.objects.get(slug="earthman")
except DeckVariant.DoesNotExist:
return
# Reverse suit renames
reverse_renames = {new: old for old, new in SUIT_RENAMES.items()}
for new_suit, old_suit in reverse_renames.items():
new_display = new_suit.capitalize()
old_display = old_suit.capitalize()
cards = TarotCard.objects.filter(deck_variant=earthman, suit=new_suit)
for card in cards:
card.name = card.name.replace(f" of {new_display}", f" of {old_display}")
card.suit = old_suit
card.save()
# Clear all Earthman keywords
TarotCard.objects.filter(deck_variant=earthman).update(
keywords_upright=[],
keywords_reversed=[],
)
class Migration(migrations.Migration):
dependencies = [
("epic", "0025_earthman_middle_arcana_and_major_icons"),
]
operations = [
migrations.RunPython(forward, reverse_code=reverse),
]

View File

@@ -1,65 +0,0 @@
"""
Schema + data migration:
1. Add `cautions` JSONField (list, default=[]) to TarotCard.
2. Seed The Schizo (Earthman MAJOR #1) with 4 rival-interaction cautions.
All other cards default to [] — the UI shows a placeholder when empty.
"""
from django.db import migrations, models
SCHIZO_CAUTIONS = [
'This card will reverse into <span class="card-ref">The Pervert</span> when it'
' comes under dominion of <span class="card-ref">The Occultist</span>, which in turn'
' reverses into <span class="card-ref">Pestilence</span>.',
'This card will reverse into <span class="card-ref">The Paranoiac</span> when it'
' comes under dominion of <span class="card-ref">The Despot</span>, which in turn'
' reverses into <span class="card-ref">War</span>.',
'This card will reverse into <span class="card-ref">The Neurotic</span> when it'
' comes under dominion of <span class="card-ref">The Capitalist</span>, which in turn'
' reverses into <span class="card-ref">Famine</span>.',
'This card will reverse into <span class="card-ref">The Suicidal</span> when it'
' comes under dominion of <span class="card-ref">The Fascist</span>, which in turn'
' reverses into <span class="card-ref">Death</span>.',
]
def seed_schizo_cautions(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
try:
earthman = DeckVariant.objects.get(slug="earthman")
except DeckVariant.DoesNotExist:
return
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=1
).update(cautions=SCHIZO_CAUTIONS)
def clear_schizo_cautions(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
try:
earthman = DeckVariant.objects.get(slug="earthman")
except DeckVariant.DoesNotExist:
return
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=1
).update(cautions=[])
class Migration(migrations.Migration):
dependencies = [
("epic", "0026_earthman_suit_renames_and_keywords"),
]
operations = [
migrations.AddField(
model_name="tarotcard",
name="cautions",
field=models.JSONField(default=list),
),
migrations.RunPython(seed_schizo_cautions, reverse_code=clear_schizo_cautions),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 6.0 on 2026-04-07 03:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0027_tarotcard_cautions'),
]
operations = [
migrations.AlterField(
model_name='tarotcard',
name='suit',
field=models.CharField(blank=True, choices=[('WANDS', 'Wands'), ('CUPS', 'Cups'), ('SWORDS', 'Swords'), ('PENTACLES', 'Pentacles'), ('CROWNS', 'Crowns'), ('BRANDS', 'Brands'), ('GRAILS', 'Grails'), ('BLADES', 'Blades')], max_length=10, null=True),
),
]

View File

@@ -1,61 +0,0 @@
"""
Data fix: clear Schizo cautions from The Nomad (number=0) if present,
and ensure they land on The Schizo (number=1).
"""
from django.db import migrations
SCHIZO_CAUTIONS = [
'This card will reverse into <span class="card-ref">I. The Pervert</span> when it'
' comes under dominion of <span class="card-ref">II. The Occultist</span>, which in turn'
' reverses into <span class="card-ref">II. Pestilence</span>.',
'This card will reverse into <span class="card-ref">I. The Paranoiac</span> when it'
' comes under dominion of <span class="card-ref">III. The Despot</span>, which in turn'
' reverses into <span class="card-ref">III. War</span>.',
'This card will reverse into <span class="card-ref">I. The Neurotic</span> when it'
' comes under dominion of <span class="card-ref">IV. The Capitalist</span>, which in turn'
' reverses into <span class="card-ref">IV. Famine</span>.',
'This card will reverse into <span class="card-ref">I. The Suicidal</span> when it'
' comes under dominion of <span class="card-ref">V. The Fascist</span>, which in turn'
' reverses into <span class="card-ref">V. Death</span>.',
]
def forward(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
try:
earthman = DeckVariant.objects.get(slug="earthman")
except DeckVariant.DoesNotExist:
return
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=0
).update(cautions=[])
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=1
).update(cautions=SCHIZO_CAUTIONS)
def reverse(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
try:
earthman = DeckVariant.objects.get(slug="earthman")
except DeckVariant.DoesNotExist:
return
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=1
).update(cautions=[])
class Migration(migrations.Migration):
dependencies = [
("epic", "0028_alter_tarotcard_suit"),
]
operations = [
migrations.RunPython(forward, reverse_code=reverse),
]

View File

@@ -1,23 +0,0 @@
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0029_fix_schizo_cautions'),
]
operations = [
migrations.AddField(
model_name='sigreservation',
name='seat',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='sig_reservation',
to='epic.tableseat',
),
),
]

View File

@@ -1,33 +0,0 @@
# Generated by Django 6.0 on 2026-04-09 04:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0030_sigreservation_seat_fk'),
]
operations = [
migrations.AddField(
model_name='room',
name='sig_select_started_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='sigreservation',
name='countdown_remaining',
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='sigreservation',
name='ready',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='room',
name='table_status',
field=models.CharField(blank=True, choices=[('ROLE_SELECT', 'Role Select'), ('SIG_SELECT', 'Significator Select'), ('SKY_SELECT', 'Sky Select'), ('IN_GAME', 'In Game')], max_length=20, null=True),
),
]

View File

@@ -1,65 +0,0 @@
# Generated by Django 6.0 on 2026-04-14 05:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0031_sig_ready_sky_select'),
]
operations = [
migrations.CreateModel(
name='AspectType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20, unique=True)),
('symbol', models.CharField(max_length=5)),
('angle', models.PositiveSmallIntegerField()),
('orb', models.FloatField()),
],
options={
'ordering': ['angle'],
},
),
migrations.CreateModel(
name='HouseLabel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('number', models.PositiveSmallIntegerField(unique=True)),
('name', models.CharField(max_length=30)),
('keywords', models.CharField(blank=True, max_length=100)),
],
options={
'ordering': ['number'],
},
),
migrations.CreateModel(
name='Planet',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20, unique=True)),
('symbol', models.CharField(max_length=5)),
('order', models.PositiveSmallIntegerField(unique=True)),
],
options={
'ordering': ['order'],
},
),
migrations.CreateModel(
name='Sign',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20, unique=True)),
('symbol', models.CharField(max_length=5)),
('element', models.CharField(choices=[('Fire', 'Fire'), ('Earth', 'Earth'), ('Air', 'Air'), ('Water', 'Water')], max_length=5)),
('modality', models.CharField(choices=[('Cardinal', 'Cardinal'), ('Fixed', 'Fixed'), ('Mutable', 'Mutable')], max_length=8)),
('order', models.PositiveSmallIntegerField(unique=True)),
('start_degree', models.FloatField()),
],
options={
'ordering': ['order'],
},
),
]

View File

@@ -1,106 +0,0 @@
"""
Data migration: seed Sign, Planet, AspectType, and HouseLabel tables.
These are stable astrological reference rows — never user-edited.
The data matches the constants in pyswiss/apps/charts/calc.py so that
the proxy view and D3 wheel share a single source of truth.
"""
from django.db import migrations
# ── Signs ────────────────────────────────────────────────────────────────────
# (order, name, symbol, element, modality, start_degree)
SIGNS = [
(0, 'Aries', '', 'Fire', 'Cardinal', 0.0),
(1, 'Taurus', '', 'Earth', 'Fixed', 30.0),
(2, 'Gemini', '', 'Air', 'Mutable', 60.0),
(3, 'Cancer', '', 'Water', 'Cardinal', 90.0),
(4, 'Leo', '', 'Fire', 'Fixed', 120.0),
(5, 'Virgo', '', 'Earth', 'Mutable', 150.0),
(6, 'Libra', '', 'Air', 'Cardinal', 180.0),
(7, 'Scorpio', '', 'Water', 'Fixed', 210.0),
(8, 'Sagittarius', '', 'Fire', 'Mutable', 240.0),
(9, 'Capricorn', '', 'Earth', 'Cardinal', 270.0),
(10, 'Aquarius', '', 'Air', 'Fixed', 300.0),
(11, 'Pisces', '', 'Water', 'Mutable', 330.0),
]
# ── Planets ───────────────────────────────────────────────────────────────────
# (order, name, symbol)
PLANETS = [
(0, 'Sun', ''),
(1, 'Moon', ''),
(2, 'Mercury', ''),
(3, 'Venus', ''),
(4, 'Mars', ''),
(5, 'Jupiter', ''),
(6, 'Saturn', ''),
(7, 'Uranus', ''),
(8, 'Neptune', ''),
(9, 'Pluto', ''),
]
# ── Aspect types ──────────────────────────────────────────────────────────────
# (name, symbol, angle, orb) — mirrors ASPECTS constant in pyswiss calc.py
ASPECT_TYPES = [
('Conjunction', '', 0, 8.0),
('Sextile', '', 60, 6.0),
('Square', '', 90, 8.0),
('Trine', '', 120, 8.0),
('Opposition', '', 180, 10.0),
]
# ── House labels (distinctions) ───────────────────────────────────────────────
# (number, name, keywords)
HOUSE_LABELS = [
(1, 'Self', 'identity, appearance, first impressions'),
(2, 'Worth', 'possessions, values, finances'),
(3, 'Education', 'communication, siblings, short journeys'),
(4, 'Family', 'home, roots, ancestry'),
(5, 'Creation', 'creativity, romance, children, pleasure'),
(6, 'Ritual', 'service, health, daily routines'),
(7, 'Cooperation', 'partnerships, marriage, open enemies'),
(8, 'Regeneration', 'transformation, shared resources, death'),
(9, 'Enterprise', 'philosophy, travel, higher learning'),
(10, 'Career', 'public life, reputation, authority'),
(11, 'Reward', 'friends, groups, aspirations'),
(12, 'Reprisal', 'hidden matters, karma, self-undoing'),
]
def forward(apps, schema_editor):
Sign = apps.get_model('epic', 'Sign')
Planet = apps.get_model('epic', 'Planet')
AspectType = apps.get_model('epic', 'AspectType')
HouseLabel = apps.get_model('epic', 'HouseLabel')
for order, name, symbol, element, modality, start_degree in SIGNS:
Sign.objects.create(
order=order, name=name, symbol=symbol,
element=element, modality=modality, start_degree=start_degree,
)
for order, name, symbol in PLANETS:
Planet.objects.create(order=order, name=name, symbol=symbol)
for name, symbol, angle, orb in ASPECT_TYPES:
AspectType.objects.create(name=name, symbol=symbol, angle=angle, orb=orb)
for number, name, keywords in HOUSE_LABELS:
HouseLabel.objects.create(number=number, name=name, keywords=keywords)
def reverse(apps, schema_editor):
for model_name in ('Sign', 'Planet', 'AspectType', 'HouseLabel'):
apps.get_model('epic', model_name).objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
('epic', '0032_astro_reference_tables'),
]
operations = [
migrations.RunPython(forward, reverse_code=reverse),
]

View File

@@ -1,35 +0,0 @@
# Generated by Django 6.0 on 2026-04-14 05:29
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0033_seed_astro_reference_tables'),
]
operations = [
migrations.CreateModel(
name='Character',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('birth_dt', models.DateTimeField(blank=True, null=True)),
('birth_lat', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
('birth_lon', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
('birth_place', models.CharField(blank=True, max_length=200)),
('house_system', models.CharField(choices=[('O', 'Porphyry'), ('P', 'Placidus'), ('K', 'Koch'), ('W', 'Whole Sign')], default='O', max_length=1)),
('chart_data', models.JSONField(blank=True, null=True)),
('celtic_cross', models.JSONField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('confirmed_at', models.DateTimeField(blank=True, null=True)),
('retired_at', models.DateTimeField(blank=True, null=True)),
('seat', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='characters', to='epic.tableseat')),
('significator', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='character_significators', to='epic.tarotcard')),
],
options={
'ordering': ['-created_at'],
},
),
]

View File

@@ -1,58 +0,0 @@
# Generated by Django 6.0 on 2026-04-27 05:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0034_character_model'),
]
operations = [
migrations.AddField(
model_name='tarotcard',
name='articulations',
field=models.JSONField(default=list),
),
migrations.AddField(
model_name='tarotcard',
name='gravity_emanation',
field=models.CharField(blank=True, default='', max_length=200),
),
migrations.AddField(
model_name='tarotcard',
name='gravity_qualifier',
field=models.CharField(blank=True, default='', max_length=100),
),
migrations.AddField(
model_name='tarotcard',
name='gravity_reversal',
field=models.CharField(blank=True, default='', max_length=200),
),
migrations.AddField(
model_name='tarotcard',
name='levity_emanation',
field=models.CharField(blank=True, default='', max_length=200),
),
migrations.AddField(
model_name='tarotcard',
name='levity_qualifier',
field=models.CharField(blank=True, default='', max_length=100),
),
migrations.AddField(
model_name='tarotcard',
name='levity_reversal',
field=models.CharField(blank=True, default='', max_length=200),
),
migrations.AddField(
model_name='tarotcard',
name='mechanisms',
field=models.JSONField(default=list),
),
migrations.AddField(
model_name='tarotcard',
name='reversal',
field=models.CharField(blank=True, default='', max_length=200),
),
]

View File

@@ -1,307 +0,0 @@
"""
Re-seed the Earthman major arcana from 52 cards (0-51) to 50 cards (0 + 1-49).
Card 0 (The Nomad) is left completely untouched.
Changes:
DELETE old #10 Wheel of Fortune, #11 The Junkboat, #14 The Great Hunt
INSERT new #41 The Asteroid Belt
UPDATE all other major arcana with new numbers, names, slugs, groups,
correspondences, reversals, and levity/gravity qualifiers.
Items flagged ⚠ below were not clearly legible from the source image and are
left blank; the user should fill them in a follow-up migration.
"""
from django.db import migrations
# ---------------------------------------------------------------------------
# UPDATE spec: (current_slug, new_number, new_name, new_slug, new_group,
# correspondence, reversal,
# levity_qualifier, gravity_qualifier,
# levity_emanation, gravity_emanation,
# levity_reversal, gravity_reversal)
# ---------------------------------------------------------------------------
_UPDATES = [
# ── Pope/Horseman ──────────────────────────────────────────────────────
('pope-1-the-schizo', 1, 'The Schizo',
'the-schizo', 'Pope/Horseman',
'Territoriality', '',
'Enlightened', 'Engraven',
'', '', '', ''),
('pope-2-the-occultist', 2, 'The Occultist',
'the-occultist', 'Pope/Horseman',
'Territoriality', '',
'Enlightened', 'Engraven',
'', '', '', ''),
('pope-3-the-despot', 3, 'The Despot',
'the-despot', 'Pope/Horseman',
'Despotism', '',
'Enlightened', 'Engraven',
'', '', '', ''),
('pope-4-the-capitalist', 4, 'The Capitalist',
'the-capitalist', 'Pope/Horseman',
'Capitalism', '',
'Enlightened', 'Engraven',
'', '', '', ''),
('pope-5-the-fascist', 5, 'The Fascist',
'the-fascist', 'Pope/Horseman',
'Fascism', '',
'Enlightened', 'Engraven',
'', '', '', ''),
# ── Implicit Virtues ───────────────────────────────────────────────────
# ⚠ levity/gravity qualifiers not determined
('implicit-virtue-1-controlled-folly', 6, 'Controlled Folly',
'controlled-folly', 'Implicit Virtues',
'Fortitude', '',
'', '', '', '', '', ''),
('implicit-virtue-2-not-doing', 7, 'Not Doing',
'not-doing', 'Implicit Virtues',
'Temperance', '',
'', '', '', '', '', ''),
('implicit-virtue-3-losing-self-importance', 8, 'Losing Self-Importance',
'losing-self-importance', 'Implicit Virtues',
'Justice', '',
'', '', '', '', '', ''),
('implicit-virtue-4-erasing-personal-history', 9, 'Erasing Personal History',
'erasing-personal-history', 'Implicit Virtues',
'Prudence', '',
'', '', '', '', '', ''),
# ── Elements (6) — Absolute elements move here alongside Classical ─────
# Space: was Absolute Element 2 (#38), Chariot correspondence is new
('absolute-element-2-space', 10, 'Space',
'space-em', 'Elements',
'Chariot', 'Nexus',
'Kinetic', 'Potential',
'', '', '', ''),
# Time: was Absolute Element 1 (#37)
# ⚠ Hermit correspondence confirmed; "Hunchback" variant noted in image
('absolute-element-1-time', 11, 'Time',
'time-em', 'Elements',
'Hermit', 'Tempo',
'Kinetic', 'Potential',
'', '', '', ''),
# Stone: was Classical Element 2 (#22)
('classical-element-2-stone', 12, 'Stone',
'stone-em', 'Elements',
'Earth', 'Ossum',
'Kinetic', 'Potential',
'', '', '', ''),
# Fire: was Classical Element 1 (#21)
('classical-element-1-fire', 13, 'Fire',
'fire-em', 'Elements',
'Fire', 'Ardor',
'Kinetic', 'Potential',
'', '', '', ''),
# Water: was Classical Element 4 (#24)
('classical-element-4-water', 14, 'Water',
'water-em', 'Elements',
'Water', 'Humor',
'Kinetic', 'Potential',
'', '', '', ''),
# Air: was Classical Element 3 (#23)
('classical-element-3-air', 15, 'Air',
'air-em', 'Elements',
'Air', 'Pneuma',
'Kinetic', 'Potential',
'', '', '', ''),
# ── Realms ─────────────────────────────────────────────────────────────
('disco-inferno', 16, 'Disco Inferno',
'disco-inferno', 'Realms',
'Devil', 'Shame',
'Deasil', 'Widdershins',
'', '', '', ''),
('torre-terrestre', 17, 'Torre Terrestre',
'torre-terrestre', 'Realms',
'Tower', 'Guilt',
'Deasil', 'Widdershins',
'', '', '', ''),
('fantasia-celestia', 18, 'Fantasia Celestia',
'fantasia-celestia', 'Realms',
'Wheel of Fortune', 'Anxiety',
'Deasil', 'Widdershins',
'', '', '', ''),
# ── Explicit Virtues ───────────────────────────────────────────────────
# ⚠ levity/gravity qualifiers not determined
# Stalking: was Explicit Virtue 1 (#18, Love/Charity)
('explicit-virtue-1-stalking', 19, 'Stalking',
'stalking', 'Explicit Virtues',
'Charity', '',
'', '', '', '', '', ''),
# Dreaming: was Explicit Virtue 3 (#20, Faith) — same number
('explicit-virtue-3-dreaming', 20, 'Dreaming',
'dreaming', 'Explicit Virtues',
'Faith', '',
'', '', '', '', '', ''),
# Jovent: was Explicit Virtue 2 (#19, Intent/Hope) — renamed
('explicit-virtue-2-intent', 21, 'Jovent',
'jovent', 'Explicit Virtues',
'Hope', '',
'', '', '', '', '', ''),
# ── Zodiac Signs & Houses (renumber 25-36 → 22-33) ────────────────────
('zodiac-1-aries', 22, 'Aries', 'aries', 'Zodiac Signs & Houses', 'House of Self', '', 'Precessional', 'Recessional', '', '', '', ''),
('zodiac-2-taurus', 23, 'Taurus', 'taurus', 'Zodiac Signs & Houses', 'House of Worth', '', 'Precessional', 'Recessional', '', '', '', ''),
('zodiac-3-gemini', 24, 'Gemini', 'gemini', 'Zodiac Signs & Houses', 'House of Education', '', 'Precessional', 'Recessional', '', '', '', ''),
('zodiac-4-cancer', 25, 'Cancer', 'cancer', 'Zodiac Signs & Houses', 'House of Family', '', 'Precessional', 'Recessional', '', '', '', ''),
('zodiac-5-leo', 26, 'Leo', 'leo', 'Zodiac Signs & Houses', 'House of Creation', '', 'Precessional', 'Recessional', '', '', '', ''),
('zodiac-6-virgo', 27, 'Virgo', 'virgo', 'Zodiac Signs & Houses', 'House of Ritual', '', 'Precessional', 'Recessional', '', '', '', ''),
('zodiac-7-libra', 28, 'Libra', 'libra', 'Zodiac Signs & Houses', 'House of Cooperation', '', 'Precessional', 'Recessional', '', '', '', ''),
('zodiac-8-scorpio', 29, 'Scorpio', 'scorpio', 'Zodiac Signs & Houses', 'House of Regeneration', '', 'Precessional', 'Recessional', '', '', '', ''),
('zodiac-9-sagittarius', 30, 'Sagittarius', 'sagittarius', 'Zodiac Signs & Houses', 'House of Enterprise', '', 'Precessional', 'Recessional', '', '', '', ''),
('zodiac-10-capricorn', 31, 'Capricorn', 'capricorn', 'Zodiac Signs & Houses', 'House of Career', '', 'Precessional', 'Recessional', '', '', '', ''),
('zodiac-11-aquarius', 32, 'Aquarius', 'aquarius', 'Zodiac Signs & Houses', 'House of Reward', '', 'Precessional', 'Recessional', '', '', '', ''),
('zodiac-12-pisces', 33, 'Pisces', 'pisces', 'Zodiac Signs & Houses', 'House of Reprisal', '', 'Precessional', 'Recessional', '', '', '', ''),
# ── Lunars ─────────────────────────────────────────────────────────────
# Animal Powers: was The Junkman (#12, Hanged Man) — renamed
('the-junkman', 34, 'Animal Powers',
'animal-powers', 'Lunars',
'Hanged Man', 'Patrilineage',
'Centrifugal', 'Centripetal',
'', '', '', ''),
# Seeded Earth: was King Death & the Cosmic Tree (#13, Death) — renamed
('king-death-and-the-cosmic-tree', 35, 'Seeded Earth',
'seeded-earth', 'Lunars',
'Death', 'Matrilineage',
'Centrifugal', 'Centripetal',
'', '', '', ''),
# Twins of Pluto: was Wanderer 11 King & Queen of Hades (#49) — renamed
('wanderer-11-king-queen-hades', 36, 'The Twins of Pluto',
'twins-of-pluto', 'Lunars',
'Pluto', '',
'Prograde', 'Retrograde',
'', '', '', ''),
# ── Planets ────────────────────────────────────────────────────────────
# ⚠ correspondences left blank — image unclear; user to confirm
# Reversals: user confirmed all planet cards reverse to same (blank = same)
('wanderer-10-neptune', 37, 'Neptune', 'neptune', 'Planets', 'Neptune', '', 'Prograde', 'Retrograde', '', '', '', ''),
('wanderer-9-uranus', 38, 'Uranus', 'uranus', 'Planets', 'Uranus', '', 'Prograde', 'Retrograde', '', '', '', ''),
('wanderer-8-saturn', 39, 'Saturn', 'saturn', 'Planets', 'Saturn', '', 'Prograde', 'Retrograde', '', '', '', ''),
('wanderer-7-jupiter', 40, 'Jupiter', 'jupiter', 'Planets', 'Jupiter', '', 'Prograde', 'Retrograde', '', '', '', ''),
# #41 The Asteroid Belt — INSERT separately below
('wanderer-6-mars', 42, 'Mars', 'mars', 'Planets', 'Mars', '', 'Prograde', 'Retrograde', '', '', '', ''),
('wanderer-5-venus', 43, 'Venus', 'venus', 'Planets', 'Venus', '', 'Prograde', 'Retrograde', '', '', '', ''),
('wanderer-4-mercury', 44, 'Mercury', 'mercury', 'Planets', 'Mercury', '', 'Prograde', 'Retrograde', '', '', '', ''),
# ── Inner Rings ────────────────────────────────────────────────────────
# ⚠ Polestar levity/gravity qualifiers not confirmed from image
('wanderer-1-polestar', 45, 'The Polestar', 'the-polestar', 'Inner Rings', 'Star', '', '', '', '', '', '', ''),
('wanderer-2-antichthon',46, 'The Antichthon', 'the-antichthon', 'Inner Rings', 'Moon', '', 'Waxing', 'Waning', '', '', '', ''),
# Corestar: user changed Ascendant→Inclining, Descendent→Declining
('wanderer-3-corestar', 47, 'The Corestar', 'the-corestar', 'Inner Rings', 'Sun', '', 'Inclining', 'Declining', '', '', '', ''),
# ── Polarity-split finals ──────────────────────────────────────────────
# 48: The Eagle → Father Sky (levity) / Mother Sea (gravity)
('the-eagle', 48, 'Father Sky / Mother Sea',
'father-sky-mother-sea', '',
'World', '',
'', '',
'Father Sky', 'Mother Sea',
'The Storm', 'The Flood'),
# 49: The Mould of Man → Effulgent Mould of Man (levity) / Devouring Eagle (gravity)
('the-mould-of-man', 49, 'The Effulgent Mould of Man / The Devouring Eagle',
'effulgent-mould-devouring-eagle', '',
'Trumpets', '',
'', '',
'The Effulgent Mould of Man', 'The Devouring Eagle',
'', ''),
]
_DELETE_SLUGS = [
'wheel-of-fortune-em', # old #10 — correspondence absorbed by Fantasia Celestia
'the-junkboat', # old #11 — Space element takes the Chariot correspondence
'the-great-hunt', # old #14 — Disco Inferno takes the Devil correspondence
]
def reseed_earthman_majors(apps, schema_editor):
TarotCard = apps.get_model('epic', 'TarotCard')
DeckVariant = apps.get_model('epic', 'DeckVariant')
try:
earthman = DeckVariant.objects.get(slug='earthman')
except DeckVariant.DoesNotExist:
return # no-op in fresh DB without seeded variants
# 1. Delete retired cards
TarotCard.objects.filter(deck_variant=earthman, slug__in=_DELETE_SLUGS).delete()
# 2. Update existing cards — set new number to a temporary negative to
# avoid any incidental ordering conflicts mid-loop, then finalise.
# (No unique constraint on number, so straightforward.)
for row in _UPDATES:
(cur_slug, new_num, new_name, new_slug, new_group,
corr, reversal, lq, gq, le, ge, lr, gr) = row
TarotCard.objects.filter(
deck_variant=earthman, slug=cur_slug,
).update(
number=new_num,
name=new_name,
slug=new_slug,
group=new_group,
correspondence=corr,
reversal=reversal,
levity_qualifier=lq,
gravity_qualifier=gq,
levity_emanation=le,
gravity_emanation=ge,
levity_reversal=lr,
gravity_reversal=gr,
)
# 3. Insert The Asteroid Belt (new card, no existing equivalent)
TarotCard.objects.get_or_create(
deck_variant=earthman,
slug='the-asteroid-belt',
defaults=dict(
number=41,
name='The Asteroid Belt',
arcana='MAJOR',
group='Planets',
correspondence='',
reversal='Ouroboros',
levity_qualifier='Prograde',
gravity_qualifier='Retrograde',
),
)
def reverse_reseed(apps, schema_editor):
pass # irreversible structural change
class Migration(migrations.Migration):
dependencies = [
('epic', '0035_earthman_deck_new_fields'),
]
operations = [
migrations.RunPython(reseed_earthman_majors, reverse_reseed),
]

View File

@@ -1,24 +0,0 @@
from django.db import migrations
def set_polestar_qualifiers(apps, schema_editor):
TarotCard = apps.get_model('epic', 'TarotCard')
DeckVariant = apps.get_model('epic', 'DeckVariant')
try:
earthman = DeckVariant.objects.get(slug='earthman')
except DeckVariant.DoesNotExist:
return
TarotCard.objects.filter(
deck_variant=earthman, slug='the-polestar',
).update(levity_qualifier='Precessional', gravity_qualifier='Recessional')
class Migration(migrations.Migration):
dependencies = [
('epic', '0036_earthman_deck_reseed'),
]
operations = [
migrations.RunPython(set_polestar_qualifiers, migrations.RunPython.noop),
]

View File

@@ -1,60 +0,0 @@
"""
Fix TarotCard.correspondence values for Earthman major arcana to store
the Fiorentine/Tarot card name (1:1 correspondence) rather than Earthman
conceptual terms.
Two batches corrected:
1. Pope/Horseman (1-5): Territoriality/Despotism/Capitalism/Fascism
→ Magician/High Priestess/Empress/Emperor/Hierophant
2. Zodiac (22-33): House of Self/Worth/… → Aries/Taurus/…/Pisces
(Minchiate Fiorentine has all 12 zodiac sign cards; correspondence = sign name)
"""
from django.db import migrations
_POPE_FIXES = {
'the-schizo': 'Magician',
'the-occultist': 'High Priestess',
'the-despot': 'Empress',
'the-capitalist': 'Emperor',
'the-fascist': 'Hierophant',
}
_ZODIAC_FIXES = {
'aries': 'Aries',
'taurus': 'Taurus',
'gemini': 'Gemini',
'cancer': 'Cancer',
'leo': 'Leo',
'virgo': 'Virgo',
'libra': 'Libra',
'scorpio': 'Scorpio',
'sagittarius': 'Sagittarius',
'capricorn': 'Capricorn',
'aquarius': 'Aquarius',
'pisces': 'Pisces',
}
def fix_correspondences(apps, schema_editor):
TarotCard = apps.get_model('epic', 'TarotCard')
DeckVariant = apps.get_model('epic', 'DeckVariant')
try:
earthman = DeckVariant.objects.get(slug='earthman')
except DeckVariant.DoesNotExist:
return
for slug, corr in {**_POPE_FIXES, **_ZODIAC_FIXES}.items():
TarotCard.objects.filter(deck_variant=earthman, slug=slug).update(
correspondence=corr,
)
class Migration(migrations.Migration):
dependencies = [
('epic', '0037_polestar_qualifiers'),
]
operations = [
migrations.RunPython(fix_correspondences, migrations.RunPython.noop),
]

View File

@@ -1,59 +0,0 @@
"""
Fix TarotCard.reversal values for Pope/Horseman and Zodiac cards.
Pope/Horseman (1-5): reversed-state names read from image reversal column.
⚠ Cards 1-2 (Schizo/Occultist): image showed 'Territoriality*' spanning
rows 1-2 — stored as 'Territoriality' for both; user to confirm if
they have distinct reversed names.
Zodiac (22-33): reversed state = corresponding house (e.g. Aries → House of Self).
"""
from django.db import migrations
_POPE_REVERSALS = {
'the-schizo': 'Territoriality', # ⚠ confirm if distinct from Occultist
'the-occultist': 'Territoriality', # ⚠ confirm
'the-despot': 'Despotism',
'the-capitalist': 'Capitalism',
'the-fascist': 'Fascism',
}
_ZODIAC_REVERSALS = {
'aries': 'House of Self',
'taurus': 'House of Worth',
'gemini': 'House of Education',
'cancer': 'House of Family',
'leo': 'House of Creation',
'virgo': 'House of Ritual',
'libra': 'House of Cooperation',
'scorpio': 'House of Regeneration',
'sagittarius': 'House of Enterprise',
'capricorn': 'House of Career',
'aquarius': 'House of Reward',
'pisces': 'House of Reprisal',
}
def fix_reversals(apps, schema_editor):
TarotCard = apps.get_model('epic', 'TarotCard')
DeckVariant = apps.get_model('epic', 'DeckVariant')
try:
earthman = DeckVariant.objects.get(slug='earthman')
except DeckVariant.DoesNotExist:
return
for slug, reversal in {**_POPE_REVERSALS, **_ZODIAC_REVERSALS}.items():
TarotCard.objects.filter(
deck_variant=earthman, slug=slug,
).update(reversal=reversal)
class Migration(migrations.Migration):
dependencies = [
('epic', '0038_fix_correspondences'),
]
operations = [
migrations.RunPython(fix_reversals, migrations.RunPython.noop),
]

View File

@@ -1,24 +0,0 @@
from django.db import migrations
def rename_jovent_to_intent(apps, schema_editor):
TarotCard = apps.get_model('epic', 'TarotCard')
DeckVariant = apps.get_model('epic', 'DeckVariant')
try:
earthman = DeckVariant.objects.get(slug='earthman')
except DeckVariant.DoesNotExist:
return
TarotCard.objects.filter(
deck_variant=earthman, slug='jovent',
).update(name='Intent', slug='intent')
class Migration(migrations.Migration):
dependencies = [
('epic', '0039_fix_reversals'),
]
operations = [
migrations.RunPython(rename_jovent_to_intent, migrations.RunPython.noop),
]

View File

@@ -75,13 +75,15 @@ const NatusWheel = (() => {
const CLASSIC_ELEMENTS = new Set(['Fire', 'Stone', 'Air', 'Water']); const CLASSIC_ELEMENTS = new Set(['Fire', 'Stone', 'Air', 'Water']);
const ASPECT_SYMBOLS = { const ASPECT_SYMBOLS = {
Conjunction: '☌', Conjunction: '☌',
Semisextile: '⚺', Semisextile: '⚺',
Sextile: '', Semisquare: '',
Square: '', Sextile: '',
Trine: '', Square: '',
Quincunx: '', Trine: '',
Opposition: '', Sesquiquadrate: '',
Quincunx: '⚻',
Opposition: '☍',
}; };
const APPLY_SYM = '⇥'; // →| applying (converging toward exact) const APPLY_SYM = '⇥'; // →| applying (converging toward exact)
@@ -89,13 +91,15 @@ const NatusWheel = (() => {
// SVG stroke-dasharray and width per aspect type — color comes from applying planet. // SVG stroke-dasharray and width per aspect type — color comes from applying planet.
const ASPECT_STYLES = { const ASPECT_STYLES = {
Conjunction: { dash: 'none', width: 1.2 }, Conjunction: { dash: 'none', width: 1.2 },
Semisextile: { dash: '1 4', width: 0.6 }, Semisextile: { dash: '1 4', width: 0.6 },
Sextile: { dash: '6 4', width: 0.8 }, Semisquare: { dash: '3 4', width: 0.7 },
Square: { dash: '2 4', width: 1.2 }, Sextile: { dash: '6 4', width: 0.8 },
Trine: { dash: '12 4', width: 0.8 }, Square: { dash: '2 4', width: 1.2 },
Quincunx: { dash: '12 4 2 4', width: 0.8 }, Trine: { dash: '12 4', width: 0.8 },
Opposition: { dash: '10 4 2 4 2 4', width: 1.2 }, Sesquiquadrate: { dash: '8 4 3 4', width: 0.7 },
Quincunx: { dash: '12 4 2 4', width: 0.8 },
Opposition: { dash: '10 4 2 4 2 4', width: 1.2 },
}; };
const HOUSE_LABELS = [ const HOUSE_LABELS = [
@@ -108,16 +112,47 @@ const NatusWheel = (() => {
MC: { label: 'Midheaven', sym: 'MC', house: 10 }, MC: { label: 'Midheaven', sym: 'MC', house: 10 },
}; };
// Major aspect angles and their exact expected separations // All aspect angles — used for client-side angle (ASC/MC) aspect detection.
const MAJOR_ASPECTS = [ const ALL_ASPECTS = [
{ type: 'Conjunction', angle: 0 }, { type: 'Conjunction', angle: 0 },
{ type: 'Sextile', angle: 60 }, { type: 'Semisextile', angle: 30 },
{ type: 'Square', angle: 90 }, { type: 'Semisquare', angle: 45 },
{ type: 'Trine', angle: 120 }, { type: 'Sextile', angle: 60 },
{ type: 'Opposition', angle: 180 }, { type: 'Square', angle: 90 },
{ type: 'Trine', angle: 120 },
{ type: 'Sesquiquadrate', angle: 135 },
{ type: 'Quincunx', angle: 150 },
{ type: 'Opposition', angle: 180 },
]; ];
const ANGLE_ORB = 10; // Ott's Orb: per-body allowed orb; angle points (ASC/MC) use ANGLE_BODY_ORB.
// Allowed orb for a pair = (orbOtt_body1 + orbOtt_body2) / 2.
const PLANET_ORB = {
Sun: 10, Moon: 10,
Mercury: 8, Venus: 8, Mars: 8,
Jupiter: 6, Saturn: 6,
Uranus: 4, Neptune: 4, Pluto: 4,
};
const ANGLE_BODY_ORB = 10;
// Aspect Values for intensity scoring: A.V. × (1 actual_orb / ott_orb)
const ASPECT_AV = {
Conjunction: 12, Opposition: 6, Trine: 4, Square: 3, Sextile: 2,
Sesquiquadrate: 1.5, Semisquare: 1.5, Quincunx: 1, Semisextile: 1,
};
// Tie-break 2: partner body importance (lower = more important)
const BODY_IMPORTANCE = {
Sun: 0, Moon: 1, ASC: 2, MC: 3,
Mercury: 4, Venus: 5, Mars: 6,
Jupiter: 7, Saturn: 8, Uranus: 9, Neptune: 10, Pluto: 11,
};
// Tie-break 3: aspect importance (lower = more important)
const ASPECT_IMPORTANCE = {
Conjunction: 0, Opposition: 1, Trine: 2, Square: 3, Sextile: 4,
Sesquiquadrate: 5, Semisquare: 6, Quincunx: 7, Semisextile: 8,
};
// Cardinal / Fixed / Mutable — parallel index to SIGNS // Cardinal / Fixed / Mutable — parallel index to SIGNS
const SIGN_MODALITIES = [ const SIGN_MODALITIES = [
@@ -287,6 +322,37 @@ const NatusWheel = (() => {
return `<img src="${src}" class="tt-el-vec" width="1em" height="1em" alt="${info.classical}" aria-hidden="true">`; return `<img src="${src}" class="tt-el-vec" width="1em" height="1em" alt="${info.classical}" aria-hidden="true">`;
} }
/** Average of two bodies' Ott orbs — the allowed orb for their aspects. */
function _ottOrb(body1, body2) {
const o1 = PLANET_ORB[body1] ?? ANGLE_BODY_ORB;
const o2 = PLANET_ORB[body2] ?? ANGLE_BODY_ORB;
return (o1 + o2) / 2;
}
/**
* Sort an aspect list for display: descending intensity = A.V. × (1 orb/ottOrb).
* Ties resolved by: separating before applying → partner importance → aspect importance.
*/
function _sortAspects(aspects, thisBody) {
return aspects.filter(a => a.orb <= _ottOrb(thisBody, a.partner)).sort((a, b) => {
const ottA = _ottOrb(thisBody, a.partner);
const ottB = _ottOrb(thisBody, b.partner);
const intA = (ASPECT_AV[a.type] ?? 1) * (1 - a.orb / ottA);
const intB = (ASPECT_AV[b.type] ?? 1) * (1 - b.orb / ottB);
if (Math.abs(intA - intB) > 1e-9) return intB - intA;
// tie-break 1: separating (applying_planet !== thisBody) before applying
const aApplying = a.applying_planet === thisBody ? 1 : 0;
const bApplying = b.applying_planet === thisBody ? 1 : 0;
if (aApplying !== bApplying) return aApplying - bApplying;
// tie-break 2: partner body importance
const impA = BODY_IMPORTANCE[a.partner] ?? 99;
const impB = BODY_IMPORTANCE[b.partner] ?? 99;
if (impA !== impB) return impA - impB;
// tie-break 3: aspect importance
return (ASPECT_IMPORTANCE[a.type] ?? 99) - (ASPECT_IMPORTANCE[b.type] ?? 99);
});
}
/** CSS color string for an aspect line, keyed to the applying planet. */ /** CSS color string for an aspect line, keyed to the applying planet. */
function _aspectColor(applying_planet) { function _aspectColor(applying_planet) {
const code = PLANET_ELEMENTS[applying_planet]; const code = PLANET_ELEMENTS[applying_planet];
@@ -446,7 +512,7 @@ const NatusWheel = (() => {
const elKey = (sign.element || '').toLowerCase(); const elKey = (sign.element || '').toLowerCase();
let aspectHtml = ''; let aspectHtml = '';
const myAspects = _aspectIndex[angleName] || []; const myAspects = _sortAspects(_aspectIndex[angleName] || [], angleName);
if (myAspects.length) { if (myAspects.length) {
aspectHtml = '<small class="tt-aspects">'; aspectHtml = '<small class="tt-aspects">';
myAspects.forEach(({ partner, type, orb, applying_planet }) => { myAspects.forEach(({ partner, type, orb, applying_planet }) => {
@@ -564,7 +630,7 @@ const NatusWheel = (() => {
// Aspect list — always shown in small font; lines on SVG toggled separately. // Aspect list — always shown in small font; lines on SVG toggled separately.
let aspectHtml = ''; let aspectHtml = '';
const myAspects = _aspectIndex[item.name] || []; const myAspects = _sortAspects(_aspectIndex[item.name] || [], item.name);
if (myAspects.length) { if (myAspects.length) {
aspectHtml = '<small class="tt-aspects">'; aspectHtml = '<small class="tt-aspects">';
myAspects.forEach(({ partner, type, orb, applying_planet }) => { myAspects.forEach(({ partner, type, orb, applying_planet }) => {
@@ -1230,13 +1296,14 @@ const NatusWheel = (() => {
* Build the aspect index (planet → aspects list) and create the persistent * Build the aspect index (planet → aspects list) and create the persistent
* nw-aspects group. Lines are drawn per-planet on click, not here. * nw-aspects group. Lines are drawn per-planet on click, not here.
*/ */
/** Compute aspect type + orb between an angle (deg) and a planet (deg), or null. */ /** Compute aspect type + orb between an angle (ASC/MC) and a planet, or null. */
function _angleAspectWith(angleDeg, planetDeg) { function _angleAspectWith(angleDeg, planetDeg, planetName) {
let diff = Math.abs(planetDeg - angleDeg) % 360; let diff = Math.abs(planetDeg - angleDeg) % 360;
if (diff > 180) diff = 360 - diff; if (diff > 180) diff = 360 - diff;
for (const { type, angle } of MAJOR_ASPECTS) { const maxOrb = _ottOrb(planetName, 'ASC');
for (const { type, angle } of ALL_ASPECTS) {
const orb = Math.abs(diff - angle); const orb = Math.abs(diff - angle);
if (orb <= ANGLE_ORB) return { type, orb: +orb.toFixed(1) }; if (orb <= maxOrb) return { type, orb: +orb.toFixed(1) };
} }
return null; return null;
} }
@@ -1257,7 +1324,7 @@ const NatusWheel = (() => {
const angleDeg = angleName === 'ASC' ? data.houses.asc : data.houses.mc; const angleDeg = angleName === 'ASC' ? data.houses.asc : data.houses.mc;
_aspectIndex[angleName] = []; _aspectIndex[angleName] = [];
Object.entries(data.planets).forEach(([pname, pdata]) => { Object.entries(data.planets).forEach(([pname, pdata]) => {
const asp = _angleAspectWith(angleDeg, pdata.degree); const asp = _angleAspectWith(angleDeg, pdata.degree, pname);
if (!asp) return; if (!asp) return;
const shared = { type: asp.type, orb: asp.orb, applying_planet: pname }; const shared = { type: asp.type, orb: asp.orb, applying_planet: pname };
_aspectIndex[angleName].push({ partner: pname, ...shared }); _aspectIndex[angleName].push({ partner: pname, ...shared });

View File

@@ -152,7 +152,8 @@ def tarot_fan(request, deck_id):
deck = get_object_or_404(DeckVariant, pk=deck_id) deck = get_object_or_404(DeckVariant, pk=deck_id)
if not request.user.unlocked_decks.filter(pk=deck_id).exists(): if not request.user.unlocked_decks.filter(pk=deck_id).exists():
return HttpResponse(status=403) return HttpResponse(status=403)
_suit_order = {"WANDS": 0, "CUPS": 1, "SWORDS": 2, "PENTACLES": 3, "CROWNS": 3, "COINS": 4} _suit_order = {"BRANDS": 0, "GRAILS": 1, "BLADES": 2, "CROWNS": 3,
"WANDS": 0, "CUPS": 1, "SWORDS": 2, "PENTACLES": 3}
cards = sorted( cards = sorted(
TarotCard.objects.filter(deck_variant=deck), TarotCard.objects.filter(deck_variant=deck),
key=lambda c: (0 if c.arcana == "MAJOR" else 1, _suit_order.get(c.suit or "", 9), c.number), key=lambda c: (0 if c.arcana == "MAJOR" else 1, _suit_order.get(c.suit or "", 9), c.number),

View File

@@ -1,6 +1,8 @@
# Generated by Django 6.0 on 2026-02-20 00:48 # Generated by Django 6.0 on 2026-04-28 00:59
import django.db.models.deletion
import uuid import uuid
from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@@ -9,9 +11,18 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('drama', '0001_initial'),
('epic', '0001_initial'),
] ]
operations = [ operations = [
migrations.CreateModel(
name='LoginToken',
fields=[
('email', models.EmailField(max_length=254)),
('uid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
],
),
migrations.CreateModel( migrations.CreateModel(
name='User', name='User',
fields=[ fields=[
@@ -19,18 +30,63 @@ class Migration(migrations.Migration):
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('email', models.EmailField(max_length=254, unique=True)), ('email', models.EmailField(max_length=254, unique=True)),
('username', models.CharField(blank=True, max_length=35, null=True, unique=True)),
('searchable', models.BooleanField(default=False)),
('palette', models.CharField(default='palette-default', max_length=32)),
('stripe_customer_id', models.CharField(blank=True, max_length=255, null=True)),
('ap_public_key', models.TextField(blank=True, default='')),
('ap_private_key', models.TextField(blank=True, default='')),
('sky_birth_dt', models.DateTimeField(blank=True, null=True)),
('sky_birth_lat', models.DecimalField(blank=True, decimal_places=4, max_digits=9, null=True)),
('sky_birth_lon', models.DecimalField(blank=True, decimal_places=4, max_digits=9, null=True)),
('sky_birth_place', models.CharField(blank=True, max_length=255)),
('sky_birth_tz', models.CharField(blank=True, max_length=64)),
('sky_house_system', models.CharField(blank=True, default='O', max_length=1)),
('sky_chart_data', models.JSONField(blank=True, null=True)),
('is_staff', models.BooleanField(default=False)), ('is_staff', models.BooleanField(default=False)),
('is_superuser', models.BooleanField(default=False)), ('is_superuser', models.BooleanField(default=False)),
('active_title', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='drama.note')),
('equipped_deck', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='epic.deckvariant')),
('unlocked_decks', models.ManyToManyField(blank=True, related_name='unlocked_by', to='epic.deckvariant')),
], ],
options={ options={
'abstract': False, 'abstract': False,
}, },
), ),
migrations.CreateModel(
name='PaymentMethod',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('stripe_pm_id', models.CharField(max_length=255)),
('last4', models.CharField(max_length=4)),
('brand', models.CharField(max_length=32)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payment_methods', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel( migrations.CreateModel(
name='Token', name='Token',
fields=[ fields=[
('email', models.EmailField(max_length=254)), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('token_type', models.CharField(choices=[('coin', 'Coin-on-a-String'), ('Free', 'Free Token'), ('tithe', 'Tithe Token'), ('pass', 'Backstage Pass'), ('carte', 'Carte Blanche')], max_length=8)),
('expires_at', models.DateTimeField(blank=True, null=True)),
('next_ready_at', models.DateTimeField(blank=True, null=True)),
('slots_claimed', models.PositiveSmallIntegerField(blank=True, default=0)),
('current_room', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='coin_tokens', to='epic.room')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tokens', to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='user',
name='equipped_trinket',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='lyric.token'),
),
migrations.CreateModel(
name='Wallet',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('writs', models.IntegerField(default=0)),
('esteem', models.IntegerField(default=0)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='wallet', to=settings.AUTH_USER_MODEL)),
], ],
), ),
] ]

View File

@@ -1,23 +0,0 @@
# Generated by Django 6.0 on 2026-03-02 01:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lyric', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='user',
name='searchable',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='username',
field=models.CharField(blank=True, max_length=35, null=True, unique=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 6.0 on 2026-03-02 04:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lyric', '0002_user_searchable_user_username'),
]
operations = [
migrations.AddField(
model_name='user',
name='theme',
field=models.CharField(default='theme-default', max_length=32),
),
]

View File

@@ -1,22 +0,0 @@
# Generated by Django 6.0 on 2026-03-05 19:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lyric', '0003_user_theme'),
]
operations = [
migrations.RemoveField(
model_name='user',
name='theme',
),
migrations.AddField(
model_name='user',
name='palette',
field=models.CharField(default='palette-default', max_length=32),
),
]

View File

@@ -1,14 +0,0 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('lyric', '0004_remove_user_theme_user_palette'),
]
operations = [
migrations.RenameModel(
old_name="Token",
new_name="LoginToken",
),
]

View File

@@ -1,33 +0,0 @@
# Generated by Django 6.0 on 2026-03-08 18:19
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lyric', '0005_rename_logintoken'),
]
operations = [
migrations.CreateModel(
name='Token',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('token_type', models.CharField(choices=[('coin', 'Coin-on-a-String'), ('Free', 'Free Token'), ('tithe', 'Tithe Token')], max_length=8)),
('expires_at', models.DateTimeField(blank=True, null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tokens', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Wallet',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('writs', models.IntegerField(default=0)),
('esteem', models.IntegerField(default=0)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='wallet', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -1,30 +0,0 @@
# Generated by Django 6.0 on 2026-03-08 20:26
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lyric', '0006_token_wallet'),
]
operations = [
migrations.AddField(
model_name='user',
name='stripe_customer_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.CreateModel(
name='PaymentMethod',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('stripe_pm_id', models.CharField(max_length=255)),
('last4', models.CharField(max_length=4)),
('brand', models.CharField(max_length=32)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payment_methods', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -1,25 +0,0 @@
# Generated by Django 6.0 on 2026-03-13 20:17
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0001_initial'),
('lyric', '0007_user_stripe_customer_id_paymentmethod'),
]
operations = [
migrations.AddField(
model_name='token',
name='current_room',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='coin_tokens', to='epic.room'),
),
migrations.AddField(
model_name='token',
name='next_ready_at',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 6.0 on 2026-03-15 00:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lyric', '0008_token_current_room_token_next_ready_at'),
]
operations = [
migrations.AlterField(
model_name='token',
name='token_type',
field=models.CharField(choices=[('coin', 'Coin-on-a-String'), ('Free', 'Free Token'), ('tithe', 'Tithe Token'), ('pass', 'Backstage Pass')], max_length=8),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 6.0 on 2026-03-15 23:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lyric', '0009_alter_token_token_type'),
]
operations = [
migrations.AlterField(
model_name='token',
name='token_type',
field=models.CharField(choices=[('coin', 'Coin-on-a-String'), ('Free', 'Free Token'), ('tithe', 'Tithe Token'), ('pass', 'Backstage Pass'), ('carte', 'Carte Blanche')], max_length=8),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 6.0 on 2026-03-15 23:44
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lyric', '0010_carte_blanche_token_type'),
]
operations = [
migrations.AddField(
model_name='user',
name='equipped_trinket',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='lyric.token'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 6.0 on 2026-03-16 03:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lyric', '0011_user_equipped_trinket_fk'),
]
operations = [
migrations.AddField(
model_name='token',
name='slots_claimed',
field=models.PositiveSmallIntegerField(default=0),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 6.0 on 2026-03-17 01:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lyric', '0012_carte_slots_claimed'),
]
operations = [
migrations.AlterField(
model_name='token',
name='slots_claimed',
field=models.PositiveSmallIntegerField(blank=True, default=0),
),
]

View File

@@ -1,20 +0,0 @@
# Generated by Django 6.0 on 2026-03-25 00:48
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0009_deckvariant_alter_tarotcard_options_and_more'),
('lyric', '0013_alter_token_slots_claimed'),
]
operations = [
migrations.AddField(
model_name='user',
name='equipped_deck',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='epic.deckvariant'),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 6.0 on 2026-03-25 02:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0010_seed_deck_variants_and_earthman'),
('lyric', '0014_user_equipped_deck'),
]
operations = [
migrations.AddField(
model_name='user',
name='unlocked_decks',
field=models.ManyToManyField(blank=True, related_name='unlocked_by', to='epic.deckvariant'),
),
]

View File

@@ -1,24 +0,0 @@
from django.db import migrations
def backfill_unlocked_decks(apps, schema_editor):
User = apps.get_model("lyric", "User")
DeckVariant = apps.get_model("epic", "DeckVariant")
try:
earthman = DeckVariant.objects.get(slug="earthman")
except DeckVariant.DoesNotExist:
return
for user in User.objects.filter(unlocked_decks__isnull=True):
user.unlocked_decks.add(earthman)
class Migration(migrations.Migration):
dependencies = [
("lyric", "0015_user_unlocked_decks"),
("epic", "0010_seed_deck_variants_and_earthman"),
]
operations = [
migrations.RunPython(backfill_unlocked_decks, migrations.RunPython.noop),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 6.0 on 2026-04-02 19:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lyric', '0016_backfill_unlocked_decks'),
]
operations = [
migrations.AddField(
model_name='user',
name='ap_private_key',
field=models.TextField(blank=True, default=''),
),
migrations.AddField(
model_name='user',
name='ap_public_key',
field=models.TextField(blank=True, default=''),
),
]

View File

@@ -1,40 +0,0 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lyric', '0017_ap_keypair_fields'),
]
operations = [
migrations.AddField(
model_name='user',
name='sky_birth_dt',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='user',
name='sky_birth_lat',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=9, null=True),
),
migrations.AddField(
model_name='user',
name='sky_birth_lon',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=9, null=True),
),
migrations.AddField(
model_name='user',
name='sky_birth_place',
field=models.CharField(blank=True, max_length=255),
),
migrations.AddField(
model_name='user',
name='sky_house_system',
field=models.CharField(blank=True, default='O', max_length=1),
),
migrations.AddField(
model_name='user',
name='sky_chart_data',
field=models.JSONField(blank=True, null=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 6.0 on 2026-04-22 01:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lyric', '0018_user_sky_fields'),
]
operations = [
migrations.AddField(
model_name='user',
name='sky_birth_tz',
field=models.CharField(blank=True, max_length=64),
),
]

View File

@@ -1,20 +0,0 @@
# Generated by Django 6.0 on 2026-04-23 05:37
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('drama', '0005_rename_recognition_to_note'),
('lyric', '0019_sky_birth_tz'),
]
operations = [
migrations.AddField(
model_name='user',
name='active_title',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='drama.note'),
),
]

View File

@@ -1,4 +1,4 @@
# Generated by Django 6.0 on 2026-04-16 00:38 # Generated by Django 6.0 on 2026-04-28 00:59
from django.db import migrations, models from django.db import migrations, models

View File

@@ -89,15 +89,20 @@
.tt-title { .tt-title {
color: rgba(var(--terUser), 1); color: rgba(var(--terUser), 1);
font-size: 0.95rem; font-size: 0.95rem;
margin: 0 0 0.3rem; margin: 0 0 0.1rem;
font-weight: bold; font-weight: bold;
} }
.tt-lock { .tt-lock {
margin: 0.25rem 0 0; margin: 0.75rem 0 0.1rem;
font-size: 0.8rem; font-size: 0.8rem;
opacity: 0.85; opacity: 0.85;
i { margin-right: 0.25rem; } i { margin-right: 0.25rem; }
} }
.tt-date {
font-size: 0.75rem !important;
font-variant-caps: small-caps;
}
} }