Skip to content

Commit

Permalink
feat(webui): fancy forms for simple text fields
Browse files Browse the repository at this point in the history
  • Loading branch information
natelandau committed Nov 8, 2024
1 parent 93c2ab7 commit 9bd634b
Show file tree
Hide file tree
Showing 31 changed files with 826 additions and 515 deletions.
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,19 @@ repos:
entry: yamllint --strict --config-file .yamllint.yml

- repo: "https://github.com/charliermarsh/ruff-pre-commit"
rev: "v0.7.1"
rev: "v0.7.3"
hooks:
- id: ruff
exclude: tests/
- id: ruff-format

- repo: "https://github.com/crate-ci/typos"
rev: v1.26.8
rev: typos-dict-v0.11.34
hooks:
- id: typos

- repo: "https://github.com/djlint/djLint"
rev: v1.35.3
rev: v1.36.1
hooks:
- id: djlint
args: ["--configuration", "pyproject.toml"]
Expand Down
10 changes: 6 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@
"numpy>=2.1.1,<2.2.0",
"py-cord>=2.6.1,<2.7.0",
"pydantic>=2.9.2,<3.0.0",
"pygithub>=2.4.0,<3.0.0",
"pygithub>=2.4.0",
"quart-flask-patch>=0.3.0",
"quart-session>=3.0.0",
"quart-wtforms>=1.0.2",
"quart>=0.19.6",
"redis>=5.1.0,<6.0.0",
"rich>=13.8.1",
"semver>=3.0.2",
"typer>=0.12.5,<0.13.0",
"typer>=0.13.0,<0.14.0",
]
description = "Valentina is a Discord bot that helps you run TTRPGs."
license = "AGPL-3.0-or-later"
Expand All @@ -50,7 +50,7 @@
"coverage>=7.6.1",
"dirty-equals>=0.8.0",
"djlint>=1.35.2",
"mypy>=1.11.2,<2.0.0",
"mypy>=1.11.2",
"poethepoet>=0.29.0",
"polyfactory>=2.17.0",
"pre-commit>=3.8.0",
Expand Down Expand Up @@ -214,6 +214,7 @@
"ANN101", # missing-type-self
"ANN204", # Missing return type annotation for special method `__init__`
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed,
"ARG005", # Unused lambda argument
"ASYNC110", # Use `anyio.Event` instead of awaiting `anyio.sleep` in a `while` loop
"B006", # mutable-argument-default
"B008", # function-call-in-default-argument
Expand Down Expand Up @@ -258,14 +259,15 @@
], "migrations/*.py" = [
"ARG002",
"PLR6301",
], "tests/*.py" = [
], "tests/**/*.py" = [
"A002",
"A003",
"ANN201", # Missing return type annotation
"ARG001", # Unused argument
"D102",
"ERA001", # Commented out code
"F403",
"F405", # May be undefined from type imports
"PLR0913",
"PLR2004",
"S101",
Expand Down
4 changes: 3 additions & 1 deletion src/valentina/controllers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@

from .channel_mngr import ChannelManager
from .character_sheet_builder import CharacterSheetBuilder, TraitForCreation
from .experience import total_campaign_experience
from .permission_mngr import PermissionManager
from .rng_chargen import RNGCharGen
from .trait_modifier import TraitModifier

__all__ = [
"ChannelManager",
"CharacterSheetBuilder",
"PermissionManager",
"RNGCharGen",
"total_campaign_experience",
"TraitForCreation",
"TraitModifier",
"ChannelManager",
]
21 changes: 21 additions & 0 deletions src/valentina/controllers/experience.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Controllers for experience."""

from valentina.models import Campaign, User


async def total_campaign_experience(campaign: Campaign) -> tuple[int, int, int]:
"""Return the total experience for the campaign."""
user_id_list = {character.user_owner for character in await campaign.fetch_player_characters()}
available_xp = 0
total_xp = 0
cool_points = 0

for user_id in user_id_list:
user = await User.get(int(user_id))
user_available_xp, user_total_xp, user_cool_points = user.fetch_campaign_xp(campaign)

Check warning on line 15 in src/valentina/controllers/experience.py

View check run for this annotation

Codecov / codecov/patch

src/valentina/controllers/experience.py#L14-L15

Added lines #L14 - L15 were not covered by tests

available_xp += user_available_xp
total_xp += user_total_xp
cool_points += user_cool_points

Check warning on line 19 in src/valentina/controllers/experience.py

View check run for this annotation

Codecov / codecov/patch

src/valentina/controllers/experience.py#L17-L19

Added lines #L17 - L19 were not covered by tests

return available_xp, total_xp, cool_points
11 changes: 9 additions & 2 deletions src/valentina/webui/blueprints/HTMXPartials/blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

from quart import Blueprint

from valentina.webui.constants import TableType
from valentina.webui.constants import TableType, TextType

from .route import EditTableView
from .route import EditTableView, EditTextView

blueprint = Blueprint("partials", __name__, url_prefix="/partials")

Expand All @@ -15,3 +15,10 @@
view_func=EditTableView.as_view(i.value.route_suffix, table_type=i),
methods=["GET", "POST", "DELETE", "PUT"],
)

for t in TextType:
blueprint.add_url_rule(
f"/text/{t.value.route_suffix}",
view_func=EditTextView.as_view(t.value.route_suffix, text_type=t),
methods=["GET", "POST", "PUT"],
)
34 changes: 34 additions & 0 deletions src/valentina/webui/blueprints/HTMXPartials/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@
from valentina.constants import InventoryItemType, TraitCategory


class CampaignDescriptionForm(QuartForm):
"""Form for editing a campaign description and name."""

title = "Campaign Overview"

name = StringField(
"Campaign Name",
default="",
validators=[DataRequired(), Length(min=3, message="Must be at least 3 characters")],
filters=[str.strip, str.title],
)

campaign_description = TextAreaField(
"Description",
validators=[DataRequired(), Length(min=3, message="Must be at least 3 characters")],
description="Markdown is supported",
)

campaign_id = HiddenField()


class UserMacroForm(QuartForm):
"""Form for a user macro."""

Expand Down Expand Up @@ -111,3 +132,16 @@ class CampaignNPCForm(QuartForm):

uuid = HiddenField()
campaign_id = HiddenField()


class CharacterBioForm(QuartForm):
"""A form for editing the character biography."""

title = "The Character's Biography"

bio = TextAreaField(
"Biography",
description="Markdown is supported.",
validators=[DataRequired(), Length(min=5, message="Must be at least 5 characters")],
)
character_id = HiddenField()
174 changes: 168 additions & 6 deletions src/valentina/webui/blueprints/HTMXPartials/route.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"""Routes for handling HTMX partials."""

from typing import TYPE_CHECKING, assert_never
from typing import assert_never
from uuid import UUID

from loguru import logger
from quart import abort, request, session
from quart.views import MethodView
from quart_wtf import QuartForm

from valentina.models import (
Campaign,
Expand All @@ -20,21 +21,26 @@
)
from valentina.utils import truncate_string
from valentina.webui import catalog
from valentina.webui.constants import TableType
from valentina.webui.utils import create_toast, fetch_active_campaign
from valentina.webui.constants import TableType, TextType
from valentina.webui.utils import (
create_toast,
fetch_active_campaign,
fetch_active_character,
sync_channel_to_discord,
update_session,
)
from valentina.webui.utils.discord import post_to_audit_log

from .forms import (
CampaignChapterForm,
CampaignDescriptionForm,
CampaignNPCForm,
CharacterBioForm,
InventoryItemForm,
NoteForm,
UserMacroForm,
)

if TYPE_CHECKING:
from quart_wtf import QuartForm


class EditTableView(MethodView):
"""Handle CRUD operations for editable table items in the web UI.
Expand Down Expand Up @@ -602,3 +608,159 @@ async def delete(self) -> str: # noqa: C901, PLR0912, PLR0915
)

