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:
parent
4d1c579e3c
commit
abbe6e6b2e
|
@ -2,8 +2,6 @@
|
|||
Article query interface
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from amanuensis.db import *
|
||||
|
@ -13,8 +11,8 @@ from amanuensis.errors import ArgumentError, BackendArgumentTypeError
|
|||
def create(
|
||||
db: DbContext,
|
||||
lexicon_id: int,
|
||||
user_id: int,
|
||||
character_id: Optional[int],
|
||||
character_id: int,
|
||||
ersatz: bool = False,
|
||||
) -> Article:
|
||||
"""
|
||||
Create a new article in a lexicon.
|
||||
|
@ -22,39 +20,21 @@ def create(
|
|||
# Verify argument types are correct
|
||||
if not isinstance(lexicon_id, int):
|
||||
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):
|
||||
raise BackendArgumentTypeError(int, character_id=character_id)
|
||||
|
||||
# Check that the user is a member of this 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:
|
||||
# Check that the character belongs to the lexicon
|
||||
character: Character = db(
|
||||
select(Character).where(Character.id == character_id)
|
||||
).scalar_one_or_none()
|
||||
if not character:
|
||||
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:
|
||||
raise ArgumentError("Character belongs to the wrong lexicon")
|
||||
signature = character.signature
|
||||
else:
|
||||
signature = "~Ersatz Scrivener"
|
||||
signature = character.signature if not ersatz else "~Ersatz Scrivener"
|
||||
|
||||
new_article = Article(
|
||||
lexicon_id=lexicon_id,
|
||||
user_id=user_id,
|
||||
character_id=character_id,
|
||||
title="Article title",
|
||||
body=f"\n\n{signature}",
|
||||
|
|
|
@ -83,7 +83,6 @@ def get_for_lexicon(db: DbContext, lexicon_id: int) -> Sequence[ArticleIndex]:
|
|||
).scalars()
|
||||
|
||||
|
||||
|
||||
def update(db: DbContext, lexicon_id: int, indices: Sequence[ArticleIndex]) -> None:
|
||||
"""
|
||||
Update the indices for a lexicon. Indices are matched by type and pattern.
|
||||
|
|
|
@ -99,7 +99,6 @@ class User(ModelBase):
|
|||
|
||||
memberships = relationship("Membership", back_populates="user")
|
||||
characters = relationship("Character", back_populates="user")
|
||||
articles = relationship("Article", 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)
|
||||
|
||||
# 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)
|
||||
character_id = Column(Integer, ForeignKey("character.id"), nullable=False)
|
||||
|
||||
# The article to which this is an addendum
|
||||
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
|
||||
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 #
|
||||
####################
|
||||
|
@ -449,7 +447,6 @@ class Article(ModelBase):
|
|||
|
||||
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]))
|
||||
|
||||
|
||||
|
|
|
@ -16,43 +16,26 @@ def test_create_article(db: DbContext, make: ObjectFactory):
|
|||
lexicon1: Lexicon = make.lexicon()
|
||||
make.membership(user_id=user1.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)
|
||||
char1_2: Character = make.character(lexicon_id=lexicon1.id, user_id=user2.id)
|
||||
char_l1_u1: Character = make.character(lexicon_id=lexicon1.id, user_id=user1.id)
|
||||
char_l1_u2: Character = make.character(lexicon_id=lexicon1.id, user_id=user2.id)
|
||||
|
||||
# Create a lexicon that only one user is in
|
||||
lexicon2: Lexicon = make.lexicon()
|
||||
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):
|
||||
artiq.create(db, lexicon1.id, user1.id, char1_2.id)
|
||||
artiq.create(db, lexicon1.id, char_l2_u2.id)
|
||||
with pytest.raises(ArgumentError):
|
||||
artiq.create(db, lexicon1.id, user2.id, char1_1.id)
|
||||
|
||||
# User cannot create article for their character in the wrong lexicon
|
||||
artiq.create(db, lexicon2.id, char_l1_u1.id)
|
||||
with pytest.raises(ArgumentError):
|
||||
artiq.create(db, lexicon1.id, user2.id, char2_2.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)
|
||||
artiq.create(db, lexicon2.id, char_l1_u2.id)
|
||||
|
||||
# Users can create character-owned articles
|
||||
assert artiq.create(db, lexicon1.id, user1.id, char1_1.id)
|
||||
assert artiq.create(db, lexicon1.id, user2.id, char1_2.id)
|
||||
assert artiq.create(db, lexicon2.id, user2.id, char2_2.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)
|
||||
assert artiq.create(db, lexicon1.id, char_l1_u1.id)
|
||||
assert artiq.create(db, lexicon1.id, char_l1_u2.id)
|
||||
assert artiq.create(db, lexicon2.id, char_l2_u2.id)
|
||||
|
||||
|
||||
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()
|
||||
make.membership(user_id=user.id, lexicon_id=lexicon.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
|
||||
time.sleep(1) # The update timestamp has only second-level precision
|
||||
article.title = "New title, who dis"
|
||||
|
|
Loading…
Reference in New Issue