Add article indexes and some missing FK relationships

This commit is contained in:
Tim Van Baak 2021-05-01 12:14:25 -07:00
parent 336e4193c3
commit bf1c160140
2 changed files with 268 additions and 7 deletions

View File

@ -1,10 +1,38 @@
from sqlalchemy import create_engine, MetaData, event from sqlalchemy import create_engine, MetaData, event, TypeDecorator, CHAR
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session from sqlalchemy.orm import scoped_session
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
import sqlite3
import uuid
engine = create_engine('sqlite:///:memory:') # Register GUID as a known type with sqlite
sqlite3.register_converter('GUID', lambda h: uuid.UUID(hex=h))
sqlite3.register_adapter(uuid.UUID, lambda u: u.hex)
class Uuid(TypeDecorator):
"""
A uuid backed by a char(32) field in sqlite.
"""
impl = CHAR(32)
def process_bind_param(self, value, dialect):
if value is None:
return value
elif not isinstance(value, uuid.UUID):
return f'{uuid.UUID(value).int:32x}'
else:
return f'{value.int:32x}'
def process_result_value(self, value, dialect):
if value is None:
return value
elif not isinstance(value, uuid.UUID):
return uuid.UUID(value)
else:
return value
engine = create_engine('sqlite:///:memory:', connect_args={'detect_types': sqlite3.PARSE_DECLTYPES})
# Enable foreign key constraints # Enable foreign key constraints
@event.listens_for(engine, "connect") @event.listens_for(engine, "connect")

View File

