A.0 image-rendering schema + RWS rename + canonical-Earthman suit collapse — TDD. Sprint A.0 of [[project-image-based-deck-face-rendering]]. Adds three DeckVariant fields: has_card_images (BooleanField default=True — Earthman keeps False until its artwork ships, every new deck defaults True), family (CharField choices=[earthman, italian, english, playing] default=earthman — drives per-family display + filename slug mapping per [[reference-card-image-naming-convention]]), is_polarized (BooleanField default=False — Earthman is True today; Sprint A.4 game_kit applet will render "(×2)" in --terUser for polarized decks; Sprint C+B segment model uses it for segment-count logic). TarotCard.SUIT_CHOICES collapses from 8 values to 4 canonical Earthman values (BRANDS / CROWNS / GRAILS / BLADES); WANDS / CUPS / SWORDS / PENTACLES dropped — they were duplicative at the structural level since sig_deck_cards + levity/gravity_sig_cards already treated [WANDS, BRANDS, CROWNS] as one segment and [SWORDS, BLADES, CUPS, GRAILS] as another (so the project already *functionally* equated them; the lock just makes that explicit). Per-family display vocab (batons for Italian, wands for English, clubs for Playing) lives in Sprint A.2's display_suit_name property, not in the enum. Audit 2026-05-25 revealed the existing fiorentine-minchiate DeckVariant is actually 78-card RWS Tarot in disguise (22 majors numbered 0-21 w. RWS names: The Fool / The Magician / ... / The World; 56 minors in 4 suits × 14 cards) — NOT Minchiate (which has 40 trumps + 1 Il Matto + 56 minors = 97 cards). Migration 0012 renames the slug → tarot-rider-waite-smith, name → "Tarot (Rider-Waite-Smith)", sets family='english', has_card_images=False, is_polarized=False — and revocabs its 56 minor cards' suits in-place (WANDS→BRANDS, CUPS→GRAILS, SWORDS→BLADES, PENTACLES→CROWNS) so they match the new canonical enum. FKs (User.equipped_deck, User.unlocked_decks, TableSeat.deck_variant, etc.) survive untouched — slug-only changes don't break referential integrity. Earthman fields set explicitly in 0012 too (family=earthman, has_card_images=False, is_polarized=True). Companion code simplifications: sig_deck_cards + _sig_unique_cards_for_deck queries shrink from suit__in=[3 values] and [4 values] to [2 values] each (one per segment); TarotCard.suit_icon mapping shrinks from 8 entries to 4; gameboard.views.tarot_fan._suit_order shrinks from 8 keys to 4. Existing test files updated: test_game_room_tray.py (largest update — self.fiorentine → self.rws, id_kit_fiorentine_deck → id_kit_tarot_deck (template-id derives from deck.short_key = first slug segment), assertion "Fiorentine" → "Rider-Waite-Smith"); test_game_room_deck_contrib.py (same pattern, smaller); lyric/test_models.py + gameboard/test_views.py (slug literal swaps only); epic/test_models.py _make_sig_card test fixtures: "WANDS"→"BRANDS", "CUPS"→"GRAILS". 14 new ITs in DeckSchemaA0Test cover the schema additions + migration outcomes (field existence + choice values + earthman has all three fields set correctly + RWS rename verified + RWS cards use canonical suits + dropped enum values absent from SUIT_CHOICES). Tests: 14 new green; 1255/1255 IT+UT total green (38s); no regressions. Out of scope: Sprint A.1 will seed the actual Minchiate Fiorentine 1860-1890 (97-card) DeckVariant + TarotCard rows w. family='italian', has_card_images=True; A.2 adds the image_filename + display_suit_name properties that consume the new family field; A.3+ wires the render branches across 6 surfaces
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 6.0 on 2026-05-25 03:15
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('epic', '0010_set_reversal_drops_qualifier_realms'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='deckvariant',
|
||||||
|
name='family',
|
||||||
|
field=models.CharField(choices=[('earthman', 'Earthman'), ('italian', 'Italian / Minchiate'), ('english', 'English Tarot'), ('playing', 'Playing card')], default='earthman', max_length=10),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='deckvariant',
|
||||||
|
name='has_card_images',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='deckvariant',
|
||||||
|
name='is_polarized',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tarotcard',
|
||||||
|
name='suit',
|
||||||
|
field=models.CharField(blank=True, choices=[('BRANDS', 'Brands'), ('CROWNS', 'Crowns'), ('GRAILS', 'Grails'), ('BLADES', 'Blades')], max_length=10, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
70
src/apps/epic/migrations/0012_rws_rename_and_suit_revocab.py
Normal file
70
src/apps/epic/migrations/0012_rws_rename_and_suit_revocab.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
"""Sprint A.0 data migration.
|
||||||
|
|
||||||
|
The existing `fiorentine-minchiate` DeckVariant is actually 78-card Rider-Waite-Smith
|
||||||
|
Tarot (22 majors numbered 0-21 with RWS names + 56 minors in WANDS/CUPS/SWORDS/PENTACLES).
|
||||||
|
Rename to its true identity, set the new schema fields (family / has_card_images /
|
||||||
|
is_polarized), and revocab card suits to the canonical Earthman vocabulary that
|
||||||
|
SUIT_CHOICES now requires. Earthman gets its new-field values set too. FKs remain
|
||||||
|
intact since slug-only changes don't break referential integrity.
|
||||||
|
|
||||||
|
The actual Minchiate Fiorentine deck (97 cards, 1860-1890 publication, image set
|
||||||
|
already imported per [[reference-card-image-naming-convention]]) is seeded in
|
||||||
|
Sprint A.1's follow-up migration.
|
||||||
|
"""
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
SUIT_REVOCAB = {
|
||||||
|
"WANDS": "BRANDS",
|
||||||
|
"CUPS": "GRAILS",
|
||||||
|
"SWORDS": "BLADES",
|
||||||
|
"PENTACLES": "CROWNS",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def forward(apps, schema_editor):
|
||||||
|
DeckVariant = apps.get_model("epic", "DeckVariant")
|
||||||
|
TarotCard = apps.get_model("epic", "TarotCard")
|
||||||
|
|
||||||
|
# Earthman: set new-field values explicitly (default=True for has_card_images
|
||||||
|
# would have left it True after the schema migration — wrong for Earthman).
|
||||||
|
DeckVariant.objects.filter(slug="earthman").update(
|
||||||
|
family="earthman", has_card_images=False, is_polarized=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# fiorentine-minchiate → RWS Tarot rename + new-field values.
|
||||||
|
rws = DeckVariant.objects.filter(slug="fiorentine-minchiate").first()
|
||||||
|
if rws:
|
||||||
|
rws.slug = "tarot-rider-waite-smith"
|
||||||
|
rws.name = "Tarot (Rider-Waite-Smith)"
|
||||||
|
rws.family = "english"
|
||||||
|
rws.has_card_images = False
|
||||||
|
rws.is_polarized = False
|
||||||
|
rws.save()
|
||||||
|
# Revocab the 56 minor cards' suits to canonical Earthman vocab.
|
||||||
|
for old_suit, new_suit in SUIT_REVOCAB.items():
|
||||||
|
TarotCard.objects.filter(deck_variant=rws, suit=old_suit).update(
|
||||||
|
suit=new_suit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def backward(apps, schema_editor):
|
||||||
|
DeckVariant = apps.get_model("epic", "DeckVariant")
|
||||||
|
TarotCard = apps.get_model("epic", "TarotCard")
|
||||||
|
|
||||||
|
rws = DeckVariant.objects.filter(slug="tarot-rider-waite-smith").first()
|
||||||
|
if rws:
|
||||||
|
for new_suit, old_suit in {v: k for k, v in SUIT_REVOCAB.items()}.items():
|
||||||
|
TarotCard.objects.filter(deck_variant=rws, suit=new_suit).update(
|
||||||
|
suit=old_suit,
|
||||||
|
)
|
||||||
|
rws.slug = "fiorentine-minchiate"
|
||||||
|
rws.name = "Fiorentine Minchiate"
|
||||||
|
rws.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epic", "0011_deckvariant_family_deckvariant_has_card_images_and_more"),
|
||||||
|
]
|
||||||
|
operations = [migrations.RunPython(forward, backward)]
|
||||||
@@ -221,13 +221,27 @@ class TableSeat(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class DeckVariant(models.Model):
|
class DeckVariant(models.Model):
|
||||||
"""A named deck variant, e.g. Earthman (108 cards) or Fiorentine Minchiate (78 cards)."""
|
"""A named deck variant, e.g. Earthman or Tarot (Rider-Waite-Smith)."""
|
||||||
|
|
||||||
|
EARTHMAN = "earthman"
|
||||||
|
ITALIAN = "italian"
|
||||||
|
ENGLISH = "english"
|
||||||
|
PLAYING = "playing"
|
||||||
|
FAMILY_CHOICES = [
|
||||||
|
(EARTHMAN, "Earthman"),
|
||||||
|
(ITALIAN, "Italian / Minchiate"),
|
||||||
|
(ENGLISH, "English Tarot"),
|
||||||
|
(PLAYING, "Playing card"),
|
||||||
|
]
|
||||||
|
|
||||||
name = models.CharField(max_length=100, unique=True)
|
name = models.CharField(max_length=100, unique=True)
|
||||||
slug = models.SlugField(unique=True)
|
slug = models.SlugField(unique=True)
|
||||||
card_count = models.IntegerField()
|
card_count = models.IntegerField()
|
||||||
description = models.TextField(blank=True)
|
description = models.TextField(blank=True)
|
||||||
is_default = models.BooleanField(default=False)
|
is_default = models.BooleanField(default=False)
|
||||||
|
family = models.CharField(max_length=10, choices=FAMILY_CHOICES, default=EARTHMAN)
|
||||||
|
has_card_images = models.BooleanField(default=True)
|
||||||
|
is_polarized = models.BooleanField(default=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def short_key(self):
|
def short_key(self):
|
||||||
@@ -248,23 +262,18 @@ class TarotCard(models.Model):
|
|||||||
(MIDDLE, "Middle Arcana"),
|
(MIDDLE, "Middle Arcana"),
|
||||||
]
|
]
|
||||||
|
|
||||||
WANDS = "WANDS"
|
# Canonical SUIT_CHOICES = Earthman vocabulary (2026-05-25 lock).
|
||||||
CUPS = "CUPS"
|
# Per-family display + filename slug mapping lives in image_filename /
|
||||||
SWORDS = "SWORDS"
|
# display_suit_name properties driven by DeckVariant.family.
|
||||||
PENTACLES = "PENTACLES" # Fiorentine 4th suit
|
BRANDS = "BRANDS"
|
||||||
CROWNS = "CROWNS" # Earthman 4th suit
|
CROWNS = "CROWNS"
|
||||||
BRANDS = "BRANDS" # Earthman Wands
|
GRAILS = "GRAILS"
|
||||||
GRAILS = "GRAILS" # Earthman Cups
|
BLADES = "BLADES"
|
||||||
BLADES = "BLADES" # Earthman Swords
|
|
||||||
SUIT_CHOICES = [
|
SUIT_CHOICES = [
|
||||||
(WANDS, "Wands"),
|
(BRANDS, "Brands"),
|
||||||
(CUPS, "Cups"),
|
(CROWNS, "Crowns"),
|
||||||
(SWORDS, "Swords"),
|
(GRAILS, "Grails"),
|
||||||
(PENTACLES, "Pentacles"),
|
(BLADES, "Blades"),
|
||||||
(CROWNS, "Crowns"),
|
|
||||||
(BRANDS, "Brands"),
|
|
||||||
(GRAILS, "Grails"),
|
|
||||||
(BLADES, "Blades"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
deck_variant = models.ForeignKey(
|
deck_variant = models.ForeignKey(
|
||||||
@@ -437,14 +446,10 @@ class TarotCard(models.Model):
|
|||||||
if self.arcana == self.MAJOR:
|
if self.arcana == self.MAJOR:
|
||||||
return ''
|
return ''
|
||||||
return {
|
return {
|
||||||
self.WANDS: 'fa-wand-sparkles',
|
self.BRANDS: 'fa-wand-sparkles',
|
||||||
self.CUPS: 'fa-trophy',
|
self.CROWNS: 'fa-crown',
|
||||||
self.SWORDS: 'fa-gun',
|
self.GRAILS: 'fa-trophy',
|
||||||
self.PENTACLES: 'fa-star',
|
self.BLADES: 'fa-gun',
|
||||||
self.CROWNS: 'fa-crown',
|
|
||||||
self.BRANDS: 'fa-wand-sparkles',
|
|
||||||
self.GRAILS: 'fa-trophy',
|
|
||||||
self.BLADES: 'fa-gun',
|
|
||||||
}.get(self.suit, '')
|
}.get(self.suit, '')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -558,32 +563,12 @@ def _room_deck_variant(room):
|
|||||||
def sig_deck_cards(room):
|
def sig_deck_cards(room):
|
||||||
"""Return 36 TarotCard objects forming the Significator deck (18 unique × 2).
|
"""Return 36 TarotCard objects forming the Significator deck (18 unique × 2).
|
||||||
|
|
||||||
PC/BC pair → BRANDS/WANDS + CROWNS Middle Arcana court cards (11–14): 8 unique
|
PC/BC pair → BRANDS + CROWNS Middle Arcana court cards (11–14): 8 unique
|
||||||
SC/AC pair → BLADES/SWORDS + GRAILS/CUPS Middle Arcana court cards (11–14): 8 unique
|
SC/AC pair → BLADES + GRAILS Middle Arcana court cards (11–14): 8 unique
|
||||||
NC/EC pair → MAJOR arcana numbers 0 and 1: 2 unique
|
NC/EC pair → MAJOR arcana numbers 0 and 1: 2 unique
|
||||||
Total: 18 unique × 2 (levity + gravity piles) = 36 cards.
|
Total: 18 unique × 2 (levity + gravity piles) = 36 cards.
|
||||||
"""
|
"""
|
||||||
deck_variant = _room_deck_variant(room)
|
unique_cards = _sig_unique_cards_for_deck(_room_deck_variant(room))
|
||||||
if deck_variant is None:
|
|
||||||
return []
|
|
||||||
wands_crowns = list(TarotCard.objects.filter(
|
|
||||||
deck_variant=deck_variant,
|
|
||||||
arcana=TarotCard.MIDDLE,
|
|
||||||
suit__in=[TarotCard.WANDS, TarotCard.BRANDS, TarotCard.CROWNS],
|
|
||||||
number__in=[11, 12, 13, 14],
|
|
||||||
))
|
|
||||||
swords_cups = list(TarotCard.objects.filter(
|
|
||||||
deck_variant=deck_variant,
|
|
||||||
arcana=TarotCard.MIDDLE,
|
|
||||||
suit__in=[TarotCard.SWORDS, TarotCard.BLADES, TarotCard.CUPS, TarotCard.GRAILS],
|
|
||||||
number__in=[11, 12, 13, 14],
|
|
||||||
))
|
|
||||||
major = list(TarotCard.objects.filter(
|
|
||||||
deck_variant=deck_variant,
|
|
||||||
arcana=TarotCard.MAJOR,
|
|
||||||
number__in=[0, 1],
|
|
||||||
))
|
|
||||||
unique_cards = wands_crowns + swords_cups + major # 18 unique
|
|
||||||
return unique_cards + unique_cards # × 2 = 36
|
return unique_cards + unique_cards # × 2 = 36
|
||||||
|
|
||||||
|
|
||||||
@@ -594,16 +579,16 @@ def _sig_unique_cards_for_deck(deck_variant):
|
|||||||
via personal_sig_cards from User.equipped_deck)."""
|
via personal_sig_cards from User.equipped_deck)."""
|
||||||
if deck_variant is None:
|
if deck_variant is None:
|
||||||
return []
|
return []
|
||||||
wands_crowns = list(TarotCard.objects.filter(
|
brands_crowns = list(TarotCard.objects.filter(
|
||||||
deck_variant=deck_variant,
|
deck_variant=deck_variant,
|
||||||
arcana=TarotCard.MIDDLE,
|
arcana=TarotCard.MIDDLE,
|
||||||
suit__in=[TarotCard.WANDS, TarotCard.BRANDS, TarotCard.CROWNS],
|
suit__in=[TarotCard.BRANDS, TarotCard.CROWNS],
|
||||||
number__in=[11, 12, 13, 14],
|
number__in=[11, 12, 13, 14],
|
||||||
))
|
))
|
||||||
swords_cups = list(TarotCard.objects.filter(
|
blades_grails = list(TarotCard.objects.filter(
|
||||||
deck_variant=deck_variant,
|
deck_variant=deck_variant,
|
||||||
arcana=TarotCard.MIDDLE,
|
arcana=TarotCard.MIDDLE,
|
||||||
suit__in=[TarotCard.SWORDS, TarotCard.BLADES, TarotCard.CUPS, TarotCard.GRAILS],
|
suit__in=[TarotCard.BLADES, TarotCard.GRAILS],
|
||||||
number__in=[11, 12, 13, 14],
|
number__in=[11, 12, 13, 14],
|
||||||
))
|
))
|
||||||
major = list(TarotCard.objects.filter(
|
major = list(TarotCard.objects.filter(
|
||||||
@@ -611,7 +596,7 @@ def _sig_unique_cards_for_deck(deck_variant):
|
|||||||
arcana=TarotCard.MAJOR,
|
arcana=TarotCard.MAJOR,
|
||||||
number__in=[0, 1],
|
number__in=[0, 1],
|
||||||
))
|
))
|
||||||
return wands_crowns + swords_cups + major
|
return brands_crowns + blades_grails + major
|
||||||
|
|
||||||
|
|
||||||
def _sig_unique_cards(room):
|
def _sig_unique_cards(room):
|
||||||
|
|||||||
@@ -521,7 +521,7 @@ class SigReservationModelTest(TestCase):
|
|||||||
)
|
)
|
||||||
self.owner = User.objects.create(email="founder@test.io")
|
self.owner = User.objects.create(email="founder@test.io")
|
||||||
self.room = Room.objects.create(name="Sig Room", owner=self.owner)
|
self.room = Room.objects.create(name="Sig Room", owner=self.owner)
|
||||||
self.card = _make_sig_card(self.earthman, "WANDS", 14)
|
self.card = _make_sig_card(self.earthman, "BRANDS", 14)
|
||||||
self.seat = TableSeat.objects.create(
|
self.seat = TableSeat.objects.create(
|
||||||
room=self.room, gamer=self.owner, slot_number=1, role="PC"
|
room=self.room, gamer=self.owner, slot_number=1, role="PC"
|
||||||
)
|
)
|
||||||
@@ -538,7 +538,7 @@ class SigReservationModelTest(TestCase):
|
|||||||
SigReservation.objects.create(
|
SigReservation.objects.create(
|
||||||
room=self.room, gamer=self.owner, card=self.card, role="PC", polarity="levity"
|
room=self.room, gamer=self.owner, card=self.card, role="PC", polarity="levity"
|
||||||
)
|
)
|
||||||
card2 = _make_sig_card(self.earthman, "CUPS", 13)
|
card2 = _make_sig_card(self.earthman, "GRAILS", 13)
|
||||||
with self.assertRaises(IntegrityError):
|
with self.assertRaises(IntegrityError):
|
||||||
SigReservation.objects.create(
|
SigReservation.objects.create(
|
||||||
room=self.room, gamer=self.owner, card=card2, role="PC", polarity="levity"
|
room=self.room, gamer=self.owner, card=card2, role="PC", polarity="levity"
|
||||||
@@ -946,3 +946,94 @@ class CharacterModelTest(TestCase):
|
|||||||
retired_at=timezone.now(),
|
retired_at=timezone.now(),
|
||||||
)
|
)
|
||||||
self.assertFalse(char.is_active)
|
self.assertFalse(char.is_active)
|
||||||
|
|
||||||
|
|
||||||
|
class DeckSchemaA0Test(TestCase):
|
||||||
|
"""Sprint A.0 — DeckVariant gains has_card_images, family, is_polarized;
|
||||||
|
SUIT_CHOICES collapses to canonical Earthman vocab; existing
|
||||||
|
fiorentine-minchiate seed (which was actually RWS Tarot in disguise)
|
||||||
|
gets renamed to tarot-rider-waite-smith with revocabbed suits."""
|
||||||
|
|
||||||
|
def test_deck_variant_has_card_images_field(self):
|
||||||
|
f = DeckVariant._meta.get_field("has_card_images")
|
||||||
|
self.assertEqual(f.get_internal_type(), "BooleanField")
|
||||||
|
|
||||||
|
def test_deck_variant_family_field_has_expected_choices(self):
|
||||||
|
f = DeckVariant._meta.get_field("family")
|
||||||
|
choice_values = {c[0] for c in f.choices}
|
||||||
|
self.assertEqual(choice_values, {"earthman", "italian", "english", "playing"})
|
||||||
|
|
||||||
|
def test_deck_variant_is_polarized_field(self):
|
||||||
|
f = DeckVariant._meta.get_field("is_polarized")
|
||||||
|
self.assertEqual(f.get_internal_type(), "BooleanField")
|
||||||
|
|
||||||
|
def test_earthman_is_polarized(self):
|
||||||
|
earthman = DeckVariant.objects.get(slug="earthman")
|
||||||
|
self.assertTrue(earthman.is_polarized)
|
||||||
|
|
||||||
|
def test_earthman_family_is_earthman(self):
|
||||||
|
earthman = DeckVariant.objects.get(slug="earthman")
|
||||||
|
self.assertEqual(earthman.family, "earthman")
|
||||||
|
|
||||||
|
def test_earthman_has_card_images_false_until_artwork_painted(self):
|
||||||
|
earthman = DeckVariant.objects.get(slug="earthman")
|
||||||
|
self.assertFalse(earthman.has_card_images)
|
||||||
|
|
||||||
|
def test_rws_deck_renamed_from_fiorentine_minchiate(self):
|
||||||
|
self.assertFalse(
|
||||||
|
DeckVariant.objects.filter(slug="fiorentine-minchiate").exists(),
|
||||||
|
"fiorentine-minchiate slug should have been renamed to tarot-rider-waite-smith",
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
DeckVariant.objects.filter(slug="tarot-rider-waite-smith").exists(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rws_deck_name_is_canonical(self):
|
||||||
|
rws = DeckVariant.objects.get(slug="tarot-rider-waite-smith")
|
||||||
|
self.assertEqual(rws.name, "Tarot (Rider-Waite-Smith)")
|
||||||
|
|
||||||
|
def test_rws_family_is_english(self):
|
||||||
|
rws = DeckVariant.objects.get(slug="tarot-rider-waite-smith")
|
||||||
|
self.assertEqual(rws.family, "english")
|
||||||
|
|
||||||
|
def test_rws_has_card_images_false(self):
|
||||||
|
rws = DeckVariant.objects.get(slug="tarot-rider-waite-smith")
|
||||||
|
self.assertFalse(rws.has_card_images)
|
||||||
|
|
||||||
|
def test_rws_is_polarized_false(self):
|
||||||
|
rws = DeckVariant.objects.get(slug="tarot-rider-waite-smith")
|
||||||
|
self.assertFalse(rws.is_polarized)
|
||||||
|
|
||||||
|
def test_rws_cards_use_canonical_earthman_suits(self):
|
||||||
|
rws = DeckVariant.objects.get(slug="tarot-rider-waite-smith")
|
||||||
|
suit_values = set(
|
||||||
|
TarotCard.objects.filter(deck_variant=rws, arcana="MINOR")
|
||||||
|
.values_list("suit", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
suit_values,
|
||||||
|
{"BRANDS", "GRAILS", "BLADES", "CROWNS"},
|
||||||
|
"RWS suits should have been revocabbed: WANDS→BRANDS, CUPS→GRAILS, "
|
||||||
|
"SWORDS→BLADES, PENTACLES→CROWNS",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rws_minor_card_count_preserved(self):
|
||||||
|
"""Revocab is a string swap on the suit field — count is invariant."""
|
||||||
|
rws = DeckVariant.objects.get(slug="tarot-rider-waite-smith")
|
||||||
|
for canonical_suit in ("BRANDS", "GRAILS", "BLADES", "CROWNS"):
|
||||||
|
self.assertEqual(
|
||||||
|
TarotCard.objects.filter(
|
||||||
|
deck_variant=rws, arcana="MINOR", suit=canonical_suit
|
||||||
|
).count(),
|
||||||
|
14,
|
||||||
|
f"{canonical_suit} should have 14 cards in RWS",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_suit_choices_dropped_english_vocab(self):
|
||||||
|
choice_values = {c[0] for c in TarotCard.SUIT_CHOICES}
|
||||||
|
self.assertEqual(choice_values, {"BRANDS", "GRAILS", "BLADES", "CROWNS"})
|
||||||
|
self.assertNotIn("WANDS", choice_values)
|
||||||
|
self.assertNotIn("CUPS", choice_values)
|
||||||
|
self.assertNotIn("SWORDS", choice_values)
|
||||||
|
self.assertNotIn("PENTACLES", choice_values)
|
||||||
|
|||||||
@@ -400,11 +400,11 @@ class GameboardDeckInUseTest(TestCase):
|
|||||||
self.assertEqual("Wildfire", el.get("data-in-use-room-name"))
|
self.assertEqual("Wildfire", el.get("data-in-use-room-name"))
|
||||||
|
|
||||||
def test_non_in_use_deck_has_normal_don(self):
|
def test_non_in_use_deck_has_normal_don(self):
|
||||||
fiorentine = DeckVariant.objects.get(slug="fiorentine-minchiate")
|
fiorentine = DeckVariant.objects.get(slug="tarot-rider-waite-smith")
|
||||||
self.user.unlocked_decks.add(fiorentine)
|
self.user.unlocked_decks.add(fiorentine)
|
||||||
response = self.client.get("/gameboard/")
|
response = self.client.get("/gameboard/")
|
||||||
parsed = lxml.html.fromstring(response.content)
|
parsed = lxml.html.fromstring(response.content)
|
||||||
[don] = parsed.cssselect("#id_kit_fiorentine_deck .btn-equip")
|
[don] = parsed.cssselect("#id_kit_tarot_deck .btn-equip")
|
||||||
self.assertNotIn("btn-disabled", don.get("class", ""))
|
self.assertNotIn("btn-disabled", don.get("class", ""))
|
||||||
|
|
||||||
|
|
||||||
@@ -697,7 +697,7 @@ class TarotFanViewTest(TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
from apps.epic.models import DeckVariant
|
from apps.epic.models import DeckVariant
|
||||||
self.earthman = DeckVariant.objects.get(slug="earthman")
|
self.earthman = DeckVariant.objects.get(slug="earthman")
|
||||||
self.fiorentine = DeckVariant.objects.get(slug="fiorentine-minchiate")
|
self.fiorentine = DeckVariant.objects.get(slug="tarot-rider-waite-smith")
|
||||||
self.user = User.objects.create(email="fan@test.io")
|
self.user = User.objects.create(email="fan@test.io")
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
|||||||
@@ -630,8 +630,7 @@ 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 = {"BRANDS": 0, "GRAILS": 1, "BLADES": 2, "CROWNS": 3,
|
_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),
|
||||||
|
|||||||
@@ -409,14 +409,14 @@ class EquippedDeckTest(TestCase):
|
|||||||
|
|
||||||
def test_fiorentine_is_not_auto_assigned_to_new_users(self):
|
def test_fiorentine_is_not_auto_assigned_to_new_users(self):
|
||||||
from apps.epic.models import DeckVariant
|
from apps.epic.models import DeckVariant
|
||||||
fiorentine = DeckVariant.objects.get(slug="fiorentine-minchiate")
|
fiorentine = DeckVariant.objects.get(slug="tarot-rider-waite-smith")
|
||||||
user = User.objects.create(email="deck2@test.io")
|
user = User.objects.create(email="deck2@test.io")
|
||||||
user.refresh_from_db()
|
user.refresh_from_db()
|
||||||
self.assertNotEqual(user.equipped_deck, fiorentine)
|
self.assertNotEqual(user.equipped_deck, fiorentine)
|
||||||
|
|
||||||
def test_equipped_deck_can_be_switched(self):
|
def test_equipped_deck_can_be_switched(self):
|
||||||
from apps.epic.models import DeckVariant
|
from apps.epic.models import DeckVariant
|
||||||
fiorentine = DeckVariant.objects.get(slug="fiorentine-minchiate")
|
fiorentine = DeckVariant.objects.get(slug="tarot-rider-waite-smith")
|
||||||
user = User.objects.create(email="deck3@test.io")
|
user = User.objects.create(email="deck3@test.io")
|
||||||
user.equipped_deck = fiorentine
|
user.equipped_deck = fiorentine
|
||||||
user.save(update_fields=["equipped_deck"])
|
user.save(update_fields=["equipped_deck"])
|
||||||
|
|||||||
@@ -182,16 +182,16 @@ class DeckInUseGameKitTest(FunctionalTest):
|
|||||||
def test_non_contributing_deck_has_normal_don_doff(self):
|
def test_non_contributing_deck_has_normal_don_doff(self):
|
||||||
"""A deck not assigned to any active seat shows the normal DON/DOFF apparatus."""
|
"""A deck not assigned to any active seat shows the normal DON/DOFF apparatus."""
|
||||||
gamer, earthman, room, seat = self._setup_in_use_deck()
|
gamer, earthman, room, seat = self._setup_in_use_deck()
|
||||||
# Unlock Fiorentine for the gamer so it appears in Game Kit
|
# Unlock RWS for the gamer so it appears in Game Kit
|
||||||
fiorentine, _ = DeckVariant.objects.get_or_create(
|
rws, _ = DeckVariant.objects.get_or_create(
|
||||||
slug="fiorentine-minchiate",
|
slug="tarot-rider-waite-smith",
|
||||||
defaults={"name": "Fiorentine Minchiate", "card_count": 97},
|
defaults={"name": "Tarot (Rider-Waite-Smith)", "card_count": 78},
|
||||||
)
|
)
|
||||||
gamer.unlocked_decks.add(fiorentine)
|
gamer.unlocked_decks.add(rws)
|
||||||
self.create_pre_authenticated_session(GAMER_EMAIL)
|
self.create_pre_authenticated_session(GAMER_EMAIL)
|
||||||
self.browser.get(self.live_server_url + "/gameboard/")
|
self.browser.get(self.live_server_url + "/gameboard/")
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_fiorentine_deck")
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_tarot_deck")
|
||||||
).click()
|
).click()
|
||||||
don_btn = self.wait_for(
|
don_btn = self.wait_for(
|
||||||
lambda: self.browser.find_element(
|
lambda: self.browser.find_element(
|
||||||
|
|||||||
@@ -527,17 +527,17 @@ class GameKitDeckSelectionTest(FunctionalTest):
|
|||||||
slug="earthman",
|
slug="earthman",
|
||||||
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
|
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
|
||||||
)
|
)
|
||||||
self.fiorentine, _ = DeckVariant.objects.get_or_create(
|
self.rws, _ = DeckVariant.objects.get_or_create(
|
||||||
slug="fiorentine-minchiate",
|
slug="tarot-rider-waite-smith",
|
||||||
defaults={"name": "Fiorentine Minchiate", "card_count": 78, "is_default": False},
|
defaults={"name": "Tarot (Rider-Waite-Smith)", "card_count": 78, "is_default": False},
|
||||||
)
|
)
|
||||||
self.gamer = User.objects.create(email="gamer@deck.io")
|
self.gamer = User.objects.create(email="gamer@deck.io")
|
||||||
# Signal sets equipped_deck = earthman and unlocked_decks = [earthman].
|
# Signal sets equipped_deck = earthman and unlocked_decks = [earthman].
|
||||||
# Explicitly grant fiorentine too, then switch equipped_deck to it so
|
# Explicitly grant fiorentine too, then switch equipped_deck to it so
|
||||||
# the test can exercise switching back to Earthman.
|
# the test can exercise switching back to Earthman.
|
||||||
self.gamer.refresh_from_db()
|
self.gamer.refresh_from_db()
|
||||||
self.gamer.unlocked_decks.add(self.fiorentine)
|
self.gamer.unlocked_decks.add(self.rws)
|
||||||
self.gamer.equipped_deck = self.fiorentine
|
self.gamer.equipped_deck = self.rws
|
||||||
self.gamer.save(update_fields=["equipped_deck"])
|
self.gamer.save(update_fields=["equipped_deck"])
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@@ -578,23 +578,23 @@ class GameKitDeckSelectionTest(FunctionalTest):
|
|||||||
don = portal.find_element(By.CSS_SELECTOR, ".btn-equip")
|
don = portal.find_element(By.CSS_SELECTOR, ".btn-equip")
|
||||||
self.assertNotIn("btn-disabled", don.get_attribute("class"))
|
self.assertNotIn("btn-disabled", don.get_attribute("class"))
|
||||||
|
|
||||||
# ── Hover over Fiorentine Minchiate deck ─────────────────────────
|
# ── Hover over Tarot (Rider-Waite-Smith) deck ────────────────────
|
||||||
fiorentine_el = self.browser.find_element(By.ID, "id_kit_fiorentine_deck")
|
rws_el = self.browser.find_element(By.ID, "id_kit_tarot_deck")
|
||||||
self.browser.execute_script(
|
self.browser.execute_script(
|
||||||
"arguments[0].scrollIntoView({block: 'center'})", fiorentine_el
|
"arguments[0].scrollIntoView({block: 'center'})", rws_el
|
||||||
)
|
)
|
||||||
ActionChains(self.browser).move_to_element(fiorentine_el).perform()
|
ActionChains(self.browser).move_to_element(rws_el).perform()
|
||||||
|
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
lambda: self.assertIn(
|
lambda: self.assertIn(
|
||||||
"Fiorentine",
|
"Rider-Waite-Smith",
|
||||||
self.browser.find_element(By.ID, "id_tooltip_portal").text,
|
self.browser.find_element(By.ID, "id_tooltip_portal").text,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
portal = self.browser.find_element(By.ID, "id_tooltip_portal")
|
portal = self.browser.find_element(By.ID, "id_tooltip_portal")
|
||||||
self.assertIn("78", portal.text)
|
self.assertIn("78", portal.text)
|
||||||
|
|
||||||
# Mini tooltip shows "Equipped" — Fiorentine is the active deck
|
# Mini tooltip shows "Equipped" — RWS is the active deck
|
||||||
mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal")
|
mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal")
|
||||||
self.wait_for(lambda: self.assertTrue(mini.is_displayed()))
|
self.wait_for(lambda: self.assertTrue(mini.is_displayed()))
|
||||||
self.assertIn("Equipped", mini.text)
|
self.assertIn("Equipped", mini.text)
|
||||||
@@ -639,8 +639,8 @@ class GameKitDeckSelectionTest(FunctionalTest):
|
|||||||
deck_cards = self.browser.find_elements(By.CSS_SELECTOR, "#id_game_kit .deck-variant")
|
deck_cards = self.browser.find_elements(By.CSS_SELECTOR, "#id_game_kit .deck-variant")
|
||||||
self.assertEqual(len(deck_cards), 1)
|
self.assertEqual(len(deck_cards), 1)
|
||||||
self.browser.find_element(By.ID, "id_kit_earthman_deck")
|
self.browser.find_element(By.ID, "id_kit_earthman_deck")
|
||||||
fiorentine_cards = self.browser.find_elements(By.ID, "id_kit_fiorentine_deck")
|
rws_cards = self.browser.find_elements(By.ID, "id_kit_tarot_deck")
|
||||||
self.assertEqual(len(fiorentine_cards), 0)
|
self.assertEqual(len(rws_cards), 0)
|
||||||
|
|
||||||
|
|
||||||
class GameKitPageTest(FunctionalTest):
|
class GameKitPageTest(FunctionalTest):
|
||||||
|
|||||||
Reference in New Issue
Block a user