Initial Commit

This commit is contained in:
Neill Cox 2024-06-26 14:29:06 +10:00
commit 715224653d
58 changed files with 7760 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
db.sqlite3
.idea/
.venv/
.hidden_notes.txt

0
django_gurps/__init__.py Normal file
View file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

16
django_gurps/asgi.py Normal file
View file

@ -0,0 +1,16 @@
"""
ASGI config for django_gurps project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_gurps.settings")
application = get_asgi_application()

157
django_gurps/settings.py Normal file
View file

@ -0,0 +1,157 @@
"""
Django settings for django_gurps project.
Generated by 'django-admin startproject' using Django 5.0.1.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-x%rs&gy9iphje-l+!^!g@s@w$4@oc0^honvnr-!edwm+uujiu2"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = [
"gcs.neill.id.au",
"localhost",
"127.0.0.1",
]
CSRF_TRUSTED_ORIGINS = [
"https://gcs.neill.id.au",
]
# Application definition
INSTALLED_APPS = [
"gurps_character",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"tailwind",
"theme",
"django_browser_reload",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"django_browser_reload.middleware.BrowserReloadMiddleware",
]
ROOT_URLCONF = "django_gurps.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "django_gurps.wsgi.application"
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_URL = "static/"
# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ["GURPS_DATABASE_NAME"],
"USER": os.environ["GURPS_DATABASE_USER"],
"PASSWORD": os.environ["GURPS_DATABASE_PASSWORD"],
"HOST": os.environ["GURPS_DATABASE_HOST"],
"PORT": os.environ["GURPS_DATABASE_PORT"],
}
}
TAILWIND_APP_NAME = 'theme'
INTERNAL_IPS = [
"127.0.0.1",
]
LOGIN_REDIRECT_URL = "home"
LOGOUT_REDIRECT_URL = "home"

29
django_gurps/urls.py Normal file
View file

@ -0,0 +1,29 @@
"""
URL configuration for django_gurps project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path
from django.views.generic.base import TemplateView
from django.contrib.auth import views
urlpatterns = [
path("gurps/", include("gurps_character.urls")),
path("admin/", admin.site.urls),
path("__reload__/", include("django_browser_reload.urls")),
path("accounts/", include("django.contrib.auth.urls")),
path("", TemplateView.as_view(template_name="home.html"), name="home"),
path('logout/', views.LogoutView.as_view(), name='logout'),
]

1
django_gurps/views.py Normal file
View file

@ -0,0 +1 @@
def logout(request):

16
django_gurps/wsgi.py Normal file
View file

@ -0,0 +1,16 @@
"""
WSGI config for django_gurps project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_gurps.settings")
application = get_wsgi_application()

View file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

7
gurps_character/admin.py Normal file
View file

@ -0,0 +1,7 @@
from django.contrib import admin
# Register your models here.
from .models import GURPSCharacter, GameSystem
admin.site.register(GURPSCharacter)
admin.site.register(GameSystem)

6
gurps_character/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class GurpsCharacterConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "gurps_character"

28
gurps_character/forms.py Normal file
View file

@ -0,0 +1,28 @@
import json
from django import forms
from django.core.exceptions import ValidationError
def validate_file(value):
if value.size > 10**6:
raise ValidationError("File too large")
try:
data = json.loads(value.read())
except json.JSONDecodeError:
raise ValidationError("Not a GCS file - json decode")
try:
version = data['version']
except KeyError:
import bpdb;bpdb.set_trace()
raise ValidationError("Not a GCS file - key error")
if version < 4:
raise ValidationError(
f"The file version ({version}) is too old. Please use a newer version (5.20.3) of GCS."
)
class UploadFileForm(forms.Form):
file = forms.FileField(validators=[validate_file])

View file

@ -0,0 +1,28 @@
# Generated by Django 5.0.1 on 2024-01-08 10:25
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="GURPSCharacter",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("uuid", models.CharField(max_length=128, unique=True)),
("name", models.CharField(max_length=255, unique=True)),
],
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.0.1 on 2024-01-10 07:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("gurps_character", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="gurpscharacter",
name="details",
field=models.JSONField(default={}),
preserve_default=False,
),
]

