Track Ersatz status with flag instead of null character

Previously, articles written by Ersatz Scrivener were represented in
the data model by having a NULL character id. This had several awkward
consequences, such as therebeing no real way to distinguish Ersatz
articles from different players without doubling up on foreign keys
whenever there was a character reference, because there would need to
be a user reference as well.

Using a flag on the article itself is a much cleaner solution. There is
no longer a need to have both character and user FKs. Ersatz-ness is
still a special case, but one easily tracked on articles without
changing how basic objects of the game relate to each other. Ersatz
articles can be treated differently in the stats just as easily while
still being subject to character-specific rules like index assignments.
This commit is contained in:
Tim Van Baak 2021-09-15 00:17:14 -07:00
parent 4d1c579e3c
commit abbe6e6b2e
4 changed files with 25 additions and 66 deletions

View File

@ -2,8 +2,6 @@
Article query interface Article query interface
""" """
from typing import Optional
from sqlalchemy import select from sqlalchemy import select
from amanuensis.db import * from amanuensis.db import *
@ -13,8 +11,8 @@ from amanuensis.errors import ArgumentError, BackendArgumentTypeError
def create( def create(
db: DbContext, db: DbContext,
lexicon_id: int, lexicon_id: int,
user_id: int, character_id: int,
character_id: Optional[int], ersatz: bool = False,
) -> Article: ) -> Article:
""" """
Create a new article in a lexicon. Create a new article in a lexicon.
@ -22,39 +20,21 @@ def create(
# Verify argument types are correct # Verify argument types are correct
if not isinstance(lexicon_id, int): if not isinstance(lexicon_id, int):
raise BackendArgumentTypeError(int, lexicon_id=lexicon_id) raise BackendArgumentTypeError(int, lexicon_id=lexicon_id)
if not isinstance(user_id, int):
raise BackendArgumentTypeError(int, user_id=user_id)
if character_id is not None and not isinstance(character_id, int): if character_id is not None and not isinstance(character_id, int):
raise BackendArgumentTypeError(int, character_id=character_id) raise BackendArgumentTypeError(int, character_id=character_id)
# Check that the user is a member of this lexicon # Check that the character belongs to the lexicon
mem: Membership = db(
select(Membership)
.where(Membership.user_id == user_id)
.where(Membership.lexicon_id == lexicon_id)
).scalar_one_or_none()
if not mem:
raise ArgumentError("User is not a member of lexicon")
# If the character id is provided, check that the user owns the character
# and the character belongs to the lexicon
if character_id is not None:
character: Character = db( character: Character = db(
select(Character).where(Character.id == character_id) select(Character).where(Character.id == character_id)
).scalar_one_or_none() ).scalar_one_or_none()
if not character: if not character:
raise ArgumentError("Character does not exist") raise ArgumentError("Character does not exist")
if character.user.id != user_id:
raise ArgumentError("Character is owned by the wrong player")
if character.lexicon.id != lexicon_id: if character.lexicon.id != lexicon_id:
raise ArgumentError("Character belongs to the wrong lexicon") raise ArgumentError("Character belongs to the wrong lexicon")
signature = character.signature signature = character.signature if not ersatz else "~Ersatz Scrivener"
else:
signature = "~Ersatz Scrivener"
new_article = Article( new_article = Article(
lexicon_id=lexicon_id, lexicon_id=lexicon_id,
user_id=user_id,
character_id=character_id, character_id=character_id,
title="Article title", title="Article title",
body=f"\n\n{signature}", body=f"\n\n{signature}",

View File

@ -83,7 +83,6 @@ def get_for_lexicon(db: DbContext, lexicon_id: int) -> Sequence[ArticleIndex]:
).scalars() ).scalars()
def update(db: DbContext, lexicon_id: int, indices: Sequence[ArticleIndex]) -> None: def update(db: DbContext, lexicon_id: int, indices: Sequence[ArticleIndex]) -> None:
""" """
Update the indices for a lexicon. Indices are matched by type and pattern. Update the indices for a lexicon. Indices are matched by type and pattern.

View File

@ -99,7 +99,6 @@ class User(ModelBase):
memberships = relationship("Membership", back_populates="user") memberships = relationship("Membership", back_populates="user")
characters = relationship("Character", back_populates="user") characters = relationship("Character", back_populates="user")
articles = relationship("Article", back_populates="user")
posts = relationship("Post", back_populates="user") posts = relationship("Post", back_populates="user")
######################### #########################
@ -393,11 +392,7 @@ class Article(ModelBase):
lexicon_id = Column(Integer, ForeignKey("lexicon.id"), nullable=False) lexicon_id = Column(Integer, ForeignKey("lexicon.id"), nullable=False)
# The character who is the author of this article # 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=False)
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 # The article to which this is an addendum
addendum_to = Column(Integer, ForeignKey("article.id"), nullable=True) addendum_to = Column(Integer, ForeignKey("article.id"), nullable=True)
@ -416,6 +411,9 @@ class Article(ModelBase):
# The number of times the article has been submitted # The number of times the article has been submitted
submit_nonce = Column(Integer, nullable=False, default=0) submit_nonce = Column(Integer, nullable=False, default=0)
# Whether the article is an Ersatz Scrivener article
ersatz = Column(Boolean, nullable=False, default=False)
#################### ####################
# History tracking # # History tracking #
#################### ####################
@ -449,7 +447,6 @@ class Article(ModelBase):
lexicon = relationship("Lexicon", back_populates="articles") lexicon = relationship("Lexicon", back_populates="articles")
character = relationship("Character", back_populates="articles") character = relationship("Character", back_populates="articles")
user = relationship("User", back_populates="articles")
addenda = relationship("Article", backref=backref("parent", remote_side=[id])) addenda = relationship("Article", backref=backref("parent", remote_side=[id]))

View File

@ -16,43 +16,26 @@ def test_create_article(db: DbContext, make: ObjectFactory):
lexicon1: Lexicon = make.lexicon() lexicon1: Lexicon = make.lexicon()
make.membership(user_id=user1.id, lexicon_id=lexicon1.id) make.membership(user_id=user1.id, lexicon_id=lexicon1.id)
make.membership(user_id=user2.id, lexicon_id=lexicon1.id) make.membership(user_id=user2.id, lexicon_id=lexicon1.id)
char1_1: Character = make.character(lexicon_id=lexicon1.id, user_id=user1.id) char_l1_u1: Character = make.character(lexicon_id=lexicon1.id, user_id=user1.id)
char1_2: Character = make.character(lexicon_id=lexicon1.id, user_id=user2.id) char_l1_u2: Character = make.character(lexicon_id=lexicon1.id, user_id=user2.id)
# Create a lexicon that only one user is in # Create a lexicon that only one user is in
lexicon2: Lexicon = make.lexicon() lexicon2: Lexicon = make.lexicon()
make.membership(user_id=user2.id, lexicon_id=lexicon2.id) make.membership(user_id=user2.id, lexicon_id=lexicon2.id)
char2_2: Character = make.character(lexicon_id=lexicon2.id, user_id=user2.id) char_l2_u2: Character = make.character(lexicon_id=lexicon2.id, user_id=user2.id)
# User cannot create article for another user's character # Characters can't own articles in other lexicons
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
artiq.create(db, lexicon1.id, user1.id, char1_2.id) artiq.create(db, lexicon1.id, char_l2_u2.id)
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
artiq.create(db, lexicon1.id, user2.id, char1_1.id) artiq.create(db, lexicon2.id, char_l1_u1.id)
# User cannot create article for their character in the wrong lexicon
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
artiq.create(db, lexicon1.id, user2.id, char2_2.id) artiq.create(db, lexicon2.id, char_l1_u2.id)
with pytest.raises(ArgumentError):
artiq.create(db, lexicon2.id, user2.id, char1_2.id)
# User cannot create article in a lexicon they aren't in
with pytest.raises(ArgumentError):
artiq.create(db, lexicon2.id, user1.id, char1_1.id)
# User cannot create anonymous articles in a lexicon they aren't in
with pytest.raises(ArgumentError):
artiq.create(db, lexicon2.id, user1.id, character_id=None)
# Users can create character-owned articles # Users can create character-owned articles
assert artiq.create(db, lexicon1.id, user1.id, char1_1.id) assert artiq.create(db, lexicon1.id, char_l1_u1.id)
assert artiq.create(db, lexicon1.id, user2.id, char1_2.id) assert artiq.create(db, lexicon1.id, char_l1_u2.id)
assert artiq.create(db, lexicon2.id, user2.id, char2_2.id) assert artiq.create(db, lexicon2.id, char_l2_u2.id)
# Users can create anonymous articles
assert artiq.create(db, lexicon1.id, user1.id, character_id=None)
assert artiq.create(db, lexicon1.id, user2.id, character_id=None)
assert artiq.create(db, lexicon2.id, user2.id, character_id=None)
def test_article_update_ts(db: DbContext, make: ObjectFactory): def test_article_update_ts(db: DbContext, make: ObjectFactory):
@ -61,7 +44,7 @@ def test_article_update_ts(db: DbContext, make: ObjectFactory):
lexicon: Lexicon = make.lexicon() lexicon: Lexicon = make.lexicon()
make.membership(user_id=user.id, lexicon_id=lexicon.id) make.membership(user_id=user.id, lexicon_id=lexicon.id)
char: Character = make.character(lexicon_id=lexicon.id, user_id=user.id) char: Character = make.character(lexicon_id=lexicon.id, user_id=user.id)
article = artiq.create(db, lexicon.id, user.id, char.id) article = artiq.create(db, lexicon.id, char.id)
created = article.last_updated created = article.last_updated
time.sleep(1) # The update timestamp has only second-level precision time.sleep(1) # The update timestamp has only second-level precision
article.title = "New title, who dis" article.title = "New title, who dis"