@ -1,16 +1,20 @@
import enum
from sqlalchemy import ( from sqlalchemy import (
Boolean, Boolean,
Column, Column,
DateTime, DateTime,
Enum,
ForeignKey, ForeignKey,
Integer, Integer,
String, String,
Table, Table,
Text,
text, text,
) )
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship, backref
from uuid import uuid4
from .database import ModelBase from .database import ModelBase, Uuid
class User(ModelBase): class User(ModelBase):
@ -46,16 +50,20 @@ class User(ModelBase):
created = Column(DateTime, nullable=False, server_default=text('CURRENT_TIMESTAMP')) created = Column(DateTime, nullable=False, server_default=text('CURRENT_TIMESTAMP'))
# The timestamp the user last logged in # The timestamp the user last logged in
# This is NULL if the user has never logged in
last_login = Column(DateTime, nullable=True) last_login = Column(DateTime, nullable=True)
# The timestamp the user last performed an action # The timestamp the user last performed an action
# This is NULL if the user has never performed an action
last_activity = Column(DateTime, nullable=True) last_activity = Column(DateTime, nullable=True)
############################# #############################
# Foreign key relationships # # Foreign key relationships #
############################# #############################
memberships = relationship('Membership', back_populates='player') memberships = relationship('Membership', back_populates='user')
characters = relationship('Character', back_populates='user')
articles = relationship('Article', back_populates='user')
class Lexicon(ModelBase): class Lexicon(ModelBase):
@ -74,7 +82,8 @@ class Lexicon(ModelBase):
# The lexicon's human-readable identifier # The lexicon's human-readable identifier
name = Column(String, nullable=False, unique=True) name = Column(String, nullable=False, unique=True)
# Optional title override, instead of "Lexicon <name>" # Optional title override
# If this is NULL, the title is rendered as "Lexicon <name>"
title = Column(String, nullable=True) title = Column(String, nullable=True)
# The initial prompt describing the game's setting # The initial prompt describing the game's setting
@ -91,9 +100,11 @@ class Lexicon(ModelBase):
last_updated = Column(DateTime, nullable=False, server_default=text('CURRENT_TIMESTAMP')) last_updated = Column(DateTime, nullable=False, server_default=text('CURRENT_TIMESTAMP'))
# The timestamp the first turn was started # The timestamp the first turn was started
# This is NULL until the game starts
started = Column(DateTime, nullable=True) started = Column(DateTime, nullable=True)
# The timestamp when the last turn was published # The timestamp when the last turn was published
# This is NULL until the game is completed
completed = Column(DateTime, nullable=True) completed = Column(DateTime, nullable=True)
############## ##############
@ -101,6 +112,7 @@ class Lexicon(ModelBase):
############## ##############
# The current turn number # The current turn number
# This is NULL until the game strts
current_turn = Column(Integer, nullable=True) current_turn = Column(Integer, nullable=True)
# The number of turns in the game # The number of turns in the game
@ -117,12 +129,15 @@ class Lexicon(ModelBase):
public = Column(Boolean, nullable=False, default=False) public = Column(Boolean, nullable=False, default=False)
# Optional password required to join # Optional password required to join
# If this is NULL, no password is required to join
join_password = Column(String, nullable=True) join_password = Column(String, nullable=True)
# Maximum number of players who can join # Maximum number of players who can join
# If this is NULL, there is no limit to player joins
player_limit = Column(Integer, nullable=True) player_limit = Column(Integer, nullable=True)
# Maximum number of characters per player # Maximum number of characters per player
# If this is NULL, there is no limit to creating characters
character_limit = Column(Integer, nullable=True, default=1) character_limit = Column(Integer, nullable=True, default=1)
#################### ####################
@ -130,12 +145,14 @@ class Lexicon(ModelBase):
#################### ####################
# Recurrence for turn publish attempts, as crontab spec # Recurrence for turn publish attempts, as crontab spec
# If this is NULL, turns will not publish on a recurrence
publish_recur = Column(String, nullable=True) publish_recur = Column(String, nullable=True)
# Whether to attempt publish when an article is approved # Whether to attempt publish when an article is approved
publish_asap = Column(Boolean, nullable=False, default=True) publish_asap = Column(Boolean, nullable=False, default=True)
# Allow an incomplete turn to be published with this many articles # Allow an incomplete turn to be published with this many articles
# If this is NULL, the publish quorum is the number of characters
publish_quorum = Column(Integer, nullable=True) publish_quorum = Column(Integer, nullable=True)
##################### #####################
@ -146,9 +163,11 @@ class Lexicon(ModelBase):
allow_addendum = Column(Boolean, nullable=False, default=False) allow_addendum = Column(Boolean, nullable=False, default=False)
# Maximum number of addenda per player per turn # Maximum number of addenda per player per turn
# If this is NULL, there is no limit
addendum_turn_limit = Column(Integer, nullable=True) addendum_turn_limit = Column(Integer, nullable=True)
# Maximum number of addenda per title # Maximum number of addenda per title
# If this is NULL, there is no limit
addendum_title_limit = Column(Integer, nullable=True) addendum_title_limit = Column(Integer, nullable=True)
################# #################
@ -166,6 +185,10 @@ class Lexicon(ModelBase):
############################# #############################
memberships = relationship('Membership', back_populates='lexicon') memberships = relationship('Membership', back_populates='lexicon')
characters = relationship('Character', back_populates='lexicon')
articles = relationship('Article', back_populates='lexicon')
indexes = relationship('ArticleIndex', back_populates='lexicon')
index_rules = relationship('ArticleIndexRule', back_populates='lexicon')
class Membership(ModelBase): class Membership(ModelBase):
@ -219,5 +242,215 @@ class Membership(ModelBase):
############################# #############################
user = relationship('User', back_populates='memberships') user = relationship('User', back_populates='memberships')
lexicon = relationship('Lexicon', back_populates='memberships') lexicon = relationship('Lexicon', back_populates='memberships')
class Character(ModelBase):
"""
Represents a character played by a uaser in a Lexicon game.
"""
__tablename__ = 'character'
##################
# Character info #
##################
# Primary character id
id = Column(Integer, primary_key=True)
# Public-facing character id
public_id = Column(Uuid, nullable=False, unique=True, default=uuid4)
# The lexicon to which this character belongs
lexicon_id = Column(Integer, ForeignKey('lexicon.id'), nullable=False)
# The user to whom this character belongs
user_id = Column(Integer, ForeignKey('user.id'), nullable=False)
# The character's name
name = Column(String, nullable=False)
# The character's signature
signature = Column(String, nullable=False)
#############################
# Foreign key relationships #
#############################
user = relationship('User', back_populates='characters')
lexicon = relationship('Lexicon', back_populates='characters')
articles = relationship('Article', back_populates='character')
index_rules = relationship('ArticleIndexRule', back_populates='character')
class ArticleState(enum.Enum):
"""
The step of the editorial process an article is in.
"""
DRAFT = 0
SUBMITTED = 1
APPROVED = 2
class Article(ModelBase):
"""
Represents a single article in a lexicon.
"""
__tablename__ = 'article'
################
# Article info #
################
# Primary article id
id = Column(Integer, primary_key=True)
# Public-facing article id
public_id = Column(Uuid, nullable=False, unique=True, default=uuid4)
# The lexicon to which this article belongs
lexicon_id = Column(Integer, ForeignKey('lexicon.id'), nullable=False)
# The character who is the author of this article
# If this is NULL, the article is written by Ersatz Scrivener
character_id = Column(Integer, ForeignKey('character.id'), nullable=True)
# The user who owns this article
user_id = Column(Integer, ForeignKey('user.id'), nullable=False)
# The article to which this is an addendum
addendum_to = Column(Integer, ForeignKey('article.id'), nullable=True)
#################
# Article state #
#################
# The turn in which the article was published
# This is NULL until the article is published
turn = Column(Integer, nullable=True)
# The stage of review the article is in
state = Column(Enum(ArticleState), nullable=False, default=ArticleState.DRAFT)
# The number of times the article has been submitted
submit_nonce = Column(Integer, nullable=False, default=0)
####################
# History tracking #
####################
# Timestamp the content of the article was last updated
last_updated = Column(DateTime, nullable=False, server_default=text('CURRENT_TIMESTAMP'))
# Timestamp the article was last submitted
# This is NULL until the article is submitted
submitted = Column(DateTime, nullable=True)
# Timestamp the article was last approved
# This is NULL until the article is approved
approved = Column(DateTime, nullable=True)
###################
# Article content #
###################
# The article's title
title = Column(String, nullable=False, default="")
# The article's text
body = Column(Text, nullable=False)
#############################
# Foreign key relationships #
#############################
lexicon = relationship('Lexicon', back_populates='articles')
character = relationship('Character', back_populates='articles')
user = relationship('User', back_populates='articles')
addenda = relationship('Article', backref=backref('parent', remote_side=[id]))
class IndexType(enum.Enum):
"""
The title-matching behavior of an article index.
"""
CHAR = 0
RANGE = 1
PREFIX = 2
ETC = 3
class ArticleIndex(ModelBase):
"""
Represents an index definition.
"""
__tablename__ = 'article_index'
##############
# Index info #
##############
# Primary index id
id = Column(Integer, primary_key=True)
# The lexicon this index is in
lexicon_id = Column(Integer, ForeignKey('lexicon.id'), nullable=False)
# The index type
index_type = Column(Enum(IndexType), nullable=False)
# The index pattern
pattern = Column(String, nullable=False)
# The order in which the index is processed
logical_order = Column(Integer, nullable=False, default=0)
# The order in which the index is displayed
display_order = Column(Integer, nullable=False, default=0)
# The maximum number of articles allowed in this index
# If this is NULL, there is no limit on this index
capacity = Column(Integer, nullable=True)
#############################
# Foreign key relationships #
#############################
lexicon = relationship('Lexicon', back_populates='indexes')
index_rules = relationship('ArticleIndexRule', back_populates='index')
class ArticleIndexRule(ModelBase):
"""
Represents a restriction of which index a character may write in for a turn.
A character with multiple index rules may write in any index that satisfies
a rule. A character with no index rules may write in any index.
"""
__tablename__ = 'article_index_rule'
###################
# Index rule info #
###################
# Primary index rule id
id = Column(Integer, primary_key=True)
# The lexicon of this index rule
lexicon_id = Column(Integer, ForeignKey('lexicon.id'), nullable=False)
# The character to whom this rule applies
character_id = Column(Integer, ForeignKey('character.id'), nullable=False)
# The index to which the character is restricted
index = Column(Integer, ForeignKey('index.id'), nullable=False)
# The turn in which this rule applies
turn = Column(Integer, nullable=False)
#############################
# Foreign key relationships #
#############################
lexicon = relationship('Lexicon', back_populates='index_rules')
index = relationship('ArticleIndex', back_populates='index_rules')
character = relationship('Character', back_populates='index_rules')