View file

@ -0,0 +1,28 @@
# Generated by Django 5.0.1 on 2024-01-16 07:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("gurps_character", "0002_gurpscharacter_details"),
]
operations = [
migrations.CreateModel(
name="GameSystem",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255, unique=True)),
("description", models.TextField(null=True)),
],
),
]

View file

391
gurps_character/models.py Normal file
View file

@ -0,0 +1,391 @@
from django.db import models
# Create your models here.
class GameSystem(models.Model):
name = models.CharField(max_length=255, unique=True)
description = models.TextField(null=True)
class GURPSCharacter(models.Model):
uuid = models.CharField(max_length=128, unique=True)
name = models.CharField(max_length=255, unique=True)
details = models.JSONField()
# owner => user
# campaign => campaign
def adv_points(self):
return sum([ a['calc']['points'] for a in self.details.get('advantages',[]) if a['calc']['points'] > 0 ])
def disadv_points(self):
return sum([ a['calc']['points'] for a in self.details.get('advantages', []) if a['calc']['points'] < -1 ])
def quirks_points(self):
return sum([ a['calc']['points'] for a in self.details.get('advantages',[]) if a['calc']['points'] == -1 ])
def attr_points(self):
return sum([ a['calc']['points'] for a in self.details['attributes'] ])
def skills_points(self):
return sum([ s['points'] for s in self.details.get('skills',[]) ])
def spells_points(self):
return 0
def race_points(self):
return 0
def unspent_points(self):
return self.details['total_points'] - self.adv_points() - \
self.disadv_points() - self.attr_points() - \
self.skills_points() - self.spells_points() - \
self.race_points() - self.quirks_points()
def get_primary_attr(self, attr_id):
return [ a['calc'] for a in self.details['attributes'] if a['attr_id'] == attr_id ][0]
def st(self):
return self.get_primary_attr('st')
def iq(self):
return self.get_primary_attr('iq')
def dx(self):
return self.get_primary_attr('dx')
def ht(self):
return self.get_primary_attr('ht')
def will(self):
return self.get_primary_attr('will')
def fright_check(self):
return self.get_primary_attr('fright_check')
def per(self):
return self.get_primary_attr('per')
def vision(self):
return self.get_primary_attr('vision')
def hearing(self):
return self.get_primary_attr('hearing')
def taste_smell(self):
return self.get_primary_attr('taste_smell')
def touch(self):
return self.get_primary_attr('touch')
def basic_move(self):
return self.get_primary_attr('basic_move')
def basic_speed(self):
return self.get_primary_attr('basic_speed')
def hp(self):
return self.get_primary_attr('hp')
def fp(self):
return self.get_primary_attr('fp')
def weight_carried(self):
items = [ i['calc']['extended_weight'] for i in self.details['equipment'] ]
total_weight = 0
for i in items:
total_weight += float(i.split()[0])
return total_weight
def enc_level(self):
if self.weight_carried() <= self.basic_lift():
return 0
elif self.weight_carried() <= self.basic_lift() * 2:
return 1
elif self.weight_carried() <= self.basic_lift() * 3:
return 2
elif self.weight_carried() <= self.basic_lift() * 6:
return 3
else:
return 4
def enc_levels(self):
enc_level = self.enc_level()
dodge = self.details["calc"]["dodge"][0]
basic_move = self.basic_move()["value"]
return [
{ "max_load": round(self.basic_lift()), "move": basic_move, "dodge": dodge, "current": "current" * (enc_level == 0) },
{ "max_load": round(self.basic_lift()) * 2, "move": basic_move -2, "dodge": dodge -1, "current": "current" * (enc_level == 1) },
{ "max_load": round(self.basic_lift()) * 3, "move": basic_move -3, "dodge": dodge -2, "current": "current" * (enc_level == 2) },
{ "max_load": round(self.basic_lift()) * 6, "move": basic_move -4, "dodge": dodge -3, "current": "current" * (enc_level == 3) },
{ "max_load": round(self.basic_lift()) * 10, "move": basic_move -5, "dodge": dodge -4, "current": "current" * (enc_level == 4) },
]
def basic_lift(self):
return float(self.details["calc"]["basic_lift"].split()[0])
def weight_unit(self):
return self.details['settings']['default_weight_units']
def lift_table(self):
return [
{"value": round(self.basic_lift()), "label":"Basic Lift"},
{"value": round(self.basic_lift()) * 2, "label": "One-Handed Lift" },
{"value": round(self.basic_lift()) * 8, "label":"Two-Handed Lift"},
{"value": round(self.basic_lift()) * 12, "label":"Shove &ampersand; Knock Over"},
{"value": round(self.basic_lift()) * 24, "label":"Running Shove & Knock Over"},
{"value": round(self.basic_lift()) * 15, "label":"Carry on Back"},
{"value": round(self.basic_lift()) * 50, "label":"Shift Slightly"},
]
def reaction_modifiers(self):
modifiers = []
for a in self.details.get('advantages',[]):
if "features" in a:
for f in a["features"]:
if f["type"] == "reaction_bonus":
modifiers.append(f)
return modifiers
def conditional_modifiers(self):
modifiers = []
for a in self.details.get('advantages', []):
if "features" in a:
for f in a["features"]:
if f["type"] == "conditional_modifier":
modifiers.append(f)
return modifiers
def weapons(self):
return [ w for w in self.details['equipment'] if "weapons" in w] + \
[ a for a in self.details.get("advantages",[]) if "weapons" in a ] + \
[ a for a in self.details.get("traits",[]) if "weapons" in a ] + \
[ s for s in self.details.get("spells", []) if "weapons" in s ]
def melee_weapons(self):
mw = []
for item in self.weapons():
for weapon in item["weapons"]:
if weapon["type"] == "melee_weapon":
if "description" in item:
name = item["description"]
else:
name = item["name"]
mw.append(
{
"name":name,
"usage": weapon["usage"],
"skill_level":weapon["calc"].get("level", 0),
"parry":weapon["calc"].get("parry", "No"),
"block":weapon["calc"].get("block", "No"),
"damage":weapon["calc"]["damage"],
"reach":weapon["calc"].get("reach", ""),
"strength":weapon.get("strength", " ")
}
)
return mw
def ranged_weapons(self):
def muscle_range(wpn_range):
if wpn_range.startswith("x"):
ranges = wpn_range.split("/")
new_ranges = []
for wpn_range in ranges:
wpn_range = float(wpn_range[1:])
wpn_range = wpn_range * self.st()["value"]
wpn_range = str(wpn_range)
wpn_range = wpn_range[:wpn_range.index(".")]
new_ranges.append(wpn_range)
return "/".join(new_ranges)
else:
return wpn_range
rw = []
for item in self.weapons():
for weapon in item["weapons"]:
if weapon["type"] == "ranged_weapon":
if "description" in item:
name = item["description"]
else:
name = item["name"]
rw.append(
{
"name":name,
"bulk": weapon.get("bulk", " "),
"usage": weapon.get("usage", " "),
"skill_level":weapon["calc"]["level"],
"damage":weapon["calc"]["damage"],
"strength":weapon.get("strength", " "),
"acc":weapon.get("accuracy", 0),
"range": muscle_range(weapon.get("range", " ")),
"rof": weapon.get("rate_of_fire", " "),
"shots": weapon.get("shots", " "),
"recoil": weapon.get("recoil", " ")
}
)
return rw
def traits(self):
traits = []
for advantage in self.details.get("advantages",[]):
cost = advantage["calc"]["points"]
name = advantage["name"]
if "categories" in advantage and "Language" in advantage["categories"]:
levels = [ m for m in advantage['modifiers'] if "disabled" not in m ]
notes = ""
for level in levels:
notes += f"{level['name']}"
if "notes" in level:
notes += f" ({level['notes']}); "
else:
notes += "; "
notes = notes[:-2]
elif "notes" in advantage:
notes = advantage["notes"]
elif "modifiers" in advantage:
notes = [ m for m in advantage['modifiers'] if m["cost"] == cost ][0]["name"]
else:
notes = ""
traits.append({"name":name, "notes":notes, "points":cost, "reference":advantage["reference"]})
return traits
def spells(self):
def get_casting_details(spell):
level = spell['calc']['level']
if level < 10:
descr = (
"Ritual: Need both hands and both feet "
"free, and must speak .Time: 2x."
)
elif level <15:
descr = (
"Ritual: Must speak a few quiet words "
"and make a gesture."
)
elif level < 20:
descr = (
"Ritual: Must speak a word or two or make "
"a small gesture. May move one yard per second "
"while concentrating. Cost: -1."
)
elif level < 25:
descr = "Ritual: None. Time: / 2 (round up). Cost: -2."
elif level < 30:
descr = (
"Ritual: None. Time: / 4 (round up). Cost: -3."
)
else:
delta = int((level - 25) / 5)
power = 2 + delta
divisor = 2**power
cost = 3 + delta
descr = (
f"Ritual: None. Time: / {power} round up. Cost: "
f"-{cost}"
)
return descr
spells = []
for spell in self.details.get('spells',[]):
notes = (
f"{get_casting_details(spell)}<br> Class: {spell['spell_class']}; "
f"Cost: {spell['casting_cost']}; Maintain: {spell['maintenance_cost']}; Time: {spell['casting_time']}; Duration: {spell['duration']}; "
)
spells.append(
{
'name': spell["name"],
"college":", ".join(spell["college"]),
"level": spell["calc"]["level"],
"rsl":spell["calc"]["rsl"],
"points":spell["points"],
"reference":spell["reference"],
"notes":notes
}
)
return spells
def skills(self):
skills = []
for skill in self.details.get('skills',[]):
skills.append({'name': skill["name"], "level": skill["calc"]["level"], "rsl":skill["calc"]["rsl"], "points":skill["points"], "reference":skill["reference"] })
return skills
def equipment(self):
def get_children(parent, level=1):
children = []
for item in parent["children"]:
equipment = {
"name": item["description"],
"uses":"",
"tech_level":item["tech_level"],
"lc":"",
"value":item["value"],
"weight":item["weight"],
"quantity":item.get("quantity",1),
"ref":item["reference"],
"ext_weight": item["calc"]["extended_weight"],
"ext_value": item["calc"]["extended_value"],
"notes": item.get("notes",""),
"equipped": item["equipped"],
"level": level
}
children.append(equipment)
if "children" in item:
children += get_children(item, level +1)
return children
equipment = self.details['equipment']
equipment_list = []
for item in equipment:
equipment_list.append(
{
"name": item["description"],
"uses":"",
"tech_level":item["tech_level"],
"lc":"",
"level": 0,
"value":item["value"],
"weight":item["weight"],
"quantity":item.get("quantity",1),
"ref":item.get("reference",""),
"ext_weight": item["calc"]["extended_weight"],
"ext_value": item["calc"]["extended_value"],
"notes": item.get("notes",""),
"equipped": item["equipped"]
}
)
if 'children' in item:
equipment_list += get_children(item)
return equipment_list
def hit_locations(self):
try:
# v2
return self.details["settings"]["hit_locations"]["locations"]
except KeyError:
# v4
return self.details["settings"]["body_type"]["locations"]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% block content %}
<ul>
{% for character in characters %}
<li>
<a href="{% url 'details' character.uuid %}">{{ character.name }}</a>
<a href="{% url 'download' character.uuid %}">Download</a>
</li>
{% endfor %}
</ul>
<a href="{% url 'upload' %}">Upload new character</a>
<form method="POST" enctype="multipart/form-data" action="{% url 'index' %}">
{% csrf_token %}
{{ form }}
<input type="submit" value="Upload">
</form>
{% endblock %}

