Compare commits
2 Commits
15ac3216ff
...
b03ba09b65
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b03ba09b65 | ||
|
|
befa61e1e9 |
@@ -73,6 +73,14 @@ src/
|
||||
functional_tests/
|
||||
```
|
||||
|
||||
### Template directory convention
|
||||
Templates live under `templates/apps/<frontend-app>/`, not under the backend app that owns the view logic. Specifically:
|
||||
- `lyric/` views → `templates/apps/dashboard/`
|
||||
- `epic/` views → `templates/apps/gameboard/`
|
||||
- `drama/` views → `templates/apps/billboard/`
|
||||
|
||||
Backend apps (`lyric`, `epic`, `drama`) have **no** `templates/` subdirectory.
|
||||
|
||||
## Dev Commands
|
||||
```bash
|
||||
# Dev server (ASGI — required for WebSockets; no npm/webpack build step)
|
||||
|
||||
@@ -476,7 +476,7 @@ def tarot_deck(request, room_id):
|
||||
room=room,
|
||||
defaults={"deck_variant": deck_variant},
|
||||
)
|
||||
return render(request, "apps/epic/tarot_deck.html", {
|
||||
return render(request, "apps/gameboard/tarot_deck.html", {
|
||||
"room": room,
|
||||
"deck": deck,
|
||||
"remaining": deck.remaining_count,
|
||||
@@ -499,7 +499,7 @@ def tarot_deal(request, room_id):
|
||||
}
|
||||
for i, (card, is_reversed) in enumerate(drawn)
|
||||
]
|
||||
return render(request, "apps/epic/tarot_deck.html", {
|
||||
return render(request, "apps/gameboard/tarot_deck.html", {
|
||||
"room": room,
|
||||
"deck": deck,
|
||||
"remaining": deck.remaining_count,
|
||||
|
||||
@@ -32,7 +32,7 @@ def gameboard(request):
|
||||
"carte": carte,
|
||||
"equipped_trinket_id": str(request.user.equipped_trinket_id or ""),
|
||||
"equipped_deck_id": str(request.user.equipped_deck_id or ""),
|
||||
"deck_variants": list(DeckVariant.objects.all()),
|
||||
"deck_variants": list(request.user.unlocked_decks.all()),
|
||||
"free_tokens": free_tokens,
|
||||
"free_count": len(free_tokens),
|
||||
"applets": applet_context(request.user, "gameboard"),
|
||||
@@ -62,7 +62,7 @@ def toggle_game_applets(request):
|
||||
"carte": request.user.tokens.filter(token_type=Token.CARTE).first(),
|
||||
"equipped_trinket_id": str(request.user.equipped_trinket_id or ""),
|
||||
"equipped_deck_id": str(request.user.equipped_deck_id or ""),
|
||||
"deck_variants": list(DeckVariant.objects.all()),
|
||||
"deck_variants": list(request.user.unlocked_decks.all()),
|
||||
"free_tokens": list(request.user.tokens.filter(
|
||||
token_type=Token.FREE, expires_at__gt=timezone.now()
|
||||
).order_by("expires_at")),
|
||||
|
||||
19
src/apps/lyric/migrations/0015_user_unlocked_decks.py
Normal file
19
src/apps/lyric/migrations/0015_user_unlocked_decks.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
24
src/apps/lyric/migrations/0016_backfill_unlocked_decks.py
Normal file
24
src/apps/lyric/migrations/0016_backfill_unlocked_decks.py
Normal file
@@ -0,0 +1,24 @@
|
||||
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),
|
||||
]
|
||||
@@ -41,6 +41,9 @@ class User(AbstractBaseUser):
|
||||
"epic.DeckVariant", null=True, blank=True,
|
||||
on_delete=models.SET_NULL, related_name="+",
|
||||
)
|
||||
unlocked_decks = models.ManyToManyField(
|
||||
"epic.DeckVariant", blank=True, related_name="unlocked_by",
|
||||
)
|
||||
|
||||
is_staff = models.BooleanField(default=False)
|
||||
is_superuser = models.BooleanField(default=False)
|
||||
@@ -175,3 +178,5 @@ def create_wallet_and_tokens(sender, instance, created, **kwargs):
|
||||
earthman = DeckVariant.objects.filter(slug="earthman").first()
|
||||
instance.equipped_deck = earthman
|
||||
instance.save(update_fields=['equipped_trinket', 'equipped_deck'])
|
||||
if earthman:
|
||||
instance.unlocked_decks.add(earthman)
|
||||
|
||||
@@ -231,9 +231,11 @@ class GameKitDeckSelectionTest(FunctionalTest):
|
||||
defaults={"name": "Fiorentine Minchiate", "card_count": 78, "is_default": False},
|
||||
)
|
||||
self.gamer = User.objects.create(email="gamer@deck.io")
|
||||
# Signal sets equipped_deck = earthman (now it exists); put gamer on
|
||||
# Fiorentine so the test can exercise switching back to Earthman.
|
||||
# Signal sets equipped_deck = earthman and unlocked_decks = [earthman].
|
||||
# Explicitly grant fiorentine too, then switch equipped_deck to it so
|
||||
# the test can exercise switching back to Earthman.
|
||||
self.gamer.refresh_from_db()
|
||||
self.gamer.unlocked_decks.add(self.fiorentine)
|
||||
self.gamer.equipped_deck = self.fiorentine
|
||||
self.gamer.save(update_fields=["equipped_deck"])
|
||||
|
||||
@@ -321,3 +323,22 @@ class GameKitDeckSelectionTest(FunctionalTest):
|
||||
game_kit.get_attribute("data-equipped-deck-id"), ""
|
||||
)
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Test 6 — new user's Game Kit shows only the default Earthman deck #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_new_user_game_kit_shows_only_earthman_deck(self):
|
||||
"""A fresh user's game kit contains only the Earthman deck card;
|
||||
the Fiorentine deck is not visible because it has not been unlocked."""
|
||||
newcomer = User.objects.create(email="newcomer@deck.io")
|
||||
newcomer.unlocked_decks.add(self.earthman)
|
||||
self.create_pre_authenticated_session("newcomer@deck.io")
|
||||
self.browser.get(self.live_server_url + "/gameboard/")
|
||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_game_kit"))
|
||||
|
||||
deck_cards = self.browser.find_elements(By.CSS_SELECTOR, "#id_game_kit .deck-variant")
|
||||
self.assertEqual(len(deck_cards), 1)
|
||||
self.browser.find_element(By.ID, "id_kit_earthman_deck")
|
||||
fiorentine_cards = self.browser.find_elements(By.ID, "id_kit_fiorentine_deck")
|
||||
self.assertEqual(len(fiorentine_cards), 0)
|
||||
|
||||
@@ -68,8 +68,10 @@ body.page-gameboard {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
overflow-x: visible;
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
|
||||
Reference in New Issue
Block a user