Add article indexes and some missing FK relationships
This commit is contained in:
parent
336e4193c3
commit
bf1c160140
|
@ -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")
|
||||||
|
|
|
@ -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')
|
||||||
|
|
Loading…
Reference in New Issue