View file

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block content %}
<form method="POST" enctype="multipart/form-data" action="{% url 'upload' %}">
{% csrf_token %}
{{ form }}
</form>
{% endblock %}

View file

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block content %}
<h3>Welcome to the character store</h3>
<p<This is a webapp for storing characters.<p>
<p>Currently it works with GURPS, specifically characters created by GCS</p>
{% endblock %}

View file

@ -0,0 +1,48 @@
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link active" aria-current="page" href="/">Home</a></li>
<li class="nav-item"><a class="nav-link active" aria-current="page" href="/gurps">GURPS</a></li>
<li class="nav-item"><a class="nav-link disabled" aria-current="page" href="/rq">RuneQuest</a></li>
<li class="nav-item">
<a class="nav-link" href="/admin">Admin</a>
</li>
<!--<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Dropdown
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">Action</a></li>
<li><a class="dropdown-item" href="#">Another action</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#">Something else here</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link disabled" aria-disabled="true">Disabled</a>
</li>
</ul>
<form class="d-flex" role="search">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
-->
</div>
<div class="navbar-text float-end">
{% if user.is_authenticated %}
<form method="post" action="{% url 'logout' %}">
{% csrf_token %}
<button type="submit">logout</button>
</form>
{% else %}
<a class="nav-link" href="{% url 'login' %}">Login</a>
{% endif %}
</div>
</div>
</nav>

