full passing test suite w. new stripe integration across multiple project nodes; new gameboard django app; stripe in test mode on staging
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
Disco DeDisco
2026-03-09 01:07:16 -04:00
parent ad0caa7c17
commit bd72135a2f
27 changed files with 397 additions and 33 deletions

View File

@@ -5,6 +5,8 @@ DJANGO_SUPERUSER_EMAIL={{ django_superuser_email }}
DJANGO_SUPERUSER_PASSWORD={{ django_superuser_password }} DJANGO_SUPERUSER_PASSWORD={{ django_superuser_password }}
DATABASE_URL=postgresql://gamearray:{{ postgres_password }}@gamearray_postgres/gamearray DATABASE_URL=postgresql://gamearray:{{ postgres_password }}@gamearray_postgres/gamearray
MAILGUN_API_KEY={{ mailgun_api_key }} MAILGUN_API_KEY={{ mailgun_api_key }}
STRIPE_PUBLISHABLE_KEY={{ stripe_publishable_key }}
STRIPE_SECRET_KEY={{ stripe_secret_key }}
CELERY_BROKER_URL=redis://gamearray_redis:6379/0 CELERY_BROKER_URL=redis://gamearray_redis:6379/0
REDIS_URL=redis://gamearray_redis:6379/1 REDIS_URL=redis://gamearray_redis:6379/1

View File

@@ -1,28 +1,42 @@
$ANSIBLE_VAULT;1.1;AES256 $ANSIBLE_VAULT;1.1;AES256
65383061626464353936363564313761663834646361326362613934363565623234636337313363 38383061343764656262613934313230656462366163363263653462333338333863326338343838
3933313962643261353830333463336166393030313936370a616234626135633432613366633363 3664646437643462346636623231633639396239333532340a363338313839353734326238643735
61633265363937326231623365646336333737306634646335376135633031643564666164336230 39343237396433336436366430626332343666666461613636656433363838613432393539386266
3435353764383936620a396165386538666433356166383661323037333861373632376432313332 3237336434346333350a663530623334633438616135376437666631313064333735653633396461
66666236373462363236663335623734633364653539323331396361613738636166323134386466 31306163343838336465626663373661343839653037333235313361633335646337353339616333
66656431663261633036333537373336643866623236643139656662333831366435373837656262 35343233346562346236636364316265313936646235373866636333353866623161663935626637
36333734376363373462643239623437623735373935633732343639313666663436616630363933 31633864366339653930626365373237326531366632626337636163333266656434323063333365
61396530336461393064323161666537646135383462383532363932326132363331633438313138 38373437383261613439306666373764633737623466626235356465636365646337306534326535
61623431326537313637626239653038353263313731303262653537316134383264616661623962 36633866663161613632613434666134343465383663633165663330376535653537333763376232
32333564366362383431336432303964663835363365636434303332613161363930333065336637 61653265303134656338393033303834663630653064666134633638393235346631346461633030
33343466343062306434663765613837343635386630326439303739616166396134393939626434 35343332393961363361613661633633613262663231366236396663636239326534373134623762
62336634303963653230626630636363343730623734626336363039623231633532653330646366 30653139333134616236666238616466633733656633326331386138363839653566333434346534
66613432633834393133386666623466326131386633303264333766306135623337353433306632 63326539333461383265316332336333656365386531393630663537363365643061363263313738
66323733373232383862646661313966366465333463366361366337656537623562613964666631 37633564363533633762393736636333306433306534393539636231656162343562383232663932
65373566316432383134666434393338626138363632633766636561383263333636623530326664 62646339363266303564383438636636373661656465666663613863396639633732636635326166
63333265366132376437396431393535323931383637323833303839336635633735333565333530 39323738303338373466366236623665633538363134616565326665386564613735393638656630
65343263373630633063383931646163323237643436366566363932646566323539373136646433 31326431316163376132623064376634643737313864336464623431333834663361336133353838
37623638333834373537316164633166633738333363656431356163623332396631353864333333 32303635663261333732306137383133623134373363613837306637663566303634653863343766
33306666646532626636376239326438373737383432663539333736363866663938396136383035 33613936626362653466333537666462373633313038376565623363666631353162643634653730
32343534613862653538346430313338326435356230636535343464666262626663376635363835 30323532623261643136666237316561353038323265303930336364633731333533386563623133
65363862663461353464313533313333323863313539643533343431643130383663656161616131 31343965643336613933663431626435333235366639363334653065303434386165333739336632
33323639333564383830346163386362386238323936393832623961646565613961356263356365 61363030376664643638653365626365623936623864666663326534343863613962616431376666
65376431666130356564666236383764316136326366666661326538653133343165326431393564 39363837386639393235316339323932326466616330303165613032663637616232656162653335
36303065366263316232663230343137333231346538633036613066643365616331336135376461 61613266376262626234383135306238313366346330656333383465383861663962653638303362
35613265623134663633303238366363336137383436663836353863623533396236666433303738 34353833646461383839386238626661346263363131643438343461393739336132386466373665
38356361653633323065303035376664326238633066623731623436333332373363636634323433 32646238633161363064666335626639653335306236613866333934646366323564306133396131
393631303539373234386465663630316335 36343032623964316138386538333863363530396330646431373466646538663063326330663639
32323762356632336364333162336133336335623865323861663131626232633066643238333237
32343938353166353037316162653832663433343534626331633936633866356666653932656665
38396533356131326262633431653435306362633966383531356236396639376437396333616130
35666435393461316232323234653865346338326330623065373461323961393663306262313066
30313430353065616230356135333565333338373663643434353561363438656233383739663233
35653832353062396634613832353837333835636461616234343462626239636634613430373931
31656534343764643065643733326637343631356633653531313062633362663461313732633331
35626364393563373339636466346339383032383635303865306636623737343237333863353238
63306132396262656365323833323635633563653735366630313363386236613231346339643430
63396230353566633830383932666335373665356434656438336338633035653465613665613862
31663565653338376662323866613538363566306635333735646363363730646331306234353839
30346363393231623563646439623261643634663831313338393761343865303930373133633733
31656466303365316164396463373335396464643130643337656361333339653238333633373662
6539

