layout updates, added json views, fixed relationships in schema
This commit is contained in:
parent
1baf73a338
commit
e231828425
|
@ -24,6 +24,7 @@ unicode-slugify = "^0.1.5"
|
||||||
nanoid = "^2.0.0"
|
nanoid = "^2.0.0"
|
||||||
nanoid-dictionary = "^2.4.0"
|
nanoid-dictionary = "^2.4.0"
|
||||||
wtforms-alchemy = "^0.18.0"
|
wtforms-alchemy = "^0.18.0"
|
||||||
|
sqlalchemy-serializer = "^1.4.1"
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|
|
@ -4,6 +4,19 @@ body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
position : relative;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
.disabled:after {
|
||||||
|
position :absolute;
|
||||||
|
left : 0;
|
||||||
|
top : 0;
|
||||||
|
width : 100%;
|
||||||
|
height : 100%;
|
||||||
|
content :' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
margin: 1rem auto;
|
margin: 1rem auto;
|
||||||
|
@ -35,79 +48,161 @@ ul.nav li {
|
||||||
padding: 0 0.5rem;
|
padding: 0 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#character_sheet {
|
#sheet_container {
|
||||||
width:50%;
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
#character_sheet .statblock {
|
#character_sheet {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom:3rem;
|
margin-bottom:3rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 250px;
|
|
||||||
grid-gap: 1rem;
|
grid-gap: 1rem;
|
||||||
|
grid-template-columns: min-content max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
#character_sheet .banner {
|
#sheet_container .banner {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 64px 1fr;
|
grid-template-columns: 64px 1fr;
|
||||||
grid-gap: 1rem;
|
grid-gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#character_sheet .banner #portrait {
|
#sheet_container .banner #portrait {
|
||||||
width: 64px;
|
width: 64px;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
background: #e7e7e7;
|
background: #e7e7e7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#controls {
|
||||||
|
float: right;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
#character_sheet h1 {
|
.temp_hp input {
|
||||||
|
font-size: 0.75rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sheet_container h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#character_sheet .sidebar {
|
#sheet_container .sidebar {
|
||||||
min-height: 300px;
|
grid-column-start: 2;
|
||||||
|
grid-row-start: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#character_sheet input,
|
#sheet_container .sidebar .card {
|
||||||
#character_sheet select,
|
margin-bottom: 1rem;
|
||||||
#character_sheet textarea {
|
}
|
||||||
|
|
||||||
|
#sheet_container .sidebar ul {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
#sheet_container .sidebar ul > li {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hp {
|
||||||
|
grid-row-start: 1;
|
||||||
|
grid-column: 7;
|
||||||
|
grid-column-end: 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
#saves {
|
||||||
|
grid-row-start: 2;
|
||||||
|
grid-column: 3;
|
||||||
|
grid-column-start: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#proficiency {
|
||||||
|
grid-row-start: 2;
|
||||||
|
grid-column: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#initiative {
|
||||||
|
grid-row-start: 2;
|
||||||
|
grid-column: 6;
|
||||||
|
}
|
||||||
|
#ac {
|
||||||
|
grid-row-start: 2;
|
||||||
|
grid-column: 7;
|
||||||
|
}
|
||||||
|
#speed {
|
||||||
|
grid-row-start: 2;
|
||||||
|
grid-column: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
#skills {
|
||||||
|
grid-row-start: 2;
|
||||||
|
grid-row-end: 50;
|
||||||
|
grid-column: 1;
|
||||||
|
grid-column-start: span 2;
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#actions {
|
||||||
|
grid-row-start: 3;
|
||||||
|
grid-column: 4;
|
||||||
|
grid-column-start: span 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
table {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(50px, 150px) 1fr;
|
||||||
|
grid-gap: 0rem;
|
||||||
|
}
|
||||||
|
table th {
|
||||||
|
grid-column-start: span 4;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-right: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td {
|
||||||
|
padding-right: 1rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
font-size: 0.75em;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#sheet_container input,
|
||||||
|
#sheet_container select,
|
||||||
|
#sheet_container textarea {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#character_sheet input#name {
|
#sheet_container input#name {
|
||||||
font-size: 1.5rem;
|
font-size: 2.0rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats .cards {
|
.stats {
|
||||||
margin-top: 1rem;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, 1fr);
|
grid-template-columns: repeat(8, minmax(6rem, 1fr));
|
||||||
grid-auto-rows: auto;
|
|
||||||
grid-gap: 1rem;
|
grid-gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .cards {
|
.card {
|
||||||
margin-top: 1rem;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
grid-template-rows: auto;
|
|
||||||
grid-gap: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cards .card {
|
|
||||||
border: 2px solid #e7e7e7;
|
border: 2px solid #e7e7e7;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: .5rem;
|
padding: .5rem;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card .label {
|
.label {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card input {
|
.card input {
|
||||||
|
|
|
@ -58,6 +58,6 @@ function setSpellSaveDC() {
|
||||||
stats.forEach(applyStatModifiers);
|
stats.forEach(applyStatModifiers);
|
||||||
stats.forEach(setStatBonus);
|
stats.forEach(setStatBonus);
|
||||||
setProficiencyBonus();
|
setProficiencyBonus();
|
||||||
setSpellSaveDC();
|
// setSpellSaveDC();
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
{% from "list.html" import build_list %}
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
@ -14,6 +15,7 @@
|
||||||
{% block headers %}{% endblock %}
|
{% block headers %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{{ build_list(c) }}
|
||||||
<div id='content'>
|
<div id='content'>
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,66 +1,153 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% from "list.html" import build_list %}
|
{% set DISABLED = False if c.record.id else True %}
|
||||||
|
|
||||||
|
{% macro field(name, disabled=False) %}
|
||||||
|
{% set default_value = c.record[name] if c.record.id else c.form[name].default %}
|
||||||
|
{{ c.form[name](disabled=disabled, **{'data-initial_value': default_value}) }}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{{ build_list(c) }}
|
<div id='sheet_container'>
|
||||||
|
|
||||||
<div id='character_sheet'>
|
|
||||||
<form name="character_sheet" method="post" novalidate class="form">
|
<form name="character_sheet" method="post" novalidate class="form">
|
||||||
|
|
||||||
<div class='banner'>
|
<div class='banner'>
|
||||||
<div><img id='portrait' /></div>
|
<div><img id='portrait' /></div>
|
||||||
<div>
|
<div>
|
||||||
{{ c.form.name }}
|
{{ field('name') }}
|
||||||
{{ c.form.ancestry }} {{ c.record.character_class|join(' / ') }} Level {{ c.form.level }}
|
{{ field('ancestry') }} {{ c.record.character_class|join(' / ') }} Level {{ field('level') }}
|
||||||
|
<div id='controls'>
|
||||||
|
{{ c.form.save }} {{ c.form.delete }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='statblock'>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id='character_sheet' {% if not c.record.id %}class='disabled'{% endif %} >
|
||||||
<div class='stats'>
|
<div class='stats'>
|
||||||
<div class='cards'>
|
|
||||||
{% for stat in ['str', 'dex', 'con', 'int', 'wis', 'cha'] %}
|
{% for stat in ['str', 'dex', 'con', 'int', 'wis', 'cha'] %}
|
||||||
<div class='card'>
|
<div class='card'>
|
||||||
<div class='label'>{{ c.form[stat].label }}</div>
|
<div class='label'>{{ c.form[stat].label }}</div>
|
||||||
{{ c.form[stat] }}
|
{{ field(stat, DISABLED) }}
|
||||||
<div id='{{stat}}_bonus'></div>
|
<div id='{{stat}}_bonus'></div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class='card'>
|
<div id='hp' class='card'>
|
||||||
<div class='label'>AC</div>
|
<div class='label'>HP</div>
|
||||||
{{ c.form.armor_class }}
|
{{ field('hit_points', DISABLED) }} / {{ field('max_hit_points', DISABLED) }}
|
||||||
|
<div id='temp_hp'>
|
||||||
|
<span class='label'>TEMP</span> {{ field('temp_hit_points', DISABLED) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<div id='skills'>
|
||||||
<li>Initiative: <span id='initiative'>3</span></li>
|
<div class='label'>Skills</div>
|
||||||
<li>Proficiency Bonus: <span id='proficiency_bonus'></span></li>
|
<table>
|
||||||
<li>Spell Save DC: <span id='spell_save_dc'></span></li>
|
{% for skill in c.record.skills %}
|
||||||
<li>Saving Throws: <span id='saving_throws'>{{ c.record.saving_throws |join(', ') }}</span></li>
|
<tr><td>{{ skill }}</td><td>3</td></tr>
|
||||||
<li>Skills: <span id='skills'>{{ c.record.skills |join(', ') }}</span></li>
|
|
||||||
{% for field in c.form %}
|
|
||||||
{% if field.name in ['proficiencies', 'speed', 'passive_perception', 'passive_insight', 'passive_investigation'] %}
|
|
||||||
<li>{{ field.label }}: {{ field }} {{ field.errors|join(',') }}</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div id='saves' class='card'>
|
||||||
|
<div class='label'>Saving Throws</div>
|
||||||
|
{% for save in c.record.saving_throws %}
|
||||||
|
{{ save }} 3
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id='proficiency' class='card'>
|
||||||
|
<div class='label'>PROF</div>
|
||||||
|
<div id='proficiency_bonus'></div>
|
||||||
|
<div class='label'>BONUS</div>
|
||||||
|
</div>
|
||||||
|
<div id="ac" class='card'>
|
||||||
|
<div class='label'>Armor</div>
|
||||||
|
{{ field('armor_class', DISABLED) }}
|
||||||
|
<div class='label'>Class</div>
|
||||||
|
</div>
|
||||||
|
<div id='initiative' class='card'>
|
||||||
|
<div class='label'>Initiative</div>
|
||||||
|
<span id='initiative_bonus'>3 </span>
|
||||||
|
<div class='label'>Bonus</div>
|
||||||
|
</div>
|
||||||
|
<div id='speed' class='card'>
|
||||||
|
<div class='label'>Speed</div>
|
||||||
|
{{ field('speed', DISABLED) }}
|
||||||
|
</div>
|
||||||
|
<div id="actions" class='card'>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td class='label' colspan='2'>Actions</td>
|
||||||
|
<td class='label'>To Hit</td>
|
||||||
|
<td class='label'>Range</td>
|
||||||
|
<td class='label'>Targets</td>
|
||||||
|
<td class='label'>Damage</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Attack</th>
|
||||||
|
<td>Dagger</td>
|
||||||
|
<td>+7</td>
|
||||||
|
<td>5</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>1d4+3 slashing</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Attack</th>
|
||||||
|
<td>Sabetha's Fans</td>
|
||||||
|
<td>+7</td>
|
||||||
|
<td>5</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>2d6 slashing</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Spell</th>
|
||||||
|
<td>Eldritch Blast</td>
|
||||||
|
<td>+5</td>
|
||||||
|
<td>120</td>
|
||||||
|
<td>1</td>
|
||||||
|
<td>1d10 force</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class='label' colspan='2'>Bonus Actions</td>
|
||||||
|
<td class='label'>To Hit</td>
|
||||||
|
<td class='label'>Range</td>
|
||||||
|
<td class='label'>Targets</td>
|
||||||
|
<td class='label'>Damage</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p>
|
||||||
|
<span class='note'>
|
||||||
|
Attack (1 per Action), Cast a Spell, Dash, Disengage, Dodge, Grapple,<br>Help, Hide, Improvise, Ready, Search, Shove, or Use an Object
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- SIDEBAR -->
|
||||||
|
|
||||||
|
<div class='sidebar'>
|
||||||
|
<div class='card'>
|
||||||
|
<div class='label'>Inspiration</div>
|
||||||
|
<ul>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class='sidebar'>
|
|
||||||
<div class='cards'>
|
|
||||||
<div class='card'>
|
<div class='card'>
|
||||||
<div class='label'>HP</div>
|
<div class='label'>Conditions</div>
|
||||||
{{ c.form.max_hit_points }} / {{ c.form.hit_points }}
|
<ul>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class='card'>
|
<div class='card'>
|
||||||
<div class='label'>TEMP HP</div>
|
<div class='label'>Defenses</div>
|
||||||
{{ c.form.temp_hit_points }}
|
<ul>
|
||||||
</div>
|
<li>Vulnerable to Fire</li>
|
||||||
|
<li>Immune to Cold</li>
|
||||||
|
<li>Resistant to Poison</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
{{ c.form.save }} {{ c.form.delete }}
|
|
||||||
</div>
|
|
||||||
{{ c.form.csrf_token }}
|
{{ c.form.csrf_token }}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
@ -70,6 +157,8 @@
|
||||||
<div style='clear:both;display:block;'>
|
<div style='clear:both;display:block;'>
|
||||||
<h2>Debug</h2>
|
<h2>Debug</h2>
|
||||||
<code>
|
<code>
|
||||||
|
{{ DISABLED }}
|
||||||
|
<code>
|
||||||
{{ c }}
|
{{ c }}
|
||||||
</code>
|
</code>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
|
import logging
|
||||||
import nanoid
|
import nanoid
|
||||||
from nanoid_dictionary import human_alphabet
|
from nanoid_dictionary import human_alphabet
|
||||||
from sqlalchemy import Column
|
from sqlalchemy import Column
|
||||||
from sqlalchemy import String
|
from sqlalchemy import String
|
||||||
from sqlalchemy import String
|
|
||||||
from pyramid_sqlalchemy import BaseObject
|
from pyramid_sqlalchemy import BaseObject
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
|
|
||||||
|
@ -31,9 +31,23 @@ class IterableMixin:
|
||||||
for attr in self.__mapper__.columns.keys():
|
for attr in self.__mapper__.columns.keys():
|
||||||
if attr in values:
|
if attr in values:
|
||||||
yield attr, values[attr]
|
yield attr, values[attr]
|
||||||
|
for relname in self.__mapper__.relationships.keys():
|
||||||
|
relvals = []
|
||||||
|
for rel in self.__getattribute__(relname):
|
||||||
|
try:
|
||||||
|
relvals.append({k: v for k, v in vars(rel).items() if not k.startswith('_')})
|
||||||
|
except TypeError:
|
||||||
|
relvals.append(rel)
|
||||||
|
yield relname, relvals
|
||||||
|
|
||||||
def __repr__(self):
|
def __json__(self, request):
|
||||||
return f"{self.__class__.__name__}: {str(dict(self))}"
|
serialized = dict()
|
||||||
|
for (key, value) in self:
|
||||||
|
try:
|
||||||
|
serialized[key] = getattr(self.value, '__json__')(request)
|
||||||
|
except AttributeError:
|
||||||
|
serialized[key] = value
|
||||||
|
return serialized
|
||||||
|
|
||||||
|
|
||||||
def multivalue_string_factory(name, column=Column(String), separator=';'):
|
def multivalue_string_factory(name, column=Column(String), separator=';'):
|
||||||
|
|
|
@ -62,7 +62,7 @@ data = {
|
||||||
'max_hit_points': 14,
|
'max_hit_points': 14,
|
||||||
'hit_points': 14,
|
'hit_points': 14,
|
||||||
'temp_hit_points': 0,
|
'temp_hit_points': 0,
|
||||||
'speed': '30 ft.',
|
'speed': 30,
|
||||||
'str': 16,
|
'str': 16,
|
||||||
'dex': 12,
|
'dex': 12,
|
||||||
'con': 18,
|
'con': 18,
|
||||||
|
@ -88,18 +88,32 @@ data = {
|
||||||
{
|
{
|
||||||
'id': 1,
|
'id': 1,
|
||||||
'ancestry_id': 1,
|
'ancestry_id': 1,
|
||||||
'description': '+1 to All Ability Scores',
|
'name': '+1 to All Ability Scores',
|
||||||
|
'level': 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 2,
|
||||||
|
'ancestry_id': 2,
|
||||||
|
'name': 'Breath Weapon',
|
||||||
'level': 1,
|
'level': 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
'Modifier': [
|
'Modifier': [
|
||||||
|
# Humans
|
||||||
{'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'str'},
|
{'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'str'},
|
||||||
{'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'dex'},
|
{'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'dex'},
|
||||||
{'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'con'},
|
{'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'con'},
|
||||||
{'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'int'},
|
{'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'int'},
|
||||||
{'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'wis'},
|
{'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'wis'},
|
||||||
{'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'cha'},
|
{'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'cha'},
|
||||||
|
|
||||||
|
# Dragonborn
|
||||||
|
{'source_table_name': 'ancestry_trait', 'source_table_id': 2, 'value': '60', 'type': 'attribute ', 'target': 'Darkvision'},
|
||||||
|
{'source_table_name': 'ancestry_trait', 'source_table_id': 2, 'value': '+1', 'type': 'stat', 'target': ''},
|
||||||
|
{'source_table_name': 'ancestry_trait', 'source_table_id': 2, 'value': '+1', 'type': 'stat', 'target': ''},
|
||||||
|
|
||||||
|
# Fighting Style: Archery
|
||||||
{'source_table_name': 'class_attribute', 'source_table_id': 1, 'value': '+2', 'type': 'weapon ', 'target': 'ranged'},
|
{'source_table_name': 'class_attribute', 'source_table_id': 1, 'value': '+2', 'type': 'weapon ', 'target': 'ranged'},
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from sqlalchemy import ForeignKey
|
||||||
from sqlalchemy import Enum
|
from sqlalchemy import Enum
|
||||||
from sqlalchemy import Text
|
from sqlalchemy import Text
|
||||||
from sqlalchemy import UniqueConstraint
|
from sqlalchemy import UniqueConstraint
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from ttfrog.db.base import Bases, BaseObject, IterableMixin
|
from ttfrog.db.base import Bases, BaseObject, IterableMixin
|
||||||
from ttfrog.db.base import multivalue_string_factory
|
from ttfrog.db.base import multivalue_string_factory
|
||||||
|
@ -17,9 +18,18 @@ STATS = ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA']
|
||||||
CREATURE_TYPES = ['aberation', 'beast', 'celestial', 'construct', 'dragon', 'elemental', 'fey', 'fiend', 'Giant',
|
CREATURE_TYPES = ['aberation', 'beast', 'celestial', 'construct', 'dragon', 'elemental', 'fey', 'fiend', 'Giant',
|
||||||
'humanoid', 'monstrosity', 'ooze', 'plant', 'undead']
|
'humanoid', 'monstrosity', 'ooze', 'plant', 'undead']
|
||||||
|
|
||||||
|
|
||||||
|
class EnumField(enum.Enum):
|
||||||
|
"""
|
||||||
|
A serializable enum.
|
||||||
|
"""
|
||||||
|
def __json__(self, request):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
# enums for db schemas
|
# enums for db schemas
|
||||||
StatsEnum = enum.Enum("StatsEnum", ((k, k) for k in STATS))
|
StatsEnum = EnumField("StatsEnum", ((k, k) for k in STATS))
|
||||||
CreatureTypesEnum = enum.Enum("CreatureTypesEnum", ((k, k) for k in CREATURE_TYPES))
|
CreatureTypesEnum = EnumField("CreatureTypesEnum", ((k, k) for k in CREATURE_TYPES))
|
||||||
|
|
||||||
CharacterClassMixin = multivalue_string_factory('character_class', Column(String, nullable=False))
|
CharacterClassMixin = multivalue_string_factory('character_class', Column(String, nullable=False))
|
||||||
SavingThrowsMixin = multivalue_string_factory('saving_throws')
|
SavingThrowsMixin = multivalue_string_factory('saving_throws')
|
||||||
|
@ -50,6 +60,7 @@ class Ancestry(*Bases):
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
name = Column(String, index=True, unique=True)
|
name = Column(String, index=True, unique=True)
|
||||||
creature_type = Column(Enum(CreatureTypesEnum))
|
creature_type = Column(Enum(CreatureTypesEnum))
|
||||||
|
traits = relationship("AncestryTrait")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return str(self.name)
|
return str(self.name)
|
||||||
|
@ -59,13 +70,10 @@ class AncestryTrait(BaseObject, IterableMixin):
|
||||||
__tablename__ = "ancestry_trait"
|
__tablename__ = "ancestry_trait"
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
ancestry_id = Column(Integer, ForeignKey("ancestry.id"), nullable=False)
|
ancestry_id = Column(Integer, ForeignKey("ancestry.id"), nullable=False)
|
||||||
name = Column(String, nullable="False")
|
name = Column(String, nullable=False)
|
||||||
description = Column(Text)
|
description = Column(Text)
|
||||||
level = Column(Integer, nullable=False, info={'min': 1, 'max': 20})
|
level = Column(Integer, nullable=False, info={'min': 1, 'max': 20})
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return str(self.name)
|
|
||||||
|
|
||||||
|
|
||||||
class CharacterClass(*Bases, SavingThrowsMixin, SkillsMixin):
|
class CharacterClass(*Bases, SavingThrowsMixin, SkillsMixin):
|
||||||
__tablename__ = "character_class"
|
__tablename__ = "character_class"
|
||||||
|
@ -83,8 +91,8 @@ class ClassAttribute(BaseObject, IterableMixin):
|
||||||
__tablename__ = "class_attribute"
|
__tablename__ = "class_attribute"
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
character_class_id = Column(Integer, ForeignKey("character_class.id"), nullable=False)
|
character_class_id = Column(Integer, ForeignKey("character_class.id"), nullable=False)
|
||||||
name = Column(String, nullable="False")
|
name = Column(String, nullable=False)
|
||||||
value = Column(String, nullable="False")
|
value = Column(String, nullable=False)
|
||||||
description = Column(Text)
|
description = Column(Text)
|
||||||
level = Column(Integer, nullable=False, info={'min': 1, 'max': 20})
|
level = Column(Integer, nullable=False, info={'min': 1, 'max': 20})
|
||||||
|
|
||||||
|
@ -95,14 +103,14 @@ class ClassAttribute(BaseObject, IterableMixin):
|
||||||
class Character(*Bases, CharacterClassMixin, SavingThrowsMixin, SkillsMixin):
|
class Character(*Bases, CharacterClassMixin, SavingThrowsMixin, SkillsMixin):
|
||||||
__tablename__ = "character"
|
__tablename__ = "character"
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
ancestry = Column(String, ForeignKey("ancestry.name"), nullable=False)
|
ancestry = Column(String, ForeignKey("ancestry.name"), nullable=False, default='human')
|
||||||
name = Column(String, default='New Character', nullable=False)
|
name = Column(String, default='New Character', nullable=False)
|
||||||
level = Column(Integer, default=1, nullable=False, info={'min': 1, 'max': 20})
|
level = Column(Integer, default=1, nullable=False, info={'min': 1, 'max': 20})
|
||||||
armor_class = Column(Integer, default=10, nullable=False, info={'min': 1, 'max': 99})
|
armor_class = Column(Integer, default=10, nullable=False, info={'min': 1, 'max': 99})
|
||||||
hit_points = Column(Integer, default=1, nullable=False, info={'min': 0, 'max': 999})
|
hit_points = Column(Integer, default=1, nullable=False, info={'min': 0, 'max': 999})
|
||||||
max_hit_points = Column(Integer, default=1, nullable=False, info={'min': 0, 'max': 999})
|
max_hit_points = Column(Integer, default=1, nullable=False, info={'min': 0, 'max': 999})
|
||||||
temp_hit_points = Column(Integer, default=0, nullable=False, info={'min': 0})
|
temp_hit_points = Column(Integer, default=0, nullable=False, info={'min': 0, 'max': 999})
|
||||||
speed = Column(String, nullable=False, default="30 ft.")
|
speed = Column(Integer, nullable=False, default=30, info={'min': 0, 'max': 99})
|
||||||
str = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
|
str = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
|
||||||
dex = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
|
dex = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
|
||||||
con = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
|
con = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from .base import BaseController
|
from .base import BaseController
|
||||||
from .character_sheet import CharacterSheet
|
from .character_sheet import CharacterSheet
|
||||||
|
from .json_data import JsonData
|
||||||
|
|
||||||
__all__ = [BaseController, CharacterSheet]
|
__all__ = [BaseController, CharacterSheet, JsonData]
|
||||||
|
|
|
@ -63,6 +63,8 @@ class BaseController:
|
||||||
def form(self):
|
def form(self):
|
||||||
if not self.model:
|
if not self.model:
|
||||||
return
|
return
|
||||||
|
if not self.model_form:
|
||||||
|
return
|
||||||
if not self._form:
|
if not self._form:
|
||||||
if self.request.POST:
|
if self.request.POST:
|
||||||
self._form = self.model_form(self.request.POST, obj=self.record)
|
self._form = self.model_form(self.request.POST, obj=self.record)
|
||||||
|
@ -106,8 +108,9 @@ class BaseController:
|
||||||
db.add(self.record)
|
db.add(self.record)
|
||||||
logging.debug(f"Added {self.record = }")
|
logging.debug(f"Added {self.record = }")
|
||||||
location = self.request.current_route_path()
|
location = self.request.current_route_path()
|
||||||
if self.slug not in location:
|
if self.record.slug not in location:
|
||||||
location = f"{location}/{self.record.uri}"
|
location = f"{location}/{self.record.uri}"
|
||||||
|
logging.debug(f"Redirecting to {location}")
|
||||||
return HTTPFound(location=location)
|
return HTTPFound(location=location)
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
|
|
|
@ -17,7 +17,7 @@ class CharacterForm(ModelForm):
|
||||||
save = SubmitField()
|
save = SubmitField()
|
||||||
delete = SubmitField()
|
delete = SubmitField()
|
||||||
|
|
||||||
ancestry = DeferredSelectField('Ancestry', model=Ancestry, validate_choice=True, widget=Select())
|
ancestry = DeferredSelectField('Ancestry', model=Ancestry, default='human', validate_choice=True, widget=Select())
|
||||||
|
|
||||||
character_class = DeferredSelectMultipleField(
|
character_class = DeferredSelectMultipleField(
|
||||||
'CharacterClass',
|
'CharacterClass',
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
def routes(config):
|
def routes(config):
|
||||||
config.add_route('index', '/')
|
config.add_route('index', '/')
|
||||||
config.add_route('sheet', '/c{uri:.*}', factory='ttfrog.webserver.controllers.CharacterSheet')
|
config.add_route('sheet', '/c{uri:.*}', factory='ttfrog.webserver.controllers.CharacterSheet')
|
||||||
|
config.add_route('data', '/_/{table_name}{uri:.*}', factory='ttfrog.webserver.controllers.JsonData')
|
||||||
|
|
|
@ -17,3 +17,7 @@ def index(request):
|
||||||
@view_config(route_name='sheet', renderer='character_sheet.html')
|
@view_config(route_name='sheet', renderer='character_sheet.html')
|
||||||
def sheet(request):
|
def sheet(request):
|
||||||
return response_from(request.context)
|
return response_from(request.context)
|
||||||
|
|
||||||
|
@view_config(route_name='data', renderer='json')
|
||||||
|
def data(request):
|
||||||
|
return response_from(request.context)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user