View file

@ -0,0 +1,38 @@
{% extends "base.html" %}
{% block content %}
{% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}
{% if next %}
{% if user.is_authenticated %}
<p>Your account doesn't have access to this page. To proceed,
please login with an account that has access.</p>
{% else %}
<p>Please login to see this page.</p>
{% endif %}
{% endif %}
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<table>
<tr>
<td>{{ form.username.label_tag }}</td>
<td>{{ form.username }}</td>
</tr>
<tr>
<td>{{ form.password.label_tag }}</td>
<td>{{ form.password }}</td>
</tr>
</table>
<input type="submit" value="login">
<input type="hidden" name="next" value="{{ next }}">
</form>
{# Assumes you set up the password_reset view in your URLconf #}
<p><a href="{% url 'password_reset' %}">Lost password?</a></p>
{% endblock %}

3
gurps_character/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

10
gurps_character/urls.py Normal file
View file

@ -0,0 +1,10 @@
from django.urls import path
from . import views
urlpatterns = [
path("", views.index, name="index"),
path("details/<str:uuid>/", views.details, name="details"),
path("upload", views.upload_file, name="upload"),
path("download/<str:uuid>/", views.download, name="download"),
]

67
gurps_character/views.py Normal file
View file

@ -0,0 +1,67 @@
import json
from django.shortcuts import render
from django.http import HttpResponse, HttpResponseRedirect
from django.urls import reverse
from .models import GURPSCharacter
from .forms import UploadFileForm
def index(request):
characters = GURPSCharacter.objects.all()
context = {"characters":characters}
if request.method == "POST":
form = UploadFileForm(request.POST, request.FILES)
if form.is_valid():
handle_uploaded_file(request.FILES["file"])
return HttpResponseRedirect(reverse("index"))
else:
form = UploadFileForm()
context['form'] = form
return render(request, "characters/list.html", context)
def details(request, uuid):
character = GURPSCharacter.objects.get(uuid=uuid)
context = {"character": character}
#import bpdb;bpdb.set_trace()
return render(request, "characters/embedded.html", context)
def upload_file(request):
if request.method == "POST":
form = UploadFileForm(request.POST, request.FILES)
if form.is_valid():
handle_uploaded_file(request.FILES["file"])
return HttpResponseRedirect("/success/url/")
else:
form = UploadFileForm()
return render(request, "characters/upload.html", {"form": form})
def handle_uploaded_file(f):
import bpdb;bpdb.set_trace()
f.seek(0) # We read the file in the validator
data = json.loads(f.read())
uuid = data['id']
name = data["profile"]["name"]
try:
character = GURPSCharacter.objects.get(uuid=uuid)
character.details = data
character.name = name
character.save()
except GURPSCharacter.DoesNotExist:
character = GURPSCharacter(uuid=uuid, name=name, details = data)
character.save()
def download(irequest, uuid):
mime_type = "application/x-gcs-gcs"
character = GURPSCharacter.objects.get(uuid=uuid)
response = HttpResponse(json.dumps(character.details), content_type=mime_type)
response['Content-Disposition'] = "attachment; filename=%s.gcs" % character.name
return response

5
gurps_vars.sh Normal file
View file

@ -0,0 +1,5 @@
export GURPS_DATABASE_NAME=neill
export GURPS_DATABASE_USER=neill
export GURPS_DATABASE_PASSWORD=ht6a!nce3216
export GURPS_DATABASE_HOST=172.23.0.36
export GURPS_DATABASE_PORT=5432

22
manage.py Executable file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_gurps.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()

41
notes.txt Normal file
View file

@ -0,0 +1,41 @@
Users
- id
- name
(part of django auth)
GMs
- user
- campaign
# A GM is a user linked to a campaign. They get extra priovs on that campaign
Characters
- id
- owner (user-id)
- campaing (campaign id)
- game system?
Campaign
- id
- name
- game system
Campaign-Players
- campaign id
- user id
Game System
- id
- name
A character is owned by a user and is part of a campaign
A campaign has one or more gms and zero or more players
A user may view characters they own, or if they are a gm then characters that are part of one of their campaigns
Permissions

4
requirements.txt Normal file
View file

@ -0,0 +1,4 @@
django
tailwind
django_browser_reload
psycopg

0
theme/__init__.py Normal file
View file

Binary file not shown.

Binary file not shown.

5
theme/apps.py Normal file
View file

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

841
theme/static/css/dist/styles.css vendored Normal file
View file

@ -0,0 +1,841 @@
/*
! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS
*/
html,
:host {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
-webkit-tap-highlight-color: transparent;
/* 7 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-feature-settings: normal;
/* 2 */
font-variation-settings: normal;
/* 3 */
font-size: 1em;
/* 4 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-feature-settings: inherit;
/* 1 */
font-variation-settings: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Reset default styling for dialogs.
*/
dialog {
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
[type='text'],input:where(:not([type])),[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: #fff;
border-color: #6b7280;
border-width: 1px;
border-radius: 0px;
padding-top: 0.5rem;
padding-right: 0.75rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
font-size: 1rem;
line-height: 1.5rem;
--tw-shadow: 0 0 #0000;
}
[type='text']:focus, input:where(:not([type])):focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #2563eb;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
border-color: #2563eb;
}
input::-moz-placeholder, textarea::-moz-placeholder {
color: #6b7280;
opacity: 1;
}
input::placeholder,textarea::placeholder {
color: #6b7280;
opacity: 1;
}
::-webkit-datetime-edit-fields-wrapper {
padding: 0;
}
::-webkit-date-and-time-value {
min-height: 1.5em;
text-align: inherit;
}
::-webkit-datetime-edit {
display: inline-flex;
}
::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field {
padding-top: 0;
padding-bottom: 0;
}
select {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
[multiple],[size]:where(select:not([size="1"])) {
background-image: initial;
background-position: initial;
background-repeat: unset;
background-size: initial;
padding-right: 0.75rem;
-webkit-print-color-adjust: unset;
print-color-adjust: unset;
}
[type='checkbox'],[type='radio'] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
padding: 0;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
display: inline-block;
vertical-align: middle;
background-origin: border-box;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
flex-shrink: 0;
height: 1rem;
width: 1rem;
color: #2563eb;
background-color: #fff;
border-color: #6b7280;
border-width: 1px;
--tw-shadow: 0 0 #0000;
}
[type='checkbox'] {
border-radius: 0px;
}
[type='radio'] {
border-radius: 100%;
}
[type='checkbox']:focus,[type='radio']:focus {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
--tw-ring-offset-width: 2px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #2563eb;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
[type='checkbox']:checked,[type='radio']:checked {
border-color: transparent;
background-color: currentColor;
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
[type='checkbox']:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
}
@media (forced-colors: active) {
[type='checkbox']:checked {
-webkit-appearance: auto;
-moz-appearance: auto;
appearance: auto;
}
}
[type='radio']:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
}
@media (forced-colors: active) {
[type='radio']:checked {
-webkit-appearance: auto;
-moz-appearance: auto;
appearance: auto;
}
}
[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus {
border-color: transparent;
background-color: currentColor;
}
[type='checkbox']:indeterminate {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
border-color: transparent;
background-color: currentColor;
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
@media (forced-colors: active) {
[type='checkbox']:indeterminate {
-webkit-appearance: auto;
-moz-appearance: auto;
appearance: auto;
}
}
[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus {
border-color: transparent;
background-color: currentColor;
}
[type='file'] {
background: unset;
border-color: inherit;
border-width: 0;
border-radius: 0;
padding: 0;
font-size: unset;
line-height: inherit;
}
[type='file']:focus {
outline: 1px solid ButtonText;
outline: 1px auto -webkit-focus-ring-color;
}
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
.container {
width: 100%;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
.static {
position: static;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.block {
display: block;
}
.inline {
display: inline;
}
.flex {
display: flex;
}
.grid {
display: grid;
}
.hidden {
display: none;
}
.h-screen {
height: 100vh;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.border {
border-width: 1px;
}
.bg-gray-50 {
--tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
}
.font-serif {
font-family: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
}
.text-5xl {
font-size: 3rem;
line-height: 1;
}
.leading-normal {
line-height: 1.5;
}
.tracking-normal {
letter-spacing: 0em;
}

1
theme/static_src/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules

1527
theme/static_src/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
{
"name": "theme",
"version": "3.8.0",
"description": "",
"scripts": {
"start": "npm run dev",
"build": "npm run build:clean && npm run build:tailwind",
"build:clean": "rimraf ../static/css/dist",
"build:tailwind": "cross-env NODE_ENV=production tailwindcss --postcss -i ./src/styles.css -o ../static/css/dist/styles.css --minify",
"dev": "cross-env NODE_ENV=development tailwindcss --postcss -i ./src/styles.css -o ../static/css/dist/styles.css -w",
"tailwindcss": "node ./node_modules/tailwindcss/lib/cli.js"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"cross-env": "^7.0.3",
"postcss": "^8.4.32",
"postcss-import": "^15.1.0",
"postcss-nested": "^6.0.1",
"postcss-simple-vars": "^7.0.1",
"rimraf": "^5.0.5",
"tailwindcss": "^3.4.0"
}
}

View file

@ -0,0 +1,7 @@
module.exports = {
plugins: {
"postcss-import": {},
"postcss-simple-vars": {},
"postcss-nested": {}
},
}

View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -0,0 +1,57 @@
/**
* This is a minimal config.
*
* If you need the full config, get it from here:
* https://unpkg.com/browse/tailwindcss@latest/stubs/defaultConfig.stub.js
*/
module.exports = {
content: [
/**
* HTML. Paths to Django template files that will contain Tailwind CSS classes.
*/
/* Templates within theme app (<tailwind_app_name>/templates), e.g. base.html. */
'../templates/**/*.html',
/*
* Main templates directory of the project (BASE_DIR/templates).
* Adjust the following line to match your project structure.
*/
'../../templates/**/*.html',
/*
* Templates in other django apps (BASE_DIR/<any_app_name>/templates).
* Adjust the following line to match your project structure.
*/
'../../**/templates/**/*.html',
/**
* JS: If you use Tailwind CSS in JavaScript, uncomment the following lines and make sure
* patterns match your project structure.
*/
/* JS 1: Ignore any JavaScript in node_modules folder. */
// '!../../**/node_modules',
/* JS 2: Process all JavaScript files in the project. */
// '../../**/*.js',
/**
* Python: If you use Tailwind CSS classes in Python, uncomment the following line
* and make sure the pattern below matches your project structure.
*/
// '../../**/*.py'
],
theme: {
extend: {},
},
plugins: [
/**
* '@tailwindcss/forms' is the forms plugin that provides a minimal styling
* for forms. If you don't like it or have own styling for forms,
* comment the line below to disable '@tailwindcss/forms'.
*/
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
require('@tailwindcss/aspect-ratio'),
],
}

27
theme/templates/base.html Normal file
View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Django GURPS</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
<style>
a { text-decoration: underline }
</style>
{% block extra_css %}{% endblock %}
</head>
<body class="bg-gray-50 font-serif leading-normal tracking-normal">
<!--- Start Nav Bar -->
{% include "navbar.html" %}
<!--- End Nav Bar --->
<div class="container mx-auto">
<section class="flex items-center justify-center h-screen">
{% block content %}{% endblock %}
</section>
</div>
</body>
</html>