restructuring for poetry-slam
This commit is contained in:
1 parent
b1d7639a62
commit
78115023bb
38 files changed
+482
-464
No files matched your search
+29
-2
@@ -5,7 +5,7 @@ description = ""
|
|||||||
authors = ["evilchili <evilchili@gmail.com>"]
|
authors = ["evilchili <evilchili@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
packages = [
|
packages = [
|
||||||
{ include = 'ttfrog' },
|
{include = "*", from = "src"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
@@ -26,9 +26,9 @@ nanoid-dictionary = "^2.4.0"
|
|||||||
wtforms-alchemy = "^0.18.0"
|
wtforms-alchemy = "^0.18.0"
|
||||||
sqlalchemy-serializer = "^1.4.1"
|
sqlalchemy-serializer = "^1.4.1"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
pytest = "^8.1.1"
|
pytest = "^8.1.1"
|
||||||
|
pytest-cov = "^5.0.0"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
@@ -37,3 +37,30 @@ build-backend = "poetry.core.masonry.api"
|
|||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
ttfrog = "ttfrog.cli:app"
|
ttfrog = "ttfrog.cli:app"
|
||||||
|
|
||||||
|
|
||||||
|
### SLAM
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 120
|
||||||
|
target-version = ['py310']
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
multi_line_output = 3
|
||||||
|
line_length = 120
|
||||||
|
include_trailing_comma = true
|
||||||
|
|
||||||
|
[tool.autoflake]
|
||||||
|
check = false # return error code if changes are needed
|
||||||
|
in-place = true # make changes to files instead of printing diffs
|
||||||
|
recursive = true # drill down directories recursively
|
||||||
|
remove-all-unused-imports = true # remove all unused imports (not just those from the standard library)
|
||||||
|
ignore-init-module-imports = true # exclude __init__.py when removing unused imports
|
||||||
|
remove-duplicate-keys = true # remove all duplicate keys in objects
|
||||||
|
remove-unused-variables = true # remove unused variables
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
log_cli_level = "DEBUG"
|
||||||
|
addopts = "--cov=src --cov-report=term-missing"
|
||||||
|
|
||||||
|
### ENDSLAM
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -41,6 +41,7 @@ class AttributeMap(Mapping):
|
|||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
attributes: field(default_factory=dict)
|
attributes: field(default_factory=dict)
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
@@ -2,8 +2,8 @@ import io
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
@@ -12,9 +12,8 @@ from rich.logging import RichHandler
|
|||||||
|
|
||||||
from ttfrog.path import assets
|
from ttfrog.path import assets
|
||||||
|
|
||||||
|
|
||||||
default_data_path = Path("~/.dnd/ttfrog")
|
default_data_path = Path("~/.dnd/ttfrog")
|
||||||
default_host = '127.0.0.1'
|
default_host = "127.0.0.1"
|
||||||
default_port = 2323
|
default_port = 2323
|
||||||
|
|
||||||
SETUP_HELP = f"""
|
SETUP_HELP = f"""
|
||||||
@@ -47,18 +46,16 @@ def main(
|
|||||||
root: Optional[Path] = typer.Option(
|
root: Optional[Path] = typer.Option(
|
||||||
default_data_path,
|
default_data_path,
|
||||||
help="Path to the TableTop Frog environment",
|
help="Path to the TableTop Frog environment",
|
||||||
)
|
),
|
||||||
):
|
):
|
||||||
app_state['env'] = root.expanduser() / Path('defaults')
|
app_state["env"] = root.expanduser() / Path("defaults")
|
||||||
load_dotenv(stream=io.StringIO(SETUP_HELP))
|
load_dotenv(stream=io.StringIO(SETUP_HELP))
|
||||||
load_dotenv(app_state['env'])
|
load_dotenv(app_state["env"])
|
||||||
debug = os.getenv('DEBUG', None)
|
debug = os.getenv("DEBUG", None)
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
format='%(message)s',
|
format="%(message)s",
|
||||||
level=logging.DEBUG if debug else logging.INFO,
|
level=logging.DEBUG if debug else logging.INFO,
|
||||||
handlers=[
|
handlers=[RichHandler(rich_tracebacks=True, tracebacks_suppress=[typer])],
|
||||||
RichHandler(rich_tracebacks=True, tracebacks_suppress=[typer])
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -68,9 +65,10 @@ def setup(context: typer.Context):
|
|||||||
(Re)Initialize TableTop Frog. Idempotent; will preserve any existing configuration.
|
(Re)Initialize TableTop Frog. Idempotent; will preserve any existing configuration.
|
||||||
"""
|
"""
|
||||||
from ttfrog.db.bootstrap import bootstrap
|
from ttfrog.db.bootstrap import bootstrap
|
||||||
if not os.path.exists(app_state['env']):
|
|
||||||
app_state['env'].parent.mkdir(parents=True, exist_ok=True)
|
if not os.path.exists(app_state["env"]):
|
||||||
app_state['env'].write_text(dedent(SETUP_HELP))
|
app_state["env"].parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
app_state["env"].write_text(dedent(SETUP_HELP))
|
||||||
print(f"Wrote defaults file {app_state['env']}.")
|
print(f"Wrote defaults file {app_state['env']}.")
|
||||||
bootstrap()
|
bootstrap()
|
||||||
|
|
||||||
@@ -86,23 +84,20 @@ def serve(
|
|||||||
default_port,
|
default_port,
|
||||||
help="bind port",
|
help="bind port",
|
||||||
),
|
),
|
||||||
debug: bool = typer.Option(
|
debug: bool = typer.Option(False, help="Enable debugging output"),
|
||||||
False,
|
|
||||||
help='Enable debugging output'
|
|
||||||
),
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Start the TableTop Frog server.
|
Start the TableTop Frog server.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# delay loading the app until we have configured our environment
|
# delay loading the app until we have configured our environment
|
||||||
from ttfrog.webserver import application
|
|
||||||
from ttfrog.db.bootstrap import bootstrap
|
from ttfrog.db.bootstrap import bootstrap
|
||||||
|
from ttfrog.webserver import application
|
||||||
|
|
||||||
print("Starting TableTop Frog server...")
|
print("Starting TableTop Frog server...")
|
||||||
bootstrap()
|
bootstrap()
|
||||||
application.start(host=host, port=port, debug=debug)
|
application.start(host=host, port=port, debug=debug)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
app()
|
app()
|
||||||
File renamed without changes.
@@ -1,11 +1,10 @@
|
|||||||
import enum
|
import enum
|
||||||
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 String
|
|
||||||
from pyramid_sqlalchemy import BaseObject
|
from pyramid_sqlalchemy import BaseObject
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
|
from sqlalchemy import Column, String
|
||||||
|
|
||||||
|
|
||||||
def genslug():
|
def genslug():
|
||||||
@@ -17,16 +16,14 @@ class SlugMixin:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def uri(self):
|
def uri(self):
|
||||||
return '-'.join([
|
return "-".join([self.slug, slugify(self.name.title().replace(" ", ""), ok="", only_ascii=True, lower=False)])
|
||||||
self.slug,
|
|
||||||
slugify(self.name.title().replace(' ', ''), ok='', only_ascii=True, lower=False)
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
class IterableMixin:
|
class IterableMixin:
|
||||||
"""
|
"""
|
||||||
Allows for iterating over Model objects' column names and values
|
Allows for iterating over Model objects' column names and values
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
values = vars(self)
|
values = vars(self)
|
||||||
for attr in self.__mapper__.columns.keys():
|
for attr in self.__mapper__.columns.keys():
|
||||||
@@ -40,16 +37,16 @@ class IterableMixin:
|
|||||||
continue
|
continue
|
||||||
for rel in reliter:
|
for rel in reliter:
|
||||||
try:
|
try:
|
||||||
relvals.append({k: v for k, v in vars(rel).items() if not k.startswith('_')})
|
relvals.append({k: v for k, v in vars(rel).items() if not k.startswith("_")})
|
||||||
except TypeError:
|
except TypeError:
|
||||||
relvals.append(rel)
|
relvals.append(rel)
|
||||||
yield relname, relvals
|
yield relname, relvals
|
||||||
|
|
||||||
def __json__(self, request):
|
def __json__(self, request):
|
||||||
serialized = dict()
|
serialized = dict()
|
||||||
for (key, value) in self:
|
for key, value in self:
|
||||||
try:
|
try:
|
||||||
serialized[key] = getattr(self.value, '__json__')(request)
|
serialized[key] = getattr(self.value, "__json__")(request)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
serialized[key] = value
|
serialized[key] = value
|
||||||
return serialized
|
return serialized
|
||||||
@@ -58,7 +55,7 @@ class IterableMixin:
|
|||||||
return str(dict(self))
|
return str(dict(self))
|
||||||
|
|
||||||
|
|
||||||
def multivalue_string_factory(name, column=Column(String), separator=';'):
|
def multivalue_string_factory(name, column=Column(String), separator=";"):
|
||||||
"""
|
"""
|
||||||
Generate a mixin class that adds a string column with getters and setters
|
Generate a mixin class that adds a string column with getters and setters
|
||||||
that convert list values to strings and back again. Equivalent to:
|
that convert list values to strings and back again. Equivalent to:
|
||||||
@@ -77,27 +74,46 @@ def multivalue_string_factory(name, column=Column(String), separator=';'):
|
|||||||
attr = f"_{name}"
|
attr = f"_{name}"
|
||||||
prop = property(lambda self: getattr(self, attr).split(separator))
|
prop = property(lambda self: getattr(self, attr).split(separator))
|
||||||
setter = prop.setter(lambda self, val: setattr(self, attr, separator.join(val)))
|
setter = prop.setter(lambda self, val: setattr(self, attr, separator.join(val)))
|
||||||
return type('MultiValueString', (object, ), {
|
return type(
|
||||||
attr: column,
|
"MultiValueString",
|
||||||
f"{name}_property": prop,
|
(object,),
|
||||||
name: setter,
|
{
|
||||||
})
|
attr: column,
|
||||||
|
f"{name}_property": prop,
|
||||||
|
name: setter,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EnumField(enum.Enum):
|
class EnumField(enum.Enum):
|
||||||
"""
|
"""
|
||||||
A serializable enum.
|
A serializable enum.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __json__(self, request):
|
def __json__(self, request):
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
SavingThrowsMixin = multivalue_string_factory('saving_throws')
|
SavingThrowsMixin = multivalue_string_factory("saving_throws")
|
||||||
SkillsMixin = multivalue_string_factory('skills')
|
SkillsMixin = multivalue_string_factory("skills")
|
||||||
|
|
||||||
STATS = ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA']
|
STATS = ["STR", "DEX", "CON", "INT", "WIS", "CHA"]
|
||||||
CREATURE_TYPES = ['aberation', 'beast', 'celestial', 'construct', 'dragon', 'elemental', 'fey', 'fiend', 'Giant',
|
CREATURE_TYPES = [
|
||||||
'humanoid', 'monstrosity', 'ooze', 'plant', 'undead']
|
"aberation",
|
||||||
|
"beast",
|
||||||
|
"celestial",
|
||||||
|
"construct",
|
||||||
|
"dragon",
|
||||||
|
"elemental",
|
||||||
|
"fey",
|
||||||
|
"fiend",
|
||||||
|
"Giant",
|
||||||
|
"humanoid",
|
||||||
|
"monstrosity",
|
||||||
|
"ooze",
|
||||||
|
"plant",
|
||||||
|
"undead",
|
||||||
|
]
|
||||||
CreatureTypesEnum = EnumField("CreatureTypesEnum", ((k, k) for k in CREATURE_TYPES))
|
CreatureTypesEnum = EnumField("CreatureTypesEnum", ((k, k) for k in CREATURE_TYPES))
|
||||||
StatsEnum = EnumField("StatsEnum", ((k, k) for k in STATS))
|
StatsEnum = EnumField("StatsEnum", ((k, k) for k in STATS))
|
||||||
|
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
|
from ttfrog.db import schema
|
||||||
|
from ttfrog.db.manager import db
|
||||||
|
|
||||||
|
# move this to json or whatever
|
||||||
|
data = {
|
||||||
|
"CharacterClass": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "fighter",
|
||||||
|
"hit_dice": "1d10",
|
||||||
|
"hit_dice_stat": "CON",
|
||||||
|
"proficiencies": "all armor, all shields, simple weapons, martial weapons",
|
||||||
|
"saving_throws": ["STR, CON"],
|
||||||
|
"skills": [
|
||||||
|
"Acrobatics",
|
||||||
|
"Animal Handling",
|
||||||
|
"Athletics",
|
||||||
|
"History",
|
||||||
|
"Insight",
|
||||||
|
"Intimidation",
|
||||||
|
"Perception",
|
||||||
|
"Survival",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "rogue",
|
||||||
|
"hit_dice": "1d8",
|
||||||
|
"hit_dice_stat": "DEX",
|
||||||
|
"proficiencies": "simple weapons, hand crossbows, longswords, rapiers, shortswords",
|
||||||
|
"saving_throws": ["DEX", "INT"],
|
||||||
|
"skills": [
|
||||||
|
"Acrobatics",
|
||||||
|
"Athletics",
|
||||||
|
"Deception",
|
||||||
|
"Insight",
|
||||||
|
"Intimidation",
|
||||||
|
"Investigation",
|
||||||
|
"Perception",
|
||||||
|
"Performance",
|
||||||
|
"Persuasion",
|
||||||
|
"Sleight of Hand",
|
||||||
|
"Stealth",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"Skill": [
|
||||||
|
{"name": "Acrobatics"},
|
||||||
|
{"name": "Animal Handling"},
|
||||||
|
{"name": "Athletics"},
|
||||||
|
{"name": "Deception"},
|
||||||
|
{"name": "History"},
|
||||||
|
{"name": "Insight"},
|
||||||
|
{"name": "Intimidation"},
|
||||||
|
{"name": "Investigation"},
|
||||||
|
{"name": "Perception"},
|
||||||
|
{"name": "Performance"},
|
||||||
|
{"name": "Persuasion"},
|
||||||
|
{"name": "Sleight of Hand"},
|
||||||
|
{"name": "Stealth"},
|
||||||
|
{"name": "Survival"},
|
||||||
|
],
|
||||||
|
"Ancestry": [
|
||||||
|
{"id": 1, "name": "human", "creature_type": "humanoid"},
|
||||||
|
{"id": 2, "name": "dragonborn", "creature_type": "humanoid"},
|
||||||
|
{"id": 3, "name": "tiefling", "creature_type": "humanoid"},
|
||||||
|
{"id": 4, "name": "elf", "creature_type": "humanoid"},
|
||||||
|
],
|
||||||
|
"AncestryTrait": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "+1 to All Ability Scores",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Breath Weapon",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "Darkvision",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"AncestryTraitMap": [
|
||||||
|
{"ancestry_id": 1, "ancestry_trait_id": 1, "level": 1}, # human +1 to scores
|
||||||
|
{"ancestry_id": 2, "ancestry_trait_id": 2, "level": 1}, # dragonborn breath weapon
|
||||||
|
{"ancestry_id": 3, "ancestry_trait_id": 3, "level": 1}, # tiefling darkvision
|
||||||
|
{"ancestry_id": 2, "ancestry_trait_id": 2, "level": 1}, # elf darkvision
|
||||||
|
],
|
||||||
|
"CharacterClassMap": [
|
||||||
|
{
|
||||||
|
"character_id": 1,
|
||||||
|
"character_class_id": 1,
|
||||||
|
"level": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": 1,
|
||||||
|
"character_class_id": 2,
|
||||||
|
"level": 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"Character": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Sabetha",
|
||||||
|
"ancestry_id": 1,
|
||||||
|
"armor_class": 10,
|
||||||
|
"max_hit_points": 14,
|
||||||
|
"hit_points": 14,
|
||||||
|
"temp_hit_points": 0,
|
||||||
|
"speed": 30,
|
||||||
|
"str": 16,
|
||||||
|
"dex": 12,
|
||||||
|
"con": 18,
|
||||||
|
"int": 11,
|
||||||
|
"wis": 12,
|
||||||
|
"cha": 8,
|
||||||
|
"proficiencies": "all armor, all shields, simple weapons, martial weapons",
|
||||||
|
"saving_throws": ["STR", "CON"],
|
||||||
|
"skills": ["Acrobatics", "Animal Handling"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"ClassAttribute": [
|
||||||
|
{"id": 1, "name": "Fighting Style"},
|
||||||
|
{"id": 2, "name": "Another Attribute"},
|
||||||
|
],
|
||||||
|
"ClassAttributeOption": [
|
||||||
|
{"id": 1, "attribute_id": 1, "name": "Archery"},
|
||||||
|
{"id": 2, "attribute_id": 1, "name": "Battlemaster"},
|
||||||
|
{"id": 3, "attribute_id": 2, "name": "Another Option 1"},
|
||||||
|
{"id": 4, "attribute_id": 2, "name": "Another Option 2"},
|
||||||
|
],
|
||||||
|
"ClassAttributeMap": [
|
||||||
|
{"class_attribute_id": 1, "character_class_id": 1, "level": 2}, # Fighter: Fighting Style
|
||||||
|
{"class_attribute_id": 2, "character_class_id": 1, "level": 1}, # Fighter: Another Attr
|
||||||
|
],
|
||||||
|
"CharacterClassAttributeMap": [
|
||||||
|
{"character_id": 1, "class_attribute_id": 2, "option_id": 4}, # Sabetha, another option, option 2
|
||||||
|
{"character_id": 1, "class_attribute_id": 1, "option_id": 1}, # Sabetha, fighting style, archery
|
||||||
|
],
|
||||||
|
"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": "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": "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": "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",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def bootstrap():
|
||||||
|
"""
|
||||||
|
Initialize the database with source data. Idempotent; will skip anything that already exists.
|
||||||
|
"""
|
||||||
|
db.init()
|
||||||
|
for table, records in data.items():
|
||||||
|
model = getattr(schema, table)
|
||||||
|
|
||||||
|
for rec in records:
|
||||||
|
obj = model(**rec)
|
||||||
|
try:
|
||||||
|
with db.transaction():
|
||||||
|
db.session.add(obj)
|
||||||
|
logging.info(f"Created {table} {obj}")
|
||||||
|
except IntegrityError as e:
|
||||||
|
if "UNIQUE constraint failed" in str(e):
|
||||||
|
logging.info(f"Skipping existing {table} {obj}")
|
||||||
|
continue
|
||||||
|
raise
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
import os
|
|
||||||
import transaction
|
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import os
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
from pyramid_sqlalchemy import Session
|
import transaction
|
||||||
from pyramid_sqlalchemy import init_sqlalchemy
|
from pyramid_sqlalchemy import Session, init_sqlalchemy
|
||||||
from pyramid_sqlalchemy import metadata as _metadata
|
from pyramid_sqlalchemy import metadata as _metadata
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
|
import ttfrog.db.schema
|
||||||
|
from ttfrog.path import database
|
||||||
|
|
||||||
# from sqlalchemy.exc import IntegrityError
|
# from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
from ttfrog.path import database
|
|
||||||
import ttfrog.db.schema
|
|
||||||
|
|
||||||
ttfrog.db.schema
|
ttfrog.db.schema
|
||||||
|
|
||||||
@@ -23,9 +22,10 @@ class SQLDatabaseManager:
|
|||||||
"""
|
"""
|
||||||
A context manager for working with sqllite database.
|
A context manager for working with sqllite database.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def url(self):
|
def url(self):
|
||||||
return os.environ.get('DATABASE_URL', f"sqlite:///{database()}")
|
return os.environ.get("DATABASE_URL", f"sqlite:///{database()}")
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def engine(self):
|
def engine(self):
|
||||||
@@ -64,7 +64,7 @@ class SQLDatabaseManager:
|
|||||||
"""
|
"""
|
||||||
Create a uniquish slug from a dictionary.
|
Create a uniquish slug from a dictionary.
|
||||||
"""
|
"""
|
||||||
sha1bytes = hashlib.sha1(str(rec['id']).encode())
|
sha1bytes = hashlib.sha1(str(rec["id"]).encode())
|
||||||
return base64.urlsafe_b64encode(sha1bytes.digest()).decode("ascii")[:10]
|
return base64.urlsafe_b64encode(sha1bytes.digest()).decode("ascii")[:10]
|
||||||
|
|
||||||
def init(self):
|
def init(self):
|
||||||
@@ -73,7 +73,7 @@ class SQLDatabaseManager:
|
|||||||
|
|
||||||
def dump(self):
|
def dump(self):
|
||||||
results = {}
|
results = {}
|
||||||
for (table_name, table) in self.tables.items():
|
for table_name, table in self.tables.items():
|
||||||
results[table_name] = [row for row in self.query(table).all()]
|
results[table_name] = [row for row in self.query(table).all()]
|
||||||
return results
|
return results
|
||||||
|
|
||||||
File renamed without changes.
@@ -1,24 +1,16 @@
|
|||||||
from ttfrog.db.base import Bases, BaseObject, IterableMixin, SavingThrowsMixin, SkillsMixin
|
from sqlalchemy import Column, Enum, ForeignKey, Integer, String, Text, UniqueConstraint
|
||||||
from ttfrog.db.base import CreatureTypesEnum
|
|
||||||
|
|
||||||
from sqlalchemy import Column
|
|
||||||
from sqlalchemy import Enum
|
|
||||||
from sqlalchemy import Integer
|
|
||||||
from sqlalchemy import String
|
|
||||||
from sqlalchemy import Text
|
|
||||||
from sqlalchemy import ForeignKey
|
|
||||||
from sqlalchemy import UniqueConstraint
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from sqlalchemy.ext.associationproxy import association_proxy
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from ttfrog.db.base import BaseObject, Bases, CreatureTypesEnum, IterableMixin, SavingThrowsMixin, SkillsMixin
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'Ancestry',
|
"Ancestry",
|
||||||
'AncestryTrait',
|
"AncestryTrait",
|
||||||
'AncestryTraitMap',
|
"AncestryTraitMap",
|
||||||
'CharacterClassMap',
|
"CharacterClassMap",
|
||||||
'CharacterClassAttributeMap',
|
"CharacterClassAttributeMap",
|
||||||
'Character',
|
"Character",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -39,19 +31,20 @@ class AncestryTraitMap(BaseObject):
|
|||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
ancestry_id = Column(Integer, ForeignKey("ancestry.id"))
|
ancestry_id = Column(Integer, ForeignKey("ancestry.id"))
|
||||||
ancestry_trait_id = Column(Integer, ForeignKey("ancestry_trait.id"))
|
ancestry_trait_id = Column(Integer, ForeignKey("ancestry_trait.id"))
|
||||||
trait = relationship("AncestryTrait", lazy='immediate')
|
trait = relationship("AncestryTrait", lazy="immediate")
|
||||||
level = Column(Integer, nullable=False, info={'min': 1, 'max': 20})
|
level = Column(Integer, nullable=False, info={"min": 1, "max": 20})
|
||||||
|
|
||||||
|
|
||||||
class Ancestry(*Bases):
|
class Ancestry(*Bases):
|
||||||
"""
|
"""
|
||||||
A character ancestry ("race"), which has zero or more AncestryTraits.
|
A character ancestry ("race"), which has zero or more AncestryTraits.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__tablename__ = "ancestry"
|
__tablename__ = "ancestry"
|
||||||
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("AncestryTraitMap", lazy='immediate')
|
traits = relationship("AncestryTraitMap", lazy="immediate")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -61,6 +54,7 @@ class AncestryTrait(BaseObject, IterableMixin):
|
|||||||
"""
|
"""
|
||||||
A trait granted to a character via its Ancestry.
|
A trait granted to a character via its Ancestry.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__tablename__ = "ancestry_trait"
|
__tablename__ = "ancestry_trait"
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
name = Column(String, nullable=False)
|
name = Column(String, nullable=False)
|
||||||
@@ -76,9 +70,9 @@ class CharacterClassMap(BaseObject, IterableMixin):
|
|||||||
character_id = Column(Integer, ForeignKey("character.id"))
|
character_id = Column(Integer, ForeignKey("character.id"))
|
||||||
character_class_id = Column(Integer, ForeignKey("character_class.id"))
|
character_class_id = Column(Integer, ForeignKey("character_class.id"))
|
||||||
mapping = UniqueConstraint(character_id, character_class_id)
|
mapping = UniqueConstraint(character_id, character_class_id)
|
||||||
level = Column(Integer, nullable=False, info={'min': 1, 'max': 20}, default=1)
|
level = Column(Integer, nullable=False, info={"min": 1, "max": 20}, default=1)
|
||||||
|
|
||||||
character_class = relationship("CharacterClass", lazy='immediate')
|
character_class = relationship("CharacterClass", lazy="immediate")
|
||||||
character = relationship("Character", uselist=False, viewonly=True)
|
character = relationship("Character", uselist=False, viewonly=True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@@ -93,42 +87,42 @@ class CharacterClassAttributeMap(BaseObject, IterableMixin):
|
|||||||
option_id = Column(Integer, ForeignKey("class_attribute_option.id"), nullable=False)
|
option_id = Column(Integer, ForeignKey("class_attribute_option.id"), nullable=False)
|
||||||
mapping = UniqueConstraint(character_id, class_attribute_id)
|
mapping = UniqueConstraint(character_id, class_attribute_id)
|
||||||
|
|
||||||
class_attribute = relationship("ClassAttribute", lazy='immediate')
|
class_attribute = relationship("ClassAttribute", lazy="immediate")
|
||||||
option = relationship("ClassAttributeOption", lazy='immediate')
|
option = relationship("ClassAttributeOption", lazy="immediate")
|
||||||
|
|
||||||
character_class = relationship(
|
character_class = relationship(
|
||||||
"CharacterClass",
|
"CharacterClass",
|
||||||
secondary="class_map",
|
secondary="class_map",
|
||||||
primaryjoin="CharacterClassAttributeMap.character_id == CharacterClassMap.character_id",
|
primaryjoin="CharacterClassAttributeMap.character_id == CharacterClassMap.character_id",
|
||||||
secondaryjoin="CharacterClass.id == CharacterClassMap.character_class_id",
|
secondaryjoin="CharacterClass.id == CharacterClassMap.character_class_id",
|
||||||
viewonly=True
|
viewonly=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Character(*Bases, SavingThrowsMixin, SkillsMixin):
|
class Character(*Bases, SavingThrowsMixin, SkillsMixin):
|
||||||
__tablename__ = "character"
|
__tablename__ = "character"
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
name = Column(String, default='New Character', nullable=False)
|
name = Column(String, default="New Character", nullable=False)
|
||||||
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, 'max': 999})
|
temp_hit_points = Column(Integer, default=0, nullable=False, info={"min": 0, "max": 999})
|
||||||
speed = Column(Integer, nullable=False, default=30, info={'min': 0, 'max': 99})
|
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})
|
||||||
int = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
|
int = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
||||||
wis = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
|
wis = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
||||||
cha = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
|
cha = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
||||||
proficiencies = Column(String)
|
proficiencies = Column(String)
|
||||||
|
|
||||||
class_map = relationship("CharacterClassMap", cascade='all,delete,delete-orphan')
|
class_map = relationship("CharacterClassMap", cascade="all,delete,delete-orphan")
|
||||||
classes = association_proxy('class_map', 'id', creator=class_map_creator)
|
classes = association_proxy("class_map", "id", creator=class_map_creator)
|
||||||
|
|
||||||
character_class_attribute_map = relationship("CharacterClassAttributeMap", cascade='all,delete,delete-orphan')
|
character_class_attribute_map = relationship("CharacterClassAttributeMap", cascade="all,delete,delete-orphan")
|
||||||
class_attributes = association_proxy('character_class_attribute_map', 'id', creator=attr_map_creator)
|
class_attributes = association_proxy("character_class_attribute_map", "id", creator=attr_map_creator)
|
||||||
|
|
||||||
ancestry_id = Column(Integer, ForeignKey("ancestry.id"), nullable=False, default='1')
|
ancestry_id = Column(Integer, ForeignKey("ancestry.id"), nullable=False, default="1")
|
||||||
ancestry = relationship("Ancestry", uselist=False)
|
ancestry = relationship("Ancestry", uselist=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -151,11 +145,7 @@ class Character(*Bases, SavingThrowsMixin, SkillsMixin):
|
|||||||
level_in_class = level_in_class[0]
|
level_in_class = level_in_class[0]
|
||||||
level_in_class.level = level
|
level_in_class.level = level
|
||||||
return
|
return
|
||||||
self.classes.append(CharacterClassMap(
|
self.classes.append(CharacterClassMap(character_id=self.id, character_class_id=newclass.id, level=level))
|
||||||
character_id=self.id,
|
|
||||||
character_class_id=newclass.id,
|
|
||||||
level=level
|
|
||||||
))
|
|
||||||
|
|
||||||
def remove_class(self, target):
|
def remove_class(self, target):
|
||||||
self.class_map = [m for m in self.class_map if m.id != target.id]
|
self.class_map = [m for m in self.class_map if m.id != target.id]
|
||||||
@@ -1,20 +1,13 @@
|
|||||||
from ttfrog.db.base import Bases, BaseObject, IterableMixin, SavingThrowsMixin, SkillsMixin
|
from sqlalchemy import Column, Enum, ForeignKey, Integer, String
|
||||||
from ttfrog.db.base import StatsEnum
|
|
||||||
|
|
||||||
from sqlalchemy import Column
|
|
||||||
from sqlalchemy import Enum
|
|
||||||
from sqlalchemy import Integer
|
|
||||||
from sqlalchemy import String
|
|
||||||
from sqlalchemy import Text
|
|
||||||
from sqlalchemy import ForeignKey
|
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from ttfrog.db.base import BaseObject, Bases, IterableMixin, SavingThrowsMixin, SkillsMixin, StatsEnum
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'ClassAttributeMap',
|
"ClassAttributeMap",
|
||||||
'ClassAttribute',
|
"ClassAttribute",
|
||||||
'ClassAttributeOption',
|
"ClassAttributeOption",
|
||||||
'CharacterClass',
|
"CharacterClass",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -22,7 +15,7 @@ class ClassAttributeMap(BaseObject, IterableMixin):
|
|||||||
__tablename__ = "class_attribute_map"
|
__tablename__ = "class_attribute_map"
|
||||||
class_attribute_id = Column(Integer, ForeignKey("class_attribute.id"), primary_key=True)
|
class_attribute_id = Column(Integer, ForeignKey("class_attribute.id"), primary_key=True)
|
||||||
character_class_id = Column(Integer, ForeignKey("character_class.id"), primary_key=True)
|
character_class_id = Column(Integer, ForeignKey("character_class.id"), primary_key=True)
|
||||||
level = Column(Integer, nullable=False, info={'min': 1, 'max': 20}, default=1)
|
level = Column(Integer, nullable=False, info={"min": 1, "max": 20}, default=1)
|
||||||
|
|
||||||
|
|
||||||
class ClassAttribute(BaseObject, IterableMixin):
|
class ClassAttribute(BaseObject, IterableMixin):
|
||||||
@@ -46,7 +39,7 @@ class CharacterClass(*Bases, SavingThrowsMixin, SkillsMixin):
|
|||||||
__tablename__ = "character_class"
|
__tablename__ = "character_class"
|
||||||
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)
|
||||||
hit_dice = Column(String, default='1d6')
|
hit_dice = Column(String, default="1d6")
|
||||||
hit_dice_stat = Column(Enum(StatsEnum))
|
hit_dice_stat = Column(Enum(StatsEnum))
|
||||||
proficiencies = Column(String)
|
proficiencies = Column(String)
|
||||||
attributes = relationship("ClassAttributeMap")
|
attributes = relationship("ClassAttributeMap")
|
||||||
@@ -1,16 +1,11 @@
|
|||||||
from ttfrog.db.base import Bases, BaseObject, IterableMixin
|
from sqlalchemy import Column, Integer, String, Text, UniqueConstraint
|
||||||
|
|
||||||
from sqlalchemy import Column
|
|
||||||
from sqlalchemy import Integer
|
|
||||||
from sqlalchemy import String
|
|
||||||
from sqlalchemy import Text
|
|
||||||
from sqlalchemy import UniqueConstraint
|
|
||||||
|
|
||||||
|
from ttfrog.db.base import BaseObject, Bases, IterableMixin
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'Skill',
|
"Skill",
|
||||||
'Proficiency',
|
"Proficiency",
|
||||||
'Modifier',
|
"Modifier",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -35,9 +30,7 @@ class Proficiency(*Bases):
|
|||||||
|
|
||||||
class Modifier(BaseObject, IterableMixin):
|
class Modifier(BaseObject, IterableMixin):
|
||||||
__tablename__ = "modifier"
|
__tablename__ = "modifier"
|
||||||
__table_args__ = (
|
__table_args__ = (UniqueConstraint("source_table_name", "source_table_id", "value", "type", "target"),)
|
||||||
UniqueConstraint('source_table_name', 'source_table_id', 'value', 'type', 'target'),
|
|
||||||
)
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
source_table_name = Column(String, index=True, nullable=False)
|
source_table_name = Column(String, index=True, nullable=False)
|
||||||
source_table_id = Column(Integer, index=True, nullable=False)
|
source_table_id = Column(Integer, index=True, nullable=False)
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, Text
|
||||||
|
|
||||||
from ttfrog.db.base import BaseObject, IterableMixin
|
from ttfrog.db.base import BaseObject, IterableMixin
|
||||||
from sqlalchemy import Column
|
|
||||||
from sqlalchemy import Integer
|
__all__ = ["TransactionLog"]
|
||||||
from sqlalchemy import String
|
|
||||||
from sqlalchemy import Text
|
|
||||||
|
|
||||||
__all__ = ['TransactionLog']
|
|
||||||
|
|
||||||
class TransactionLog(BaseObject, IterableMixin):
|
class TransactionLog(BaseObject, IterableMixin):
|
||||||
__tablename__ = "transaction_log"
|
__tablename__ = "transaction_log"
|
||||||
File renamed without changes.
@@ -0,0 +1,29 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_setup_hint = "You may be able to solve this error by running 'ttfrog setup' or specifying the --root parameter."
|
||||||
|
|
||||||
|
|
||||||
|
def database():
|
||||||
|
path = Path(os.environ["DATA_PATH"]).expanduser()
|
||||||
|
if not path.exists() or not path.is_dir():
|
||||||
|
raise RuntimeError(f"DATA_PATH {path} doesn't exist or isn't a directory.\n\n{_setup_hint}")
|
||||||
|
return path / Path("tabletop-frog.db")
|
||||||
|
|
||||||
|
|
||||||
|
def assets():
|
||||||
|
return Path(__file__).parent / "assets"
|
||||||
|
|
||||||
|
|
||||||
|
def templates():
|
||||||
|
try:
|
||||||
|
return Path(os.environ["TEMPLATES_PATH"])
|
||||||
|
except KeyError:
|
||||||
|
return assets() / "templates"
|
||||||
|
|
||||||
|
|
||||||
|
def static_files():
|
||||||
|
try:
|
||||||
|
return Path(os.environ["STATIC_FILES_PATH"])
|
||||||
|
except KeyError:
|
||||||
|
return assets() / "public"
|
||||||
File renamed without changes.
@@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from wsgiref.simple_server import make_server
|
from wsgiref.simple_server import make_server
|
||||||
|
|
||||||
from pyramid.config import Configurator
|
from pyramid.config import Configurator
|
||||||
|
|
||||||
from ttfrog.db.manager import db
|
from ttfrog.db.manager import db
|
||||||
@@ -8,15 +8,12 @@ from ttfrog.webserver.routes import routes
|
|||||||
|
|
||||||
|
|
||||||
def configuration():
|
def configuration():
|
||||||
config = Configurator(settings={
|
config = Configurator(settings={"sqlalchemy.url": db.url, "jinja2.directories": "ttfrog.assets:templates/"})
|
||||||
'sqlalchemy.url': db.url,
|
config.include("pyramid_tm")
|
||||||
'jinja2.directories': 'ttfrog.assets:templates/'
|
config.include("pyramid_sqlalchemy")
|
||||||
})
|
config.include("pyramid_jinja2")
|
||||||
config.include('pyramid_tm')
|
config.add_static_view(name="/static", path="ttfrog.assets:static/")
|
||||||
config.include('pyramid_sqlalchemy')
|
config.add_jinja2_renderer(".html", settings_prefix="jinja2.")
|
||||||
config.include('pyramid_jinja2')
|
|
||||||
config.add_static_view(name='/static', path='ttfrog.assets:static/')
|
|
||||||
config.add_jinja2_renderer('.html', settings_prefix='jinja2.')
|
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
@@ -25,5 +22,5 @@ def start(host: str, port: int, debug: bool = False) -> None:
|
|||||||
logging.debug(f"Configuring webserver with {host=}, {port=}, {debug=}")
|
logging.debug(f"Configuring webserver with {host=}, {port=}, {debug=}")
|
||||||
config = configuration()
|
config = configuration()
|
||||||
config.include(routes)
|
config.include(routes)
|
||||||
config.scan('ttfrog.webserver.views')
|
config.scan("ttfrog.webserver.views")
|
||||||
make_server(host, int(port), config.make_wsgi_app()).serve_forever()
|
make_server(host, int(port), config.make_wsgi_app()).serve_forever()
|
||||||
File renamed without changes.
+4
-3
@@ -1,12 +1,13 @@
|
|||||||
from ttfrog.db.schema import Ancestry
|
|
||||||
from ttfrog.db.manager import db
|
|
||||||
from wtforms_alchemy import ModelForm
|
from wtforms_alchemy import ModelForm
|
||||||
|
|
||||||
|
from ttfrog.db.manager import db
|
||||||
|
from ttfrog.db.schema import Ancestry
|
||||||
|
|
||||||
|
|
||||||
class AncestryForm(ModelForm):
|
class AncestryForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Ancestry
|
model = Ancestry
|
||||||
exclude = ['slug']
|
exclude = ["slug"]
|
||||||
|
|
||||||
def get_session():
|
def get_session():
|
||||||
return db.session
|
return db.session
|
||||||
@@ -1,27 +1,25 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from pyramid.httpexceptions import HTTPFound
|
from pyramid.httpexceptions import HTTPFound
|
||||||
from pyramid.interfaces import IRoutesMapper
|
from pyramid.interfaces import IRoutesMapper
|
||||||
|
|
||||||
from ttfrog.db.manager import db
|
from ttfrog.db.manager import db
|
||||||
from ttfrog.db import transaction_log
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_routes(request):
|
def get_all_routes(request):
|
||||||
routes = {
|
routes = {
|
||||||
'static': '/static',
|
"static": "/static",
|
||||||
}
|
}
|
||||||
uri_pattern = re.compile(r"^([^\{\*]+)")
|
uri_pattern = re.compile(r"^([^\{\*]+)")
|
||||||
mapper = request.registry.queryUtility(IRoutesMapper)
|
mapper = request.registry.queryUtility(IRoutesMapper)
|
||||||
for route in mapper.get_routes():
|
for route in mapper.get_routes():
|
||||||
if route.name.startswith('__'):
|
if route.name.startswith("__"):
|
||||||
continue
|
continue
|
||||||
m = uri_pattern.search(route.pattern)
|
m = uri_pattern.search(route.pattern)
|
||||||
if m:
|
if m:
|
||||||
routes[route.name] = m .group(0)
|
routes[route.name] = m.group(0)
|
||||||
return routes
|
return routes
|
||||||
|
|
||||||
|
|
||||||
@@ -36,17 +34,14 @@ class BaseController:
|
|||||||
self._record = None
|
self._record = None
|
||||||
self._form = None
|
self._form = None
|
||||||
|
|
||||||
self.config = {
|
self.config = {"static_url": "/static", "project_name": "TTFROG"}
|
||||||
'static_url': '/static',
|
|
||||||
'project_name': 'TTFROG'
|
|
||||||
}
|
|
||||||
self.configure_for_model()
|
self.configure_for_model()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def slug(self):
|
def slug(self):
|
||||||
if not self._slug:
|
if not self._slug:
|
||||||
parts = self.request.matchdict.get('uri', '').split('-')
|
parts = self.request.matchdict.get("uri", "").split("-")
|
||||||
self._slug = parts[0].replace('/', '')
|
self._slug = parts[0].replace("/", "")
|
||||||
return self._slug
|
return self._slug
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -78,12 +73,12 @@ class BaseController:
|
|||||||
@property
|
@property
|
||||||
def resources(self):
|
def resources(self):
|
||||||
return [
|
return [
|
||||||
{'type': 'style', 'uri': 'css/styles.css'},
|
{"type": "style", "uri": "css/styles.css"},
|
||||||
]
|
]
|
||||||
|
|
||||||
def configure_for_model(self):
|
def configure_for_model(self):
|
||||||
if 'all_records' not in self.attrs:
|
if "all_records" not in self.attrs:
|
||||||
self.attrs['all_records'] = db.query(self.model).all()
|
self.attrs["all_records"] = db.query(self.model).all()
|
||||||
|
|
||||||
def template_context(self, **kwargs) -> dict:
|
def template_context(self, **kwargs) -> dict:
|
||||||
return dict(
|
return dict(
|
||||||
@@ -103,14 +98,14 @@ class BaseController:
|
|||||||
def populate_association(self, key, formdata):
|
def populate_association(self, key, formdata):
|
||||||
populated = []
|
populated = []
|
||||||
for field in formdata:
|
for field in formdata:
|
||||||
map_id = field.pop('id')
|
map_id = field.pop("id")
|
||||||
map_id = int(map_id) if map_id else 0
|
map_id = int(map_id) if map_id else 0
|
||||||
if not field[key]:
|
if not field[key]:
|
||||||
continue
|
continue
|
||||||
elif not map_id:
|
elif not map_id:
|
||||||
populated.append(field)
|
populated.append(field)
|
||||||
else:
|
else:
|
||||||
field['id'] = map_id
|
field["id"] = map_id
|
||||||
populated.append(field)
|
populated.append(field)
|
||||||
return populated
|
return populated
|
||||||
|
|
||||||
+46
-58
@@ -1,29 +1,25 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ttfrog.webserver.controllers.base import BaseController
|
from markupsafe import Markup
|
||||||
from ttfrog.webserver.forms import DeferredSelectField
|
from wtforms import ValidationError
|
||||||
from ttfrog.webserver.forms import NullableDeferredSelectField
|
from wtforms.fields import FieldList, FormField, HiddenField, SelectField, SelectMultipleField, SubmitField
|
||||||
from ttfrog.db.schema import (
|
from wtforms.validators import Optional
|
||||||
Character,
|
from wtforms.widgets import ListWidget, Select
|
||||||
Ancestry,
|
from wtforms.widgets.core import html_params
|
||||||
CharacterClass,
|
from wtforms_alchemy import ModelForm
|
||||||
CharacterClassMap,
|
|
||||||
ClassAttributeOption,
|
|
||||||
CharacterClassAttributeMap
|
|
||||||
)
|
|
||||||
|
|
||||||
from ttfrog.db.base import STATS
|
from ttfrog.db.base import STATS
|
||||||
from ttfrog.db.manager import db
|
from ttfrog.db.manager import db
|
||||||
|
from ttfrog.db.schema import (
|
||||||
from wtforms_alchemy import ModelForm
|
Ancestry,
|
||||||
from wtforms.fields import SubmitField, SelectField, SelectMultipleField, FieldList, FormField, HiddenField
|
Character,
|
||||||
from wtforms.widgets import Select, ListWidget
|
CharacterClass,
|
||||||
from wtforms import ValidationError
|
CharacterClassAttributeMap,
|
||||||
from wtforms.validators import Optional
|
CharacterClassMap,
|
||||||
from wtforms.widgets.core import html_params
|
ClassAttributeOption,
|
||||||
|
)
|
||||||
from markupsafe import Markup
|
from ttfrog.webserver.controllers.base import BaseController
|
||||||
|
from ttfrog.webserver.forms import DeferredSelectField, NullableDeferredSelectField
|
||||||
|
|
||||||
VALID_LEVELS = range(1, 21)
|
VALID_LEVELS = range(1, 21)
|
||||||
|
|
||||||
@@ -48,7 +44,7 @@ class ClassAttributesFormField(FormField):
|
|||||||
|
|
||||||
def process(self, *args, **kwargs):
|
def process(self, *args, **kwargs):
|
||||||
super().process(*args, **kwargs)
|
super().process(*args, **kwargs)
|
||||||
self.character_class_map = db.query(CharacterClassAttributeMap).get(self.data['id'])
|
self.character_class_map = db.query(CharacterClassAttributeMap).get(self.data["id"])
|
||||||
self.label.text = self.character_class_map.character_class[0].name
|
self.label.text = self.character_class_map.character_class[0].name
|
||||||
|
|
||||||
|
|
||||||
@@ -56,12 +52,7 @@ class ClassAttributesForm(ModelForm):
|
|||||||
id = HiddenField()
|
id = HiddenField()
|
||||||
class_attribute_id = HiddenField()
|
class_attribute_id = HiddenField()
|
||||||
|
|
||||||
option_id = SelectField(
|
option_id = SelectField(widget=Select(), choices=[], validators=[Optional()], coerce=int)
|
||||||
widget=Select(),
|
|
||||||
choices=[],
|
|
||||||
validators=[Optional()],
|
|
||||||
coerce=int
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, formdata=None, obj=None, prefix=None):
|
def __init__(self, formdata=None, obj=None, prefix=None):
|
||||||
if obj:
|
if obj:
|
||||||
@@ -74,13 +65,9 @@ class ClassAttributesForm(ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class MulticlassForm(ModelForm):
|
class MulticlassForm(ModelForm):
|
||||||
|
|
||||||
id = HiddenField()
|
id = HiddenField()
|
||||||
character_class_id = NullableDeferredSelectField(
|
character_class_id = NullableDeferredSelectField(
|
||||||
model=CharacterClass,
|
model=CharacterClass, validate_choice=True, widget=Select(), coerce=int
|
||||||
validate_choice=True,
|
|
||||||
widget=Select(),
|
|
||||||
coerce=int
|
|
||||||
)
|
)
|
||||||
level = SelectField(choices=VALID_LEVELS, default=1, coerce=int, validate_choice=True, widget=Select())
|
level = SelectField(choices=VALID_LEVELS, default=1, coerce=int, validate_choice=True, widget=Select())
|
||||||
|
|
||||||
@@ -98,20 +85,19 @@ class MulticlassForm(ModelForm):
|
|||||||
class CharacterForm(ModelForm):
|
class CharacterForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Character
|
model = Character
|
||||||
exclude = ['slug']
|
exclude = ["slug"]
|
||||||
|
|
||||||
save = SubmitField()
|
save = SubmitField()
|
||||||
delete = SubmitField()
|
delete = SubmitField()
|
||||||
ancestry_id = DeferredSelectField('Ancestry', model=Ancestry, default=1, validate_choice=True, widget=Select())
|
ancestry_id = DeferredSelectField("Ancestry", model=Ancestry, default=1, validate_choice=True, widget=Select())
|
||||||
classes = FieldList(FormField(MulticlassForm, label=None, widget=ListWidget()), min_entries=0)
|
classes = FieldList(FormField(MulticlassForm, label=None, widget=ListWidget()), min_entries=0)
|
||||||
newclass = FormField(MulticlassForm, widget=ListWidget())
|
newclass = FormField(MulticlassForm, widget=ListWidget())
|
||||||
|
|
||||||
class_attributes = FieldList(
|
class_attributes = FieldList(
|
||||||
ClassAttributesFormField(ClassAttributesForm, widget=ClassAttributeWidget()),
|
ClassAttributesFormField(ClassAttributesForm, widget=ClassAttributeWidget()), min_entries=1
|
||||||
min_entries=1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
saving_throws = SelectMultipleField('Saving Throws', validate_choice=True, choices=STATS)
|
saving_throws = SelectMultipleField("Saving Throws", validate_choice=True, choices=STATS)
|
||||||
|
|
||||||
|
|
||||||
class CharacterSheet(BaseController):
|
class CharacterSheet(BaseController):
|
||||||
@@ -121,7 +107,7 @@ class CharacterSheet(BaseController):
|
|||||||
@property
|
@property
|
||||||
def resources(self):
|
def resources(self):
|
||||||
return super().resources + [
|
return super().resources + [
|
||||||
{'type': 'script', 'uri': 'js/character_sheet.js'},
|
{"type": "script", "uri": "js/character_sheet.js"},
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate_callback(self):
|
def validate_callback(self):
|
||||||
@@ -129,13 +115,13 @@ class CharacterSheet(BaseController):
|
|||||||
Validate multiclass fields in form data.
|
Validate multiclass fields in form data.
|
||||||
"""
|
"""
|
||||||
ret = super().validate()
|
ret = super().validate()
|
||||||
if not self.form.data['classes']:
|
if not self.form.data["classes"]:
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
err = ""
|
err = ""
|
||||||
total_level = 0
|
total_level = 0
|
||||||
for field in self.form.data['classes']:
|
for field in self.form.data["classes"]:
|
||||||
level = field.get('level')
|
level = field.get("level")
|
||||||
total_level += level
|
total_level += level
|
||||||
if level not in VALID_LEVELS:
|
if level not in VALID_LEVELS:
|
||||||
err = f"Multiclass form field {field = } level is outside possible range."
|
err = f"Multiclass form field {field = } level is outside possible range."
|
||||||
@@ -150,9 +136,10 @@ class CharacterSheet(BaseController):
|
|||||||
def add_class_attributes(self):
|
def add_class_attributes(self):
|
||||||
# prefetch the records for each of the character's classes
|
# prefetch the records for each of the character's classes
|
||||||
classes_by_id = {
|
classes_by_id = {
|
||||||
c.id: c for c in db.query(CharacterClass).filter(CharacterClass.id.in_(
|
c.id: c
|
||||||
c.character_class_id for c in self.record.class_map
|
for c in db.query(CharacterClass)
|
||||||
)).all()
|
.filter(CharacterClass.id.in_(c.character_class_id for c in self.record.class_map))
|
||||||
|
.all()
|
||||||
}
|
}
|
||||||
|
|
||||||
assigned = [int(m.class_attribute_id) for m in self.record.character_class_attribute_map]
|
assigned = [int(m.class_attribute_id) for m in self.record.character_class_attribute_map]
|
||||||
@@ -165,18 +152,19 @@ class CharacterSheet(BaseController):
|
|||||||
# assign each class attribute available at the character's current
|
# assign each class attribute available at the character's current
|
||||||
# level to the list of the character's class attributes
|
# level to the list of the character's class attributes
|
||||||
for attr_map in [a for a in thisclass.attributes if a.level <= class_map.level]:
|
for attr_map in [a for a in thisclass.attributes if a.level <= class_map.level]:
|
||||||
|
|
||||||
# when creating a record, assign the first of the available
|
# when creating a record, assign the first of the available
|
||||||
# options to the character's class attribute.
|
# options to the character's class attribute.
|
||||||
default_option = db.query(ClassAttributeOption).filter_by(
|
default_option = (
|
||||||
attribute_id=attr_map.class_attribute_id
|
db.query(ClassAttributeOption).filter_by(attribute_id=attr_map.class_attribute_id).first()
|
||||||
).first()
|
)
|
||||||
|
|
||||||
if attr_map.class_attribute_id not in assigned:
|
if attr_map.class_attribute_id not in assigned:
|
||||||
self.record.class_attributes.append({
|
self.record.class_attributes.append(
|
||||||
'class_attribute_id': attr_map.class_attribute_id,
|
{
|
||||||
'option_id': default_option.id,
|
"class_attribute_id": attr_map.class_attribute_id,
|
||||||
})
|
"option_id": default_option.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def save_callback(self):
|
def save_callback(self):
|
||||||
self.add_class_attributes()
|
self.add_class_attributes()
|
||||||
@@ -188,16 +176,16 @@ class CharacterSheet(BaseController):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# multiclass form
|
# multiclass form
|
||||||
classes_formdata = self.form.data['classes']
|
classes_formdata = self.form.data["classes"]
|
||||||
classes_formdata.append(self.form.data['newclass'])
|
classes_formdata.append(self.form.data["newclass"])
|
||||||
del self.form.classes
|
del self.form.classes
|
||||||
del self.form.newclass
|
del self.form.newclass
|
||||||
|
|
||||||
# class attributes
|
# class attributes
|
||||||
attrs_formdata = self.form.data['class_attributes']
|
attrs_formdata = self.form.data["class_attributes"]
|
||||||
del self.form.class_attributes
|
del self.form.class_attributes
|
||||||
|
|
||||||
super().populate()
|
super().populate()
|
||||||
|
|
||||||
self.record.classes = self.populate_association('character_class_id', classes_formdata)
|
self.record.classes = self.populate_association("character_class_id", classes_formdata)
|
||||||
self.record.class_attributes = self.populate_association('class_attribute_id', attrs_formdata)
|
self.record.class_attributes = self.populate_association("class_attribute_id", attrs_formdata)
|
||||||
+4
-8
@@ -1,10 +1,9 @@
|
|||||||
import logging
|
from pyramid.httpexceptions import exception_response
|
||||||
|
|
||||||
from ttfrog.db import schema
|
from ttfrog.db import schema
|
||||||
from ttfrog.db.manager import db
|
from ttfrog.db.manager import db
|
||||||
from .base import BaseController
|
|
||||||
|
|
||||||
from pyramid.httpexceptions import exception_response
|
from .base import BaseController
|
||||||
|
|
||||||
|
|
||||||
class JsonData(BaseController):
|
class JsonData(BaseController):
|
||||||
@@ -13,13 +12,10 @@ class JsonData(BaseController):
|
|||||||
|
|
||||||
def configure_for_model(self):
|
def configure_for_model(self):
|
||||||
try:
|
try:
|
||||||
self.model = getattr(schema, self.request.matchdict.get('table_name'))
|
self.model = getattr(schema, self.request.matchdict.get("table_name"))
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise exception_response(404)
|
raise exception_response(404)
|
||||||
|
|
||||||
def response(self):
|
def response(self):
|
||||||
query = db.query(self.model).filter_by(**self.request.params)
|
query = db.query(self.model).filter_by(**self.request.params)
|
||||||
return {
|
return {"table_name": self.model.__tablename__, "records": query.all()}
|
||||||
'table_name': self.model.__tablename__,
|
|
||||||
'records': query.all()
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from ttfrog.db.manager import db
|
|
||||||
from wtforms.fields import SelectField, SelectMultipleField
|
from wtforms.fields import SelectField, SelectMultipleField
|
||||||
|
|
||||||
|
from ttfrog.db.manager import db
|
||||||
|
|
||||||
|
|
||||||
class DeferredSelectMultipleField(SelectMultipleField):
|
class DeferredSelectMultipleField(SelectMultipleField):
|
||||||
def __init__(self, *args, model=None, **kwargs):
|
def __init__(self, *args, model=None, **kwargs):
|
||||||
@@ -11,10 +12,10 @@ class DeferredSelectMultipleField(SelectMultipleField):
|
|||||||
class DeferredSelectField(SelectField):
|
class DeferredSelectField(SelectField):
|
||||||
def __init__(self, *args, model=None, **kwargs):
|
def __init__(self, *args, model=None, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.choices = [(rec.id, getattr(rec, 'name', str(rec))) for rec in db.query(model).all()]
|
self.choices = [(rec.id, getattr(rec, "name", str(rec))) for rec in db.query(model).all()]
|
||||||
|
|
||||||
|
|
||||||
class NullableDeferredSelectField(DeferredSelectField):
|
class NullableDeferredSelectField(DeferredSelectField):
|
||||||
def __init__(self, *args, model=None, label='---', **kwargs):
|
def __init__(self, *args, model=None, label="---", **kwargs):
|
||||||
super().__init__(*args, model=model, **kwargs)
|
super().__init__(*args, model=model, **kwargs)
|
||||||
self.choices = [(0, label)] + self.choices
|
self.choices = [(0, label)] + self.choices
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
def routes(config):
|
||||||
|
config.add_route("index", "/")
|
||||||
|
config.add_route("sheet", "/c{uri:.*}", factory="ttfrog.webserver.controllers.CharacterSheet")
|
||||||
|
config.add_route("data", "/_/{table_name}{uri:.*}", factory="ttfrog.webserver.controllers.JsonData")
|
||||||
@@ -1,23 +1,26 @@
|
|||||||
from pyramid.response import Response
|
from pyramid.response import Response
|
||||||
from pyramid.view import view_config
|
from pyramid.view import view_config
|
||||||
|
|
||||||
|
from ttfrog.attribute_map import AttributeMap
|
||||||
from ttfrog.db.manager import db
|
from ttfrog.db.manager import db
|
||||||
from ttfrog.db.schema import Ancestry
|
from ttfrog.db.schema import Ancestry
|
||||||
from ttfrog.attribute_map import AttributeMap
|
|
||||||
|
|
||||||
def response_from(controller):
|
def response_from(controller):
|
||||||
return controller.response() or AttributeMap.from_dict({'c': controller.template_context()})
|
return controller.response() or AttributeMap.from_dict({"c": controller.template_context()})
|
||||||
|
|
||||||
|
|
||||||
@view_config(route_name='index')
|
@view_config(route_name="index")
|
||||||
def index(request):
|
def index(request):
|
||||||
ancestries = [a.name for a in db.session.query(Ancestry).all()]
|
ancestries = [a.name for a in db.session.query(Ancestry).all()]
|
||||||
return Response(','.join(ancestries))
|
return Response(",".join(ancestries))
|
||||||
|
|
||||||
|
|
||||||
@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')
|
|
||||||
|
@view_config(route_name="data", renderer="json")
|
||||||
def data(request):
|
def data(request):
|
||||||
return response_from(request.context)
|
return response_from(request.context)
|
||||||
+6
-7
@@ -7,8 +7,7 @@ import pytest
|
|||||||
from ttfrog.db import schema
|
from ttfrog.db import schema
|
||||||
from ttfrog.db.manager import db as _db
|
from ttfrog.db.manager import db as _db
|
||||||
|
|
||||||
|
FIXTURE_PATH = Path(__file__).parent / "fixtures"
|
||||||
FIXTURE_PATH = Path(__file__).parent / 'fixtures'
|
|
||||||
|
|
||||||
|
|
||||||
def load_fixture(db, fixture_name):
|
def load_fixture(db, fixture_name):
|
||||||
@@ -23,20 +22,20 @@ def load_fixture(db, fixture_name):
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def db(monkeypatch):
|
def db(monkeypatch):
|
||||||
monkeypatch.setattr('ttfrog.db.manager.database', MagicMock(return_value=""))
|
monkeypatch.setattr("ttfrog.db.manager.database", MagicMock(return_value=""))
|
||||||
monkeypatch.setenv('DATABASE_URL', "sqlite:///:memory:")
|
monkeypatch.setenv("DATABASE_URL", "sqlite:///:memory:")
|
||||||
monkeypatch.setenv('DEBUG', '1')
|
monkeypatch.setenv("DEBUG", "1")
|
||||||
_db.init()
|
_db.init()
|
||||||
return _db
|
return _db
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def classes(db):
|
def classes(db):
|
||||||
load_fixture(db, 'classes')
|
load_fixture(db, "classes")
|
||||||
return dict((rec.name, rec) for rec in db.session.query(schema.CharacterClass).all())
|
return dict((rec.name, rec) for rec in db.session.query(schema.CharacterClass).all())
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def ancestries(db):
|
def ancestries(db):
|
||||||
load_fixture(db, 'ancestry')
|
load_fixture(db, "ancestry")
|
||||||
return dict((rec.name, rec) for rec in db.session.query(schema.Ancestry).all())
|
return dict((rec.name, rec) for rec in db.session.query(schema.Ancestry).all())
|
||||||
+16
-16
@@ -3,54 +3,54 @@ from ttfrog.db import schema
|
|||||||
|
|
||||||
def test_create_character(db, classes, ancestries):
|
def test_create_character(db, classes, ancestries):
|
||||||
with db.transaction():
|
with db.transaction():
|
||||||
darkvision = db.session.query(schema.AncestryTrait).filter_by(name='Darkvision')[0]
|
darkvision = db.session.query(schema.AncestryTrait).filter_by(name="Darkvision")[0]
|
||||||
|
|
||||||
# create a human character (the default)
|
# create a human character (the default)
|
||||||
char = schema.Character(name='Test Character')
|
char = schema.Character(name="Test Character")
|
||||||
db.add(char)
|
db.add(char)
|
||||||
assert char.id == 1
|
assert char.id == 1
|
||||||
assert char.armor_class == 10
|
assert char.armor_class == 10
|
||||||
assert char.name == 'Test Character'
|
assert char.name == "Test Character"
|
||||||
assert char.ancestry.name == 'human'
|
assert char.ancestry.name == "human"
|
||||||
assert darkvision not in char.traits
|
assert darkvision not in char.traits
|
||||||
|
|
||||||
# switch ancestry to tiefling
|
# switch ancestry to tiefling
|
||||||
char.ancestry = ancestries['tiefling']
|
char.ancestry = ancestries["tiefling"]
|
||||||
db.add(char)
|
db.add(char)
|
||||||
char = db.session.get(schema.Character, 1)
|
char = db.session.get(schema.Character, 1)
|
||||||
assert char.ancestry.name == 'tiefling'
|
assert char.ancestry.name == "tiefling"
|
||||||
assert darkvision in char.traits
|
assert darkvision in char.traits
|
||||||
|
|
||||||
# assign a class and level
|
# assign a class and level
|
||||||
char.add_class(classes['fighter'], level=1)
|
char.add_class(classes["fighter"], level=1)
|
||||||
db.add(char)
|
db.add(char)
|
||||||
assert char.levels == {'fighter': 1}
|
assert char.levels == {"fighter": 1}
|
||||||
assert char.level == 1
|
assert char.level == 1
|
||||||
assert char.class_attributes == []
|
assert char.class_attributes == []
|
||||||
|
|
||||||
# level up
|
# level up
|
||||||
char.add_class(classes['fighter'], level=2)
|
char.add_class(classes["fighter"], level=2)
|
||||||
db.add(char)
|
db.add(char)
|
||||||
assert char.levels == {'fighter': 2}
|
assert char.levels == {"fighter": 2}
|
||||||
assert char.level == 2
|
assert char.level == 2
|
||||||
assert char.class_attributes == []
|
assert char.class_attributes == []
|
||||||
|
|
||||||
# multiclass
|
# multiclass
|
||||||
char.add_class(classes['rogue'], level=1)
|
char.add_class(classes["rogue"], level=1)
|
||||||
db.add(char)
|
db.add(char)
|
||||||
assert char.level == 3
|
assert char.level == 3
|
||||||
assert char.levels == {'fighter': 2, 'rogue': 1}
|
assert char.levels == {"fighter": 2, "rogue": 1}
|
||||||
|
|
||||||
# remove a class
|
# remove a class
|
||||||
char.remove_class(classes['rogue'])
|
char.remove_class(classes["rogue"])
|
||||||
db.add(char)
|
db.add(char)
|
||||||
assert char.levels == {'fighter': 2}
|
assert char.levels == {"fighter": 2}
|
||||||
assert char.level == 2
|
assert char.level == 2
|
||||||
|
|
||||||
# remove all remaining classes
|
# remove all remaining classes
|
||||||
char.remove_class(classes['fighter'])
|
char.remove_class(classes["fighter"])
|
||||||
db.add(char)
|
db.add(char)
|
||||||
|
|
||||||
# ensure we're not persisting any orphan records in the map table
|
# ensure we're not persisting any orphan records in the map table
|
||||||
dump = db.dump()
|
dump = db.dump()
|
||||||
assert dump['class_map'] == []
|
assert dump["class_map"] == []
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from ttfrog.db.manager import db
|
|
||||||
from ttfrog.db import schema
|
|
||||||
|
|
||||||
from sqlalchemy.exc import IntegrityError
|
|
||||||
|
|
||||||
# move this to json or whatever
|
|
||||||
data = {
|
|
||||||
'CharacterClass': [
|
|
||||||
{
|
|
||||||
'id': 1,
|
|
||||||
'name': 'fighter',
|
|
||||||
'hit_dice': '1d10',
|
|
||||||
'hit_dice_stat': 'CON',
|
|
||||||
'proficiencies': 'all armor, all shields, simple weapons, martial weapons',
|
|
||||||
'saving_throws': ['STR, CON'],
|
|
||||||
'skills': ['Acrobatics', 'Animal Handling', 'Athletics', 'History', 'Insight', 'Intimidation', 'Perception', 'Survival'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 2,
|
|
||||||
'name': 'rogue',
|
|
||||||
'hit_dice': '1d8',
|
|
||||||
'hit_dice_stat': 'DEX',
|
|
||||||
'proficiencies': 'simple weapons, hand crossbows, longswords, rapiers, shortswords',
|
|
||||||
'saving_throws': ['DEX', 'INT'],
|
|
||||||
'skills': ['Acrobatics', 'Athletics', 'Deception', 'Insight', 'Intimidation', 'Investigation', 'Perception', 'Performance', 'Persuasion', 'Sleight of Hand', 'Stealth'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
'Skill': [
|
|
||||||
{'name': 'Acrobatics'},
|
|
||||||
{'name': 'Animal Handling'},
|
|
||||||
{'name': 'Athletics'},
|
|
||||||
{'name': 'Deception'},
|
|
||||||
{'name': 'History'},
|
|
||||||
{'name': 'Insight'},
|
|
||||||
{'name': 'Intimidation'},
|
|
||||||
{'name': 'Investigation'},
|
|
||||||
{'name': 'Perception'},
|
|
||||||
{'name': 'Performance'},
|
|
||||||
{'name': 'Persuasion'},
|
|
||||||
{'name': 'Sleight of Hand'},
|
|
||||||
{'name': 'Stealth'},
|
|
||||||
{'name': 'Survival'},
|
|
||||||
],
|
|
||||||
|
|
||||||
'Ancestry': [
|
|
||||||
{'id': 1, 'name': 'human', 'creature_type': 'humanoid'},
|
|
||||||
{'id': 2, 'name': 'dragonborn', 'creature_type': 'humanoid'},
|
|
||||||
{'id': 3, 'name': 'tiefling', 'creature_type': 'humanoid'},
|
|
||||||
{'id': 4, 'name': 'elf', 'creature_type': 'humanoid'},
|
|
||||||
],
|
|
||||||
|
|
||||||
'AncestryTrait': [
|
|
||||||
{'id': 1, 'name': '+1 to All Ability Scores', },
|
|
||||||
{'id': 2, 'name': 'Breath Weapon', },
|
|
||||||
{'id': 3, 'name': 'Darkvision', },
|
|
||||||
],
|
|
||||||
|
|
||||||
'AncestryTraitMap': [
|
|
||||||
{'ancestry_id': 1, 'ancestry_trait_id': 1, 'level': 1}, # human +1 to scores
|
|
||||||
{'ancestry_id': 2, 'ancestry_trait_id': 2, 'level': 1}, # dragonborn breath weapon
|
|
||||||
{'ancestry_id': 3, 'ancestry_trait_id': 3, 'level': 1}, # tiefling darkvision
|
|
||||||
{'ancestry_id': 2, 'ancestry_trait_id': 2, 'level': 1}, # elf darkvision
|
|
||||||
],
|
|
||||||
|
|
||||||
'CharacterClassMap': [
|
|
||||||
{
|
|
||||||
'character_id': 1,
|
|
||||||
'character_class_id': 1,
|
|
||||||
'level': 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'character_id': 1,
|
|
||||||
'character_class_id': 2,
|
|
||||||
'level': 3,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
'Character': [
|
|
||||||
{
|
|
||||||
'id': 1,
|
|
||||||
'name': 'Sabetha',
|
|
||||||
'ancestry_id': 1,
|
|
||||||
'armor_class': 10,
|
|
||||||
'max_hit_points': 14,
|
|
||||||
'hit_points': 14,
|
|
||||||
'temp_hit_points': 0,
|
|
||||||
'speed': 30,
|
|
||||||
'str': 16,
|
|
||||||
'dex': 12,
|
|
||||||
'con': 18,
|
|
||||||
'int': 11,
|
|
||||||
'wis': 12,
|
|
||||||
'cha': 8,
|
|
||||||
'proficiencies': 'all armor, all shields, simple weapons, martial weapons',
|
|
||||||
'saving_throws': ['STR', 'CON'],
|
|
||||||
'skills': ['Acrobatics', 'Animal Handling'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
'ClassAttribute': [
|
|
||||||
{'id': 1, 'name': 'Fighting Style'},
|
|
||||||
{'id': 2, 'name': 'Another Attribute'},
|
|
||||||
],
|
|
||||||
|
|
||||||
'ClassAttributeOption': [
|
|
||||||
{'id': 1, 'attribute_id': 1, 'name': 'Archery'},
|
|
||||||
{'id': 2, 'attribute_id': 1, 'name': 'Battlemaster'},
|
|
||||||
{'id': 3, 'attribute_id': 2, 'name': 'Another Option 1'},
|
|
||||||
{'id': 4, 'attribute_id': 2, 'name': 'Another Option 2'},
|
|
||||||
],
|
|
||||||
|
|
||||||
|
|
||||||
'ClassAttributeMap': [
|
|
||||||
{'class_attribute_id': 1, 'character_class_id': 1, 'level': 2}, # Fighter: Fighting Style
|
|
||||||
{'class_attribute_id': 2, 'character_class_id': 1, 'level': 1}, # Fighter: Another Attr
|
|
||||||
],
|
|
||||||
|
|
||||||
'CharacterClassAttributeMap': [
|
|
||||||
{'character_id': 1, 'class_attribute_id': 2, 'option_id': 4}, # Sabetha, another option, option 2
|
|
||||||
{'character_id': 1, 'class_attribute_id': 1, 'option_id': 1}, # Sabetha, fighting style, archery
|
|
||||||
],
|
|
||||||
|
|
||||||
|
|
||||||
'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': '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': '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': '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'},
|
|
||||||
],
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def bootstrap():
|
|
||||||
"""
|
|
||||||
Initialize the database with source data. Idempotent; will skip anything that already exists.
|
|
||||||
"""
|
|
||||||
db.init()
|
|
||||||
for table, records in data.items():
|
|
||||||
model = getattr(schema, table)
|
|
||||||
|
|
||||||
for rec in records:
|
|
||||||
obj = model(**rec)
|
|
||||||
try:
|
|
||||||
with db.transaction():
|
|
||||||
db.session.add(obj)
|
|
||||||
logging.info(f"Created {table} {obj}")
|
|
||||||
except IntegrityError as e:
|
|
||||||
if 'UNIQUE constraint failed' in str(e):
|
|
||||||
logging.info(f"Skipping existing {table} {obj}")
|
|
||||||
continue
|
|
||||||
raise
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
_setup_hint = "You may be able to solve this error by running 'ttfrog setup' or specifying the --root parameter."
|
|
||||||
|
|
||||||
|
|
||||||
def database():
|
|
||||||
path = Path(os.environ['DATA_PATH']).expanduser()
|
|
||||||
if not path.exists() or not path.is_dir():
|
|
||||||
raise RuntimeError(
|
|
||||||
f"DATA_PATH {path} doesn't exist or isn't a directory.\n\n{_setup_hint}"
|
|
||||||
)
|
|
||||||
return path / Path('tabletop-frog.db')
|
|
||||||
|
|
||||||
|
|
||||||
def assets():
|
|
||||||
return Path(__file__).parent / 'assets'
|
|
||||||
|
|
||||||
|
|
||||||
def templates():
|
|
||||||
try:
|
|
||||||
return Path(os.environ['TEMPLATES_PATH'])
|
|
||||||
except KeyError:
|
|
||||||
return assets() / 'templates'
|
|
||||||
|
|
||||||
|
|
||||||
def static_files():
|
|
||||||
try:
|
|
||||||
return Path(os.environ['STATIC_FILES_PATH'])
|
|
||||||
except KeyError:
|
|
||||||
return assets() / 'public'
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
def routes(config):
|
|
||||||
config.add_route('index', '/')
|
|
||||||
config.add_route('sheet', '/c{uri:.*}', factory='ttfrog.webserver.controllers.CharacterSheet')
|
|
||||||
config.add_route('data', '/_/{table_name}{uri:.*}', factory='ttfrog.webserver.controllers.JsonData')
|
|
||||||
Reference in new issue
Block a user