View File

@@ -21,11 +21,13 @@ outcome==1.3.0.post0
packaging==25.0 packaging==25.0
pycparser==2.23 pycparser==2.23
PySocks==1.7.1 PySocks==1.7.1
python-dotenv
requests==2.32.5 requests==2.32.5
selenium==4.39.0 selenium==4.39.0
sniffio==1.3.1 sniffio==1.3.1
sortedcontainers==2.4.0 sortedcontainers==2.4.0
sqlparse==0.5.5 sqlparse==0.5.5
stripe
trio==0.32.0 trio==0.32.0
trio-websocket==0.12.2 trio-websocket==0.12.2
types-PyYAML==6.0.12.20250915 types-PyYAML==6.0.12.20250915

View File

@@ -13,4 +13,5 @@ lxml==6.0.2
psycopg2-binary psycopg2-binary
redis redis
requests==2.31.0 requests==2.31.0
stripe
whitenoise==6.11.0 whitenoise==6.11.0

View File

@@ -0,0 +1,43 @@
const initWallet = () => {
let stripe, elements;
const addBtn = document.getElementById('id_add_payment_method');
const saveBtn = document.getElementById('id_save_payment_method');
if (!addBtn) return;
const getCsrf = () => document.cookie.match(/csrftoken=([^;]+)/)[1];
addBtn.addEventListener('click', async () => {
const res = await fetch('/dashboard/wallet/setup-intent', {
method: 'POST',
headers: {'X-CSRFToken': getCsrf()},
});
const {client_secret, publishable_key} = await res.json();
stripe = Stripe(publishable_key);
elements = stripe.elements({clientSecret: client_secret});
elements.create('payment').mount('#id_stripe_payment_element');
saveBtn.hidden = false;
});
saveBtn.addEventListener('click', async () => {
const {error, setupIntent} = await stripe.confirmSetup({
elements,
redirect: 'if_required',
});
if (error) { console.error(error); return; }
const res = await fetch('/dashboard/wallet/save-payment-method', {
method: 'POST',
headers: {
'X-CSRFToken': getCsrf(),
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `payment_method_id=${setupIntent.payment_method}`,
});
const {last4, brand} = await res.json();
const pm = document.createElement('div');
pm.textContent = `${brand} ····${last4}`;
document.getElementById('id_payment_methods').appendChild(pm);
});
};
document.addEventListener('DOMContentLoaded', initWallet);

View File

@@ -0,0 +1,79 @@
from unittest import mock
from django.test import TestCase
from apps.lyric.models import PaymentMethod, User
class SetupIntentViewTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="capman@test.io")
self.client.force_login(self.user)
def test_setup_intent_requires_login(self):
self.client.logout()
response = self.client.post("/dashboard/wallet/setup-intent")
self.assertRedirects(
response, "/?next=/dashboard/wallet/setup-intent",
fetch_redirect_response=False,
)
@mock.patch("apps.dashboard.views.stripe")
def test_returns_client_secret(self, mock_stripe):
mock_stripe.Customer.create.return_value = mock.Mock(id="cus_test123")
mock_stripe.SetupIntent.create.return_value = mock.Mock(client_secret="seti_secret")
response = self.client.post("/dashboard/wallet/setup-intent")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["client_secret"], "seti_secret")
self.assertIn("publishable_key", response.json())
@mock.patch("apps.dashboard.views.stripe")
def test_reuses_existing_stripe_customer(self, mock_stripe):
self.user.stripe_customer_id = "cus_existing"
self.user.save()
mock_stripe.SetupIntent.create.return_value = mock.Mock(client_secret="seti_secret")
self.client.post("/dashboard/wallet/setup-intent")
mock_stripe.Customer.create.assert_not_called()
mock_stripe.SetupIntent.create.assert_called_once_with(customer="cus_existing")
class SavePaymentMethodViewTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="capman@test.io")
self.user.stripe_customer_id = "cus_test123"
self.user.save()
self.client.force_login(self.user)
def test_save_payment_method_requires_login(self):
self.client.logout()
response = self.client.post(
"/dashboard/wallet/save-payment-method", {"payment_method_id": "pm_test"}
)
self.assertRedirects(
response, "/?next=/dashboard/wallet/save-payment-method",
fetch_redirect_response=False,
)
@mock.patch("apps.dashboard.views.stripe")
def test_creates_payment_method_record(self, mock_stripe):
mock_stripe.PaymentMethod.retrieve.return_value = mock.Mock(
card=mock.Mock(last4="4242", brand="visa")
)
self.client.post(
"/dashboard/wallet/save-payment-method",
{"payment_method_id": "pm_test123"},
)
pm = PaymentMethod.objects.get(user=self.user)
self.assertEqual(pm.last4, "4242")
self.assertEqual(pm.brand, "visa")
@mock.patch("apps.dashboard.views.stripe")
def test_returns_json_with_last4_and_brand(self, mock_stripe):
mock_stripe.PaymentMethod.retrieve.return_value = mock.Mock(
card=mock.Mock(last4="4242", brand="visa")
)
response = self.client.post(
"/dashboard/wallet/save-payment-method",
{"payment_method_id": "pm_test123"},
)
data = response.json()
self.assertEqual(data["last4"], "4242")
self.assertEqual(data["brand"], "visa")

