diff --git a/amanuensis/backend/__init__.py b/amanuensis/backend/__init__.py index e0c9365..e35cc56 100644 --- a/amanuensis/backend/__init__.py +++ b/amanuensis/backend/__init__.py @@ -1,9 +1,10 @@ import amanuensis.backend.article as artiq import amanuensis.backend.character as charq import amanuensis.backend.index as indq +import amanuensis.backend.indexrule as irq import amanuensis.backend.lexicon as lexiq import amanuensis.backend.membership as memq import amanuensis.backend.post as postq import amanuensis.backend.user as userq -__all__ = ["artiq", "charq", "indq", "lexiq", "memq", "postq", "userq"] +__all__ = ["artiq", "charq", "indq", "irq", "lexiq", "memq", "postq", "userq"] diff --git a/amanuensis/backend/indexrule.py b/amanuensis/backend/indexrule.py new file mode 100644 index 0000000..4694302 --- /dev/null +++ b/amanuensis/backend/indexrule.py @@ -0,0 +1,55 @@ +""" +Index rule query interface +""" + +from typing import Sequence + +from sqlalchemy import select + +from amanuensis.db import * +from amanuensis.errors import ArgumentError, BackendArgumentTypeError + + +def create( + db: DbContext, + lexicon_id: int, + character_id: int, + index_id: int, + turn: int, +) -> ArticleIndexRule: + """Create an index assignment.""" + # Verify argument types are correct + if not isinstance(lexicon_id, int): + raise BackendArgumentTypeError(int, lexicon_id=lexicon_id) + if character_id is not None and not isinstance(character_id, int): + raise BackendArgumentTypeError(int, character_id=character_id) + if not isinstance(index_id, int): + raise BackendArgumentTypeError(int, index_id=index_id) + if not isinstance(turn, int): + raise BackendArgumentTypeError(int, turn=turn) + + # Verify 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.lexicon.id != lexicon_id: + raise ArgumentError("Character belongs to the wrong lexicon") + + new_assignment: ArticleIndexRule = ArticleIndexRule( + lexicon_id=lexicon_id, + character_id=character_id, + index_id=index_id, + turn=turn, + ) + db.session.add(new_assignment) + db.session.commit() + return new_assignment + + +def get_for_lexicon(db: DbContext, lexicon_id: int) -> Sequence[ArticleIndex]: + """Returns all index rules for a lexicon.""" + return db( + select(ArticleIndexRule).where(ArticleIndexRule.lexicon_id == lexicon_id) + ).scalars() diff --git a/amanuensis/cli/index.py b/amanuensis/cli/index.py index 52cd3c1..55f1f5e 100644 --- a/amanuensis/cli/index.py +++ b/amanuensis/cli/index.py @@ -1,4 +1,3 @@ -import enum import logging from amanuensis.backend import * @@ -40,3 +39,26 @@ def command_create(args) -> int: ) LOG.info(f"Created {index.index_type}:{index.pattern} in {lexicon.full_title}") return 0 + + +@add_argument("--lexicon", required=True, help="The lexicon's name") +@add_argument("--character", help="The character's public id") +@add_argument("--index", required=True, help="The index pattern") +@add_argument("--turn", required=True, type=int) +def command_assign(args) -> int: + """ + Create a turn assignment for a lexicon. + """ + db: DbContext = args.get_db() + lexicon = lexiq.try_from_name(db, args.lexicon) + if not lexicon: + raise ValueError("Lexicon does not exist") + char = charq.try_from_public_id(db, args.character) + assert char + indices = indq.get_for_lexicon(db, lexicon.id) + index = [i for i in indices if i.pattern == args.index] + if not index: + raise ValueError("Index not found") + assignment = irq.create(db, lexicon.id, char.id, index[0].id, args.turn) + LOG.info("Created") + return 0 diff --git a/amanuensis/resources/page.css b/amanuensis/resources/page.css index 976f2f8..bffd77b 100644 --- a/amanuensis/resources/page.css +++ b/amanuensis/resources/page.css @@ -201,7 +201,7 @@ ul.unordered-tabs li a[href]:hover { background-color: var(--button-hover); border-color: var(--button-hover); } -#index-definition-help { +details.setting-help { margin-block-start: 1em; margin-block-end: 1em; } diff --git a/amanuensis/server/lexicon/settings/__init__.py b/amanuensis/server/lexicon/settings/__init__.py index f283756..e840f92 100644 --- a/amanuensis/server/lexicon/settings/__init__.py +++ b/amanuensis/server/lexicon/settings/__init__.py @@ -12,7 +12,7 @@ from amanuensis.server.helpers import ( current_lexicon, ) -from .forms import PlayerSettingsForm, SetupSettingsForm, IndexSchemaForm +from .forms import PlayerSettingsForm, SetupSettingsForm, IndexSchemaForm, IndexAssignmentsForm bp = Blueprint("settings", __name__, url_prefix="/settings", template_folder=".") @@ -177,6 +177,32 @@ def index_post(lexicon_name): ) +@bp.get("/assign/") +@lexicon_param +@editor_required +def assign(lexicon_name): + # Get the current assignments + rules: Sequence[ArticleIndexRule] = list(irq.get_for_lexicon(g.db, current_lexicon.id)) + rule_data = [ + { + "turn": rule.turn, + "index": rule.index.pattern, + "character": rule.character.public_id, + } + for rule in rules + ] + # Add a blank rule to allow for adding rules + rule_data.append({ + "turn": 0, + "index": "", + "character": "", + }) + form = IndexAssignmentsForm(rules=rule_data) + return render_template( + "settings.jinja", lexicon_name=lexicon_name, page_name=assign.__name__, form=form + ) + + @bp.get("/publish/") @lexicon_param @editor_required diff --git a/amanuensis/server/lexicon/settings/forms.py b/amanuensis/server/lexicon/settings/forms.py index 7b9209c..dee4161 100644 --- a/amanuensis/server/lexicon/settings/forms.py +++ b/amanuensis/server/lexicon/settings/forms.py @@ -13,7 +13,8 @@ from wtforms import ( from wtforms.validators import Optional, DataRequired, ValidationError from wtforms.widgets.html5 import NumberInput -from amanuensis.db import ArticleIndex, IndexType +from amanuensis.db import ArticleIndex, IndexType, Lexicon +from amanuensis.server.helpers import current_lexicon class PlayerSettingsForm(FlaskForm): @@ -95,3 +96,29 @@ class IndexSchemaForm(FlaskForm): indices = FieldList(FormField(IndexDefinitionForm)) submit = SubmitField("Submit") + + +class AssignmentDefinitionForm(FlaskForm): + """/lexicon//settings/assign/""" + + class Meta: + # Disable CSRF on the individual assignment definitions, since the + # schema form will have one + csrf = False + + turn = IntegerField(widget=NumberInput(min=0, max=99)) + index = SelectField() + character = SelectField() + + def __init__(self, **kwargs): + lexicon: Lexicon = current_lexicon + self.index.choices = [i.pattern for i in lexicon.indices] + self.character.choices = [(c.public_id, c.name) for c in lexicon.characters] + super().__init__(**kwargs) + + +class IndexAssignmentsForm(FlaskForm): + """/lexicon//settings/assign/""" + + rules = FieldList(FormField(AssignmentDefinitionForm)) + submit = SubmitField("Submit") diff --git a/amanuensis/server/lexicon/settings/settings.jinja b/amanuensis/server/lexicon/settings/settings.jinja index d17d712..915dd2b 100644 --- a/amanuensis/server/lexicon/settings/settings.jinja +++ b/amanuensis/server/lexicon/settings/settings.jinja @@ -24,6 +24,7 @@
  • {{ settings_page_link("player", "Player Settings") }}
  • {{ settings_page_link("setup", "Game Setup") }}
  • {{ settings_page_link("index", "Article Indices") }}
  • +
  • {{ settings_page_link("assign", "Index Assignments") }}
  • {{ settings_page_link("publish", "Turn Publishing") }}
  • {{ settings_page_link("article", "Article Requirements") }}
  • @@ -86,7 +87,7 @@ {% if page_name == "index" %}

    Article Indexes

    -
    +
    Index definition help

    An index is a rule that matches the title of a lexicon article based on its index type and pattern. A char index matches a title if the first letter of the title (excluding "A", "An", and "The") is one of the letters in the pattern. A range index has a pattern denoting a range of letters, such as "A-F", and matches a title if the first letter of the title is in the range. A prefix index matches any title that begins with the pattern. An etc index always matches a title.

    When a title is to be sorted under an index, indices are checked in order, sorted first by descending order of logical priority, and then by alphabetical order of index pattern. The title is sorted under the first index that matches it.

    @@ -128,13 +129,100 @@ {% endfor %} {% endif %} +{% if page_name == "assign" %} +

    Index Assignments

    +
    + Index assignment help +

    An index assignment is a rule that requires a player to write an article under certain indices for a particular turn. If more than one rule applies to a player, any index satisfying one of those rules is permitted. If no rule applies to a player, any index is permitted.

    +
    +
    + {{ form.hidden_tag() }} + + + + + + + {% for rule_form in form.rules %} + + + + + + {% for field in index_form %} + {% for error in field.errors %} + + + + {% endfor %} + {% endfor %} + {% endfor %} +
    TurnIndexCharacter
    {{ rule_form.turn() }}{{ rule_form.index() }}{{ rule_form.character() }}
    {{ error }}
    +

    {{ form.submit() }}

    +
    + {% for message in get_flashed_messages() %} + {{ message }}
    + {% endfor %} +{% endif %} + {% if page_name == "publish" %} -

    Turn Publishing

    +

    Turn Publishing

    +{#

    Turn Publishing

    +

    + {{ form.publishDeadlines(autocomplete="off") }} + {{ form.publishDeadlines.label }}
    + {{ flag_setting(form.publishAsap) }} + {% for error in form.publishDeadlines.errors %} + {{ error }}
    + {% endfor %} + {{ flag_setting(form.publishBlockOnReady) }} + {{ number_setting(form.publishQuorum) }} + +

    + {{ form.turnAssignment.label }}:
    + {{ form.turnAssignment(class_="fullwidth", rows=10) }} + Transfer editorial control +

    #} {% endif %} {% if page_name == "article" %} -

    Article Requirements

    +

    Article Requirements

    +{#

    Article Requirements

    +

    + {{ flag_setting(form.articleCitationAllowSelf) }} + {{ number_setting(form.articleCitationMinExtant)}} + {{ number_setting(form.articleCitationMaxExtant)}} + {{ number_setting(form.articleCitationMinPhantom)}} + {{ number_setting(form.articleCitationMaxPhantom)}} + {{ number_setting(form.articleCitationMinTotal)}} + {{ number_setting(form.articleCitationMaxTotal)}} + {{ number_setting(form.articleCitationMinChars)}} + {{ number_setting(form.articleCitationMaxChars)}} + {{ number_setting(form.articleWordLimitSoft)}} + {{ number_setting(form.articleWordLimitHard)}} + {{ flag_setting(form.articleAddendumAllowed) }} + {{ number_setting(form.articleAddendumMax) }} +

    #} {% endif %} + + +{#

    + Id: {{ g.lexicon.lid }}
    + Name: {{ g.lexicon.cfg.name }}
    + Created: {{ g.lexicon.cfg.time.created|asdate }}
    + Completed: {{ g.lexicon.cfg.time.completed|asdate }}
    + Players: + {% for uid in g.lexicon.cfg.join.joined %} + {{ uid|user_attr('username') }}{% if not loop.last %},{% endif %} + {% endfor %}
    + Characters: + {% for char in g.lexicon.cfg.character.values() %} + {{ char.name }}{% if char.player %} + ({{ char.player|user_attr('username') }}){% endif %} + {% if not loop.last %},{% endif %} + {% endfor %}
    +

    #} + {% endblock %} {% set template_content_blocks = [self.main()] %} diff --git a/tests/backend/test_rule.py b/tests/backend/test_rule.py new file mode 100644 index 0000000..e69de29