massive additions made thru somewhat new apps.epic.models, .urls, .views; new html page & partial in apps/gameboard; new apps.epic FT & ITs (all green); New Game applet now actually leads to game room feat. token-drop gatekeeper mechanism intended for 6 gamers

This commit is contained in:
Disco DeDisco
2026-03-13 00:31:17 -04:00
parent 681a1a4cd0
commit 5773462b4c
12 changed files with 303 additions and 7 deletions

View File

@@ -0,0 +1,26 @@
# 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

@@ -0,0 +1,45 @@
# Generated by Django 6.0 on 2026-03-12 19:46
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
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'), ('OPEN', 'Open'), ('RENEWAL_DUE', 'Renewal Due')], default='GATHERING', max_length=20)),
('renewal_period', models.DurationField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('board_state', models.JSONField(default=dict)),
('seed_count', models.IntegerField(default=12)),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owned_rooms', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='GateSlot',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slot_number', models.IntegerField()),
('status', models.CharField(choices=[('EMPTY', 'Empty'), ('RESERVED', 'Reserved'), ('FILLED', 'Filled')], default='EMPTY', max_length=10)),
('reserved_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)),
('gamer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='gate_slots', to=settings.AUTH_USER_MODEL)),
('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gate_slots', to='epic.room')),
],
),
]

View File

@@ -1,3 +1,69 @@
from django.db import models
import uuid
# Create your models here.
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.conf import settings
class Room(models.Model):
GATHERING = "GATHERING"
OPEN = "OPEN"
RENEWAL_DUE = "RENEWAL_DUE"
GATE_STATUS_CHOICES = [
(GATHERING, "Gathering"),
(OPEN, "Open"),
(RENEWAL_DUE, "Renewal Due"),
]
PRIVATE = "PRIVATE"
PUBLIC = "PUBLIC"
INVITE_ONLY = "INVITE ONLY"
VISIBILITY_CHOICES = [
(PRIVATE, "Private"),
(PUBLIC, "Public"),
(INVITE_ONLY, "Invite Only"),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=200)
owner = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="owned_rooms"
)
visibility = models.CharField(max_length=20, choices=VISIBILITY_CHOICES, default=PRIVATE)
gate_status = models.CharField(max_length=20, choices=GATE_STATUS_CHOICES, default=GATHERING)
renewal_period = models.DurationField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
board_state = models.JSONField(default=dict)
seed_count = models.IntegerField(default=12)
class GateSlot(models.Model):
EMPTY = "EMPTY"
RESERVED = "RESERVED"
FILLED = "FILLED"
STATUS_CHOICES = [
(EMPTY, "Empty"),
(RESERVED, "Reserved"),
(FILLED, "Filled"),
]
room = models.ForeignKey(Room, on_delete=models.CASCADE, related_name="gate_slots")
slot_number = models.IntegerField()
gamer = models.ForeignKey(
settings.AUTH_USER_MODEL, null=True, blank=True,
on_delete=models.SET_NULL, related_name="gate_slots"
)
funded_by = models.ForeignKey(
settings.AUTH_USER_MODEL, null=True, blank=True,
on_delete=models.SET_NULL, related_name="funded_slots"
)
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default=EMPTY)
reserved_at = models.DateTimeField(null=True, blank=True)
filled_at = models.DateTimeField(null=True, blank=True)
@receiver(post_save, sender=Room)
def create_gate_slots(sender, instance, created, **kwargs):
if created:
for i in range(1, 7):
GateSlot.objects.create(room=instance, slot_number=i)

View File

@@ -0,0 +1,11 @@
from django.test import TestCase
from apps.lyric.models import User
from apps.epic.models import Room, GateSlot
class RoomCreationTest(TestCase):
def test_creating_a_room_generates_six_gate_slots(self):
owner = User.objects.create(email="founder@example.com")
room = Room.objects.create(name="Test Room", owner=owner)
self.assertEqual(GateSlot.objects.filter(room=room).count(), 6)

View File

@@ -0,0 +1,31 @@
from django.test import TestCase
from django.urls import reverse
from apps.lyric.models import User
from apps.epic.models import Room
class RoomCreationViewTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="founder@test.io")
self.client.force_login(self.user)
def test_post_creates_room_and_redirects_to_gatekeeper(self):
response = self.client.post(
reverse("epic:create_room"),
data={"name": "Test Room"},
)
room = Room.objects.get(owner=self.user)
self.assertRedirects(
response, reverse(
"epic:gatekeeper",
args=[room.id],
)
)
def test_post_requires_login(self):
self.client.logout()
response = self.client.post(
reverse("epic:create_room"),
data={"name": "Test Room"},
)

View File