View File

@@ -10,4 +10,6 @@ urlpatterns = [
path('users/<uuid:user_id>/', views.my_lists, name='my_lists'), path('users/<uuid:user_id>/', views.my_lists, name='my_lists'),
path('toggle_applets', views.toggle_applets, name="toggle_applets"), path('toggle_applets', views.toggle_applets, name="toggle_applets"),
path('wallet/', views.wallet, name='wallet'), path('wallet/', views.wallet, name='wallet'),
path('wallet/setup-intent', views.setup_intent, name='setup_intent'),
path('wallet/save-payment-method', views.save_payment_method, name='save_payment_method'),
] ]

View File

@@ -1,12 +1,16 @@
import stripe
from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db.models import Max, Q from django.db.models import Max, Q
from django.http import HttpResponse, HttpResponseForbidden from django.http import HttpResponse, HttpResponseForbidden, JsonResponse
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.views.decorators.csrf import ensure_csrf_cookie
from apps.dashboard.forms import ExistingListItemForm, ItemForm from apps.dashboard.forms import ExistingListItemForm, ItemForm
from apps.dashboard.models import Applet, Item, List, UserApplet from apps.dashboard.models import Applet, Item, List, UserApplet
from apps.lyric.models import Token, User, Wallet from apps.lyric.models import PaymentMethod, Token, User, Wallet
APPLET_ORDER = ["wallet", "new-list", "my-lists", "username", "palette"] APPLET_ORDER = ["wallet", "new-list", "my-lists", "username", "palette"]
@@ -146,6 +150,7 @@ def toggle_applets(request):
return redirect("home") return redirect("home")
@login_required(login_url="/") @login_required(login_url="/")
@ensure_csrf_cookie
def wallet(request): def wallet(request):
wallet = request.user.wallet wallet = request.user.wallet
coin = request.user.tokens.filter(token_type=Token.COIN).first() coin = request.user.tokens.filter(token_type=Token.COIN).first()
@@ -155,3 +160,31 @@ def wallet(request):
"coin": coin, "coin": coin,
"free_tokens": free_tokens, "free_tokens": free_tokens,
}) })
@login_required(login_url="/")
def setup_intent(request):
stripe.api_key = settings.STRIPE_SECRET_KEY
user = request.user
if not user.stripe_customer_id:
customer = stripe.Customer.create(email=user.email)
user.stripe_customer_id = customer.id
user.save(update_fields=["stripe_customer_id"])
intent = stripe.SetupIntent.create(customer=user.stripe_customer_id)
return JsonResponse({
"client_secret": intent.client_secret,
"publishable_key": settings.STRIPE_PUBLISHABLE_KEY,
})
@login_required(login_url="/")
def save_payment_method(request):
stripe.api_key = settings.STRIPE_SECRET_KEY
pm_id = request.POST.get("payment_method_id")
pm = stripe.PaymentMethod.retrieve(pm_id)
stripe.PaymentMethod.attach(pm_id, customer=request.user.stripe_customer_id)
PaymentMethod.objects.create(
user=request.user,
stripe_pm_id=pm_id,
last4=pm.card.last4,
brand=pm.card.brand,
)
return JsonResponse({"last4": pm.card.last4, "brand": pm.card.brand})

