added transaction log, UX scaffolding
This commit is contained in:
+29
-5
@@ -1,12 +1,10 @@
|
||||
import nanoid
|
||||
from nanoid_dictionary import human_alphabet
|
||||
|
||||
from pyramid_sqlalchemy import BaseObject
|
||||
from wtforms import validators
|
||||
from slugify import slugify
|
||||
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy import String
|
||||
from pyramid_sqlalchemy import BaseObject
|
||||
from slugify import slugify
|
||||
|
||||
|
||||
def genslug():
|
||||
@@ -38,5 +36,31 @@ class IterableMixin:
|
||||
return f"{self.__class__.__name__}: {str(dict(self))}"
|
||||
|
||||
|
||||
def multivalue_string_factory(name, column=Column(String), separator=';'):
|
||||
"""
|
||||
Generate a mixin class that adds a string column with getters and setters
|
||||
that convert list values to strings and back again. Equivalent to:
|
||||
|
||||
class MultiValueString:
|
||||
_name = column
|
||||
|
||||
@property
|
||||
def name_property(self):
|
||||
return self._name.split(';')
|
||||
|
||||
@name.setter
|
||||
def name(self, val):
|
||||
return ';'.join(val)
|
||||
"""
|
||||
attr = f"_{name}"
|
||||
prop = property(lambda self: getattr(self, attr).split(separator))
|
||||
setter = prop.setter(lambda self, val: setattr(self, attr, separator.join(val)))
|
||||
return type('MultiValueString', (object, ), {
|
||||
attr: column,
|
||||
f"{name}_property": prop,
|
||||
name: setter,
|
||||
})
|
||||
|
||||
|
||||
# class Table(*Bases):
|
||||
Bases = [BaseObject, IterableMixin, SlugMixin]
|
||||
|
||||
+61
-4
@@ -7,13 +7,70 @@ from sqlalchemy.exc import IntegrityError
|
||||
|
||||
# move this to json or whatever
|
||||
data = {
|
||||
'CharacterClass': [
|
||||
{
|
||||
'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'],
|
||||
},
|
||||
{
|
||||
'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': [
|
||||
{'name': 'human'},
|
||||
{'name': 'dragonborn'},
|
||||
{'name': 'tiefling'},
|
||||
{'name': 'human', 'creature_type': 'humanoid'},
|
||||
{'name': 'dragonborn', 'creature_type': 'humanoid'},
|
||||
{'name': 'tiefling', 'creature_type': 'humanoid'},
|
||||
],
|
||||
'Character': [
|
||||
{'id': 1, 'name': 'Sabetha', 'ancestry': 'tiefling', 'level': 10, 'str': 10, 'dex': 10, 'con': 10, 'int': 10, 'wis': 10, 'cha': 10},
|
||||
{
|
||||
'id': 1,
|
||||
'name': 'Sabetha',
|
||||
'ancestry': 'tiefling',
|
||||
'character_class': ['fighter', 'rogue'],
|
||||
'level': 1,
|
||||
'armor_class': 10,
|
||||
'max_hit_points': 14,
|
||||
'hit_points': 14,
|
||||
'temp_hit_points': 0,
|
||||
'passive_perception': 10,
|
||||
'passive_insight': 10,
|
||||
'passive_investigation': 10,
|
||||
'speed': '30 ft.',
|
||||
'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'],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
+73
-8
@@ -1,32 +1,97 @@
|
||||
import enum
|
||||
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import Integer
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy import ForeignKey
|
||||
# from sqlalchemy import PrimaryKeyConstraint
|
||||
# from sqlalchemy import DateTime
|
||||
from sqlalchemy import Enum
|
||||
from sqlalchemy import Text
|
||||
|
||||
from ttfrog.db.base import Bases
|
||||
from ttfrog.db.base import Bases, BaseObject, IterableMixin
|
||||
from ttfrog.db.base import multivalue_string_factory
|
||||
|
||||
|
||||
class Ancestry(*Bases):
|
||||
__tablename__ = "ancestry"
|
||||
STATS = ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA']
|
||||
|
||||
name = Column(String, primary_key=True, unique=True)
|
||||
CREATURE_TYPES = ['aberation', 'beast', 'celestial', 'construct', 'dragon', 'elemental', 'fey', 'fiend', 'Giant',
|
||||
'humanoid', 'monstrosity', 'ooze', 'plant', 'undead']
|
||||
|
||||
# enums for db schemas
|
||||
StatsEnum = enum.Enum("StatsEnum", ((k, k) for k in STATS))
|
||||
CreatureTypesEnum = enum.Enum("CreatureTypesEnum", ((k, k) for k in CREATURE_TYPES))
|
||||
|
||||
CharacterClassMixin = multivalue_string_factory('character_class', Column(String, nullable=False))
|
||||
SavingThrowsMixin = multivalue_string_factory('saving_throws')
|
||||
SkillsMixin = multivalue_string_factory('skills')
|
||||
|
||||
|
||||
class Skill(*Bases):
|
||||
__tablename__ = "skill"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, index=True, unique=True)
|
||||
description = Column(Text)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.name)
|
||||
|
||||
|
||||
class Character(*Bases):
|
||||
__tablename__ = "character"
|
||||
class Proficiency(*Bases):
|
||||
__tablename__ = "proficiency"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, index=True, unique=True)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.name)
|
||||
|
||||
|
||||
class Ancestry(*Bases):
|
||||
__tablename__ = "ancestry"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, index=True, unique=True)
|
||||
creature_type = Column(Enum(CreatureTypesEnum))
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.name)
|
||||
|
||||
|
||||
class CharacterClass(*Bases, SavingThrowsMixin, SkillsMixin):
|
||||
__tablename__ = "character_class"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, index=True, unique=True)
|
||||
hit_dice = Column(String, default='1d6')
|
||||
hit_dice_stat = Column(Enum(StatsEnum))
|
||||
proficiencies = Column(String)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.name)
|
||||
|
||||
|
||||
class Character(*Bases, CharacterClassMixin, SavingThrowsMixin, SkillsMixin):
|
||||
__tablename__ = "character"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
ancestry = Column(String, ForeignKey("ancestry.name"), nullable=False)
|
||||
name = Column(String(255), nullable=False)
|
||||
level = Column(Integer, nullable=False, info={'min': 1, 'max': 20})
|
||||
armor_class = Column(Integer, nullable=False, info={'min': 1, 'max': 99})
|
||||
hit_points = Column(Integer, nullable=False, info={'min': 0, 'max': 999})
|
||||
max_hit_points = Column(Integer, nullable=False, info={'min': 0, 'max': 999})
|
||||
temp_hit_points = Column(Integer, nullable=False, info={'min': 0})
|
||||
passive_perception = Column(Integer, nullable=False)
|
||||
passive_insight = Column(Integer, nullable=False)
|
||||
passive_investigation = Column(Integer, nullable=False)
|
||||
speed = Column(String, nullable=False, default="30 ft.")
|
||||
str = Column(Integer, info={'min': 0, 'max': 30})
|
||||
dex = Column(Integer, info={'min': 0, 'max': 30})
|
||||
con = Column(Integer, info={'min': 0, 'max': 30})
|
||||
int = Column(Integer, info={'min': 0, 'max': 30})
|
||||
wis = Column(Integer, info={'min': 0, 'max': 30})
|
||||
cha = Column(Integer, info={'min': 0, 'max': 30})
|
||||
proficiencies = Column(String)
|
||||
|
||||
|
||||
class TransactionLog(BaseObject, IterableMixin):
|
||||
__tablename__ = "transaction_log"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
source_table_name = Column(String, index=True, nullable=False)
|
||||
primary_key = Column(Integer, index=True)
|
||||
diff = Column(Text)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from ttfrog.db.manager import db
|
||||
from ttfrog.db.schema import TransactionLog
|
||||
|
||||
|
||||
def record(previous, new):
|
||||
diff = list((set(previous.items()) ^ set(dict(new).items())))
|
||||
if not diff:
|
||||
return
|
||||
|
||||
rec = TransactionLog(
|
||||
source_table_name=new.__tablename__,
|
||||
primary_key=new.id,
|
||||
diff=json.dumps(diff),
|
||||
)
|
||||
with db.transaction():
|
||||
db.add(rec)
|
||||
logging.debug(f"Saved restore point: {dict(rec)}")
|
||||
return rec
|
||||
|
||||
|
||||
def restore(rec, log_id=None):
|
||||
if log_id:
|
||||
log = db.query(TransactionLog).filter_by(id=log_id).one()
|
||||
else:
|
||||
log = db.query(TransactionLog).filter_by(source_table_name=rec.__tablename__, primary_key=rec.id).one()
|
||||
logging.debug(f"Located restore point {log = }")
|
||||
diff = json.loads(log.diff)
|
||||
updates = dict(diff[::2])
|
||||
if not updates:
|
||||
return
|
||||
logging.debug(f"{updates = }")
|
||||
with db.transaction():
|
||||
db.query(db.tables[log.source_table_name]).update(updates)
|
||||
Reference in New Issue
Block a user