return create_toast(msg, level="SUCCESS")


class EditTextView(MethodView):
"""Handle CRUD operations for text items."""

def __init__(self, text_type: TextType):
"""Initialize view with specified text type."""
self.text_type: TextType = text_type

async def _build_form(self) -> "QuartForm":
"""Build the appropriate form based on text type."""
data = {}

match self.text_type:
case TextType.BIOGRAPHY:
character = await fetch_active_character(request.args.get("parent_id"))
data["bio"] = character.bio
data["character_id"] = str(character.id)

return await CharacterBioForm().create_form(data=data)

case TextType.CAMPAIGN_DESCRIPTION:
campaign = await fetch_active_campaign(request.args.get("parent_id"))
data["name"] = campaign.name
data["description"] = campaign.description
data["campaign_id"] = str(campaign.id)

return await CampaignDescriptionForm().create_form(data=data)

case _: # pragma: no cover
assert_never(self.text_type)

async def _update_character_bio(self, form: QuartForm) -> tuple[str, str]:
"""Update the character bio.
Args:
form: The form data
Returns:
A tuple containing the updated text and message
"""
character = await fetch_active_character(request.args.get("parent_id"))
character.bio = form.data["bio"]
await character.save()
text = character.bio
msg = f"{character.name} bio updated"
return text, msg