View File

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class GameboardConfig(AppConfig):
name = 'apps.gameboard'

View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

View File

@@ -0,0 +1,52 @@
import lxml.html
from django.test import TestCase
from apps.lyric.models import User
class GameboardViewTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="capman@test.io")
self.client.force_login(self.user)
response = self.client.get("/gameboard/")
self.parsed = lxml.html.fromstring(response.content)
def test_gameboard_requires_login(self):
self.client.logout()
response = self.client.get("/gameboard/")
self.assertRedirects(
response, "/?next=/gameboard/", fetch_redirect_response=False
)
def test_gameboard_renders(self):
response = self.client.get("/gameboard/")
self.assertEqual(response.status_code, 200)
def test_gameboard_shows_my_games_applet(self):
[_] = self.parsed.cssselect("#id_applet_my_games")
def test_gameboard_shows_new_game_applet(self):
[_] = self.parsed.cssselect("#id_applet_new_game")
def test_gameboard_shows_game_kit_btn(self):
[_] = self.parsed.cssselect("#id_game_kit_btn")
def test_gameboard_shows_game_gear(self):
[_] = self.parsed.cssselect("#id_game_gear")
def test_my_games_has_no_game_items_for_new_user(self):
game_items = self.parsed.cssselect("#id_applet_my_games .game-item")
self.assertEqual(len(game_items), 0)
def test_game_kit_has_coin_on_a_string(self):
[_] = self.parsed.cssselect("#id_game_kit #id_kit_coin_on_a_string")
def test_game_kit_has_free_token(self):
[_] = self.parsed.cssselect("#id_game_kit #id_kit_free_token_0")
def test_game_kit_has_card_deck_placeholder(self):
[_] = self.parsed.cssselect("#id_game_kit #id_kit_card_deck")
def test_game_kit_has_dice_set_placeholder(self):
[_] = self.parsed.cssselect("#id_game_kit #id_kit_dice_set")