@@ -0,0 +1,11 @@
from django.urls import path
from . import views
app_name = 'epic'
urlpatterns = [
path('rooms/create_room', views.create_room, name='create_room'),
path('room/<uuid:room_id>/gate/', views.gatekeeper, name='gatekeeper'),
]

View File

@@ -1,3 +1,22 @@
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect, render
# Create your views here.
from apps.epic.models import Room
@login_required
def create_room(request):
if request.method == "POST":
name = request.POST.get("name", "").strip()
if name:
room = Room.objects.create(name=name, owner=request.user)
return redirect("epic:gatekeeper", room_id=room.id)
return redirect("gameboard:index")
def gatekeeper(request, room_id):
room = Room.objects.get(id=room_id)
slots = room.gate_slots.order_by("slot_number")
return render(request, "apps/gameboard/room.html", {
'room': room,
'slots': slots,
})

View File

@@ -12,6 +12,7 @@ urlpatterns = [
path('dashboard/', include('apps.dashboard.urls')),
path('lyric/', include('apps.lyric.urls')),
path('gameboard/', include('apps.gameboard.urls')),
path('gameboard/', include('apps.epic.urls', 'epic')),
]
# Please remove the following urlpattern

View File

@@ -0,0 +1,42 @@
from selenium.webdriver.common.by import By
from .base import FunctionalTest
from apps.applets.models import Applet
class GatekeeperTest(FunctionalTest):
def setUp(self):
super().setUp()
Applet.objects.get_or_create(
slug="new-game", defaults={"name": "New Game", "context": "gameboard"}
)
def test_founder_creates_room_and_sees_gatekeeper(self):
# 1. Log in, navigate to gameboard
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(self.live_server_url + "/gameboard/")
# 2. New Game applet has room name input, create button
self.wait_for(
lambda: self.browser.find_element(By.ID, "id_applet_new_game")
)
self.browser.find_element(By.ID, "id_new_game_name").send_keys("Test Room")
self.browser.find_element(By.ID, "id_create_game_btn").click()
# 3. User is redirected to Gatekeeper page for new room
self.wait_for(
lambda: self.assertIn("/gameboard/room/", self.browser.current_url)
)
self.wait_for(
lambda: self.assertIn("/gate/", self.browser.current_url)
)
# 4. Page shows room name, GATHERING status
body = self.browser.find_element(By.TAG_NAME, "body")
self.assertIn("Test Room", body.text)
self.assertIn("GATHERING", body.text)
# 5. Six token slots are visible
slots = self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot")
self.assertEqual(len(slots), 6)
# 6. Slot 1 has Drop Token btn; slots 26 show as empty
slot_1 = slots[0]
slot_1.find_element(By.CSS_SELECTOR, ".drop-token-btn")
for slot in slots[1:]:
self.assertIn("empty", slot.get_attribute("class"))

View File

@@ -3,7 +3,9 @@
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
>
<h2>New Game</h2>
<ul class="game-type">
<small>[feature forthcoming]</small>
</ul>
<form method="POST" action="{% url "epic:create_room" %}">
{% csrf_token %}
<input id="id_new_game_name" name="name" type="text" placeholder="Room name" />
<button type="submit" id="id_create_game_btn" class="btn btn-primary btn-xl">Start<br>Game</button>
</form>
</section>

View File

@@ -0,0 +1,26 @@
<div class="gate-modal" role"dialog" aria-label="Gatekeeper">
<header class="gate-header">
<h1>{{ room.name }}</h1>
<span class="gate-status">{{ room.gate_status }}</span>
</header>
<div class="gate-slots">
{% for slot in slots %}
<div
class="gate-slot{% if slot.status == 'EMPTY' %} empty{% endif %}"
data-slot="{{ slot.slot_number }}"
>
<span class="slot-number">{{ slot.slot_number }}</span>
{% if slot.gamer %}
<span class="slot-gamer">{{ slot.gamer.email }}</span>
{% else %}
<span class="slot-gamer">empty</span>
{% endif %}
{% if slot.slot_number == 1 and request.user == room.owner %}
<button class="drop-token-btn">Drop Token</button>
{% endif %}
</div>
{% endfor %}
</div>
</div>

View File

@@ -0,0 +1,16 @@
{% extends "core/base.html" %}
{% block content %}
<div class="room-page">
<div class="room-shell">
{% comment "game room content" %}gaussian blur + darkening (cf., e.g., tooltip effect) {% endcomment %}
<div class="room-table"></div>
</div>
{% if room.gate_status == "GATHERING" %}
<div class="gate-overlay">
{% include "apps/gameboard/_partials/_gatekeeper.html" %}
</div>
{% endif %}
</div>
{% endblock content %}