async def _update_campaign_description(self, form: QuartForm) -> tuple[str, str]:
"""Update the campaign description and name.
Args:
form: The form data
Returns:
A tuple containing the updated text and message
"""
campaign = await fetch_active_campaign(request.args.get("parent_id"))

is_renamed = campaign.name.strip().lower() != form.data["name"].strip().lower()

campaign.name = form.data["name"].strip().title()
campaign.description = form.data["campaign_description"].strip()
await campaign.save()
text = form.data["campaign_description"].strip()
msg = f"{campaign.name} description updated"

if is_renamed:
await sync_channel_to_discord(obj=campaign, update_type="update")
await update_session()

Check warning on line 680 in src/valentina/webui/blueprints/HTMXPartials/route.py

View check run for this annotation

Codecov / codecov/patch

src/valentina/webui/blueprints/HTMXPartials/route.py#L679-L680

Added lines #L679 - L680 were not covered by tests

return text, msg

async def get(self) -> str:
"""Return just the text for a text item.
Returns:
Rendered HTML fragment containing the text suitable for HTMX integration
"""
match self.text_type:
case TextType.BIOGRAPHY:
character = await fetch_active_character(request.args.get("parent_id"))
text = character.bio

case TextType.CAMPAIGN_DESCRIPTION:
campaign = await fetch_active_campaign(request.args.get("parent_id"))
text = campaign.description

case _: # pragma: no cover
assert_never(self.text_type)

return catalog.render(
"HTMXPartials.EditText.TextDisplayPartial",
TextType=self.text_type,
text=text,
)

async def put(self) -> str:
"""Put the text item."""
form = await self._build_form()

if await form.validate_on_submit():
match self.text_type:
case TextType.BIOGRAPHY:
text, msg = await self._update_character_bio(form)

case TextType.CAMPAIGN_DESCRIPTION:
text, msg = await self._update_campaign_description(form)

await post_to_audit_log(
msg=msg,
view=self.__class__.__name__,
)

return catalog.render(
"HTMXPartials.EditText.TextDisplayPartial",
TextType=self.text_type,
text=text,
)

return catalog.render(
"HTMXPartials.EditText.TextFormPartial",
TextType=self.text_type,
form=form,
method="PUT",
)

async def post(self) -> str:
"""Post the text item."""
form = await self._build_form()

if await form.validate_on_submit():
match self.text_type:
case TextType.BIOGRAPHY:
text, msg = await self._update_character_bio(form)

case TextType.CAMPAIGN_DESCRIPTION:
text, msg = await self._update_campaign_description(form)

await post_to_audit_log(
msg=msg,
view=self.__class__.__name__,
)

return catalog.render(
"HTMXPartials.EditText.TextDisplayPartial",
TextType=self.text_type,
text=text,
)

return catalog.render(

Check warning on line 761 in src/valentina/webui/blueprints/HTMXPartials/route.py

View check run for this annotation

Codecov / codecov/patch

src/valentina/webui/blueprints/HTMXPartials/route.py#L761

Added line #L761 was not covered by tests
"HTMXPartials.EditText.TextFormPartial",
TextType=self.text_type,
form=form,
method="POST",
)
Loading

0 comments on commit 9bd634b

Please sign in to comment.