View File

@@ -0,0 +1,9 @@
from django.urls import path
from . import views
urlpatterns = [
path('', views.gameboard, name='gameboard'),
]

View File

@@ -0,0 +1,16 @@
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
from apps.lyric.models import Token
@login_required(login_url="/")
def gameboard(request):
coin = request.user.tokens.filter(token_type=Token.COIN).first()
free_tokens = list(request.user.tokens.filter(token_type=Token.FREE))
return render(
request, "apps/gameboard/gameboard.html", {
"coin": coin,
"free_tokens": free_tokens,
}
)

View File

@@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import LoginToken, User from .models import LoginToken, Token, User
class UserAdmin(admin.ModelAdmin): class UserAdmin(admin.ModelAdmin):
@@ -9,3 +9,4 @@ class UserAdmin(admin.ModelAdmin):
admin.site.register(User, UserAdmin) admin.site.register(User, UserAdmin)
admin.site.register(LoginToken) admin.site.register(LoginToken)
admin.site.register(Token)

View File

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

@@ -31,6 +31,7 @@ class User(AbstractBaseUser):
username = models.CharField(max_length=35, unique=True, null=True, blank=True) username = models.CharField(max_length=35, unique=True, null=True, blank=True)
searchable = models.BooleanField(default=False) searchable = models.BooleanField(default=False)
palette = models.CharField(max_length=32, default="palette-default") palette = models.CharField(max_length=32, default="palette-default")
stripe_customer_id = models.CharField(max_length=255, null=True, blank=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)
@@ -78,6 +79,15 @@ class Token(models.Model):
) )
return self.get_token_type_display() return self.get_token_type_display()
class PaymentMethod(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="payment_methods")
stripe_pm_id = models.CharField(max_length=255)
last4 = models.CharField(max_length=4)
brand = models.CharField(max_length=32)
def __str__(self):
return f"{self.brand} ....{self.last4}"
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def create_wallet_and_tokens(sender, instance, created, **kwargs): def create_wallet_and_tokens(sender, instance, created, **kwargs):
if not created: if not created:

View File

@@ -55,9 +55,12 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
# Custom apps # Board apps
'apps.dashboard', 'apps.dashboard',
'apps.gameboard',
# Gamer apps
'apps.lyric', 'apps.lyric',
# Custom apps
'apps.api', 'apps.api',
'functional_tests', 'functional_tests',
# Depend apps # Depend apps
@@ -197,3 +200,7 @@ LOGGING = {
# Mailgun API settings (for HTTP API instead of SMTP) # Mailgun API settings (for HTTP API instead of SMTP)
MAILGUN_API_KEY = os.environ.get("MAILGUN_API_KEY") MAILGUN_API_KEY = os.environ.get("MAILGUN_API_KEY")
MAILGUN_DOMAIN = "howdy.earthmanrpg.com" # Your Mailgun domain MAILGUN_DOMAIN = "howdy.earthmanrpg.com" # Your Mailgun domain
# Stripe payment settings
STRIPE_PUBLISHABLE_KEY = os.environ.get("STRIPE_PUBLISHABLE_KEY", "")
STRIPE_SECRET_KEY = os.environ.get("STRIPE_SECRET_KEY", "")

View File

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

View File

@@ -104,6 +104,12 @@ class WalletDisplayTest(FunctionalTest):
By.CSS_SELECTOR, "#id_stripe_payment_element iframe" By.CSS_SELECTOR, "#id_stripe_payment_element iframe"
) )
self.browser.switch_to.frame(stripe_frame) self.browser.switch_to.frame(stripe_frame)
# 4a. Wait for card inputs to render inside iframe
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, 'input[placeholder="1234 1234 1234 1234"]'
)
)
# 5. Fill in Stripe test card details # 5. Fill in Stripe test card details
self.browser.find_element( self.browser.find_element(
By.CSS_SELECTOR, 'input[placeholder="1234 1234 1234 1234"]' By.CSS_SELECTOR, 'input[placeholder="1234 1234 1234 1234"]'
@@ -115,7 +121,7 @@ class WalletDisplayTest(FunctionalTest):
By.CSS_SELECTOR, 'input[placeholder="CVC"]' By.CSS_SELECTOR, 'input[placeholder="CVC"]'
).send_keys("424") ).send_keys("424")
self.browser.find_element( self.browser.find_element(
By.CSS_SELECTOR, 'input[placeholder="ZIP"]' By.CSS_SELECTOR, 'input[placeholder="12345"]'
).send_keys("42424") ).send_keys("42424")
# 6. Return to main doc & submit form # 6. Return to main doc & submit form
self.browser.switch_to.default_content() self.browser.switch_to.default_content()

View File

@@ -5,6 +5,12 @@ import sys
def main(): def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
"""Run administrative tasks.""" """Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
try: try:

View File

@@ -1,4 +1,5 @@
{% extends "core/base.html" %} {% extends "core/base.html" %}
{% load static %}
{% block content %} {% block content %}
<div class="wallet-page"> <div class="wallet-page">
@@ -27,6 +28,7 @@
<button id="id_add_payment_method">Add Payment Method</button> <button id="id_add_payment_method">Add Payment Method</button>
<div id="id_stripe_payment_element"></div> <div id="id_stripe_payment_element"></div>
<button id="id_save_payment_method" hidden>Save Card</button>
</section> </section>
<section id="id_tithe_token_shop"> <section id="id_tithe_token_shop">
@@ -38,4 +40,6 @@
</div> </div>
</section> </section>
</div> </div>
<script src="https://js.stripe.com/v3/"></script>
<script src="{% static "apps/scripts/wallet.js" %}"></script>
{% endblock content %} {% endblock content %}

View File

@@ -0,0 +1,35 @@
{% extends "core/base.html" %}
{% block content %}
<div class="gameboard-page">
<section id="id_applet_my_games">
<h2>My Games</h2>
</section>
<section id="id_applet_new_game">
<h2>New Game</h2>
</section>
<div id="id_game_gear"></div>
<button
id="id_game_kit_btn"
onclick="document.getElementById('id_game_kit').style.display='block'"
>
Game Kit
</button>
<div id="id_game_kit" style="display:none;">
{% if coin %}
<div id="id_kit_coin_on_a_string" class="token">
<span class="token-tooltip">{{ coin.tooltip_text }}</span>
</div>
{% endif %}
{% for token in free_tokens %}
<div id="id_kit_free_token_{{ forloop.counter0 }}" class="token">
<span class="token-tooltip">{{ token.tooltip_text }}</span>
</div>
{% endfor %}
<div id="id_kit_card_deck" class="kit-item">Card Deck</div>
<div id="id_kit_dice_set" class="kit-item">Dice Set</div>
</div>
</div>
{% endblock content %}