diff --git a/src/valentina/controllers/trait_modifier.py b/src/valentina/controllers/trait_modifier.py index 56e423a..6b08db4 100644 --- a/src/valentina/controllers/trait_modifier.py +++ b/src/valentina/controllers/trait_modifier.py @@ -18,7 +18,7 @@ def __init__(self, character: Character, user: User) -> None: self.character = character self.user = user - def _can_trait_be_upgraded(self, trait: CharacterTrait, amount: int = 1) -> bool: + def can_trait_be_upgraded(self, trait: CharacterTrait, amount: int = 1) -> bool: """Check if the trait can be upgraded. Args: @@ -34,7 +34,7 @@ def _can_trait_be_upgraded(self, trait: CharacterTrait, amount: int = 1) -> bool return True - def _can_trait_be_downgraded(self, trait: CharacterTrait, amount: int = 1) -> bool: + def can_trait_be_downgraded(self, trait: CharacterTrait, amount: int = 1) -> bool: """Check if the trait can be downgraded. Args: @@ -50,16 +50,21 @@ def _can_trait_be_downgraded(self, trait: CharacterTrait, amount: int = 1) -> bo return True - async def _save_trait(self, trait: CharacterTrait) -> None: - """Saves the updates to the trait and adds the trait to the character if it's not already there. + async def _save_trait(self, trait: CharacterTrait) -> CharacterTrait: + """Saves the updates to the trait and adds the trait to the character if it's not already there. We call the character.add_trait() method to confirm the trait is linked to the character and does not already exist. Args: trait (CharacterTrait): The trait to add. - """ - await trait.save() + Returns: + CharacterTrait: The saved trait. + + Raises: + errors.TraitExistsError: If the trait already exists. Inherited from the character.add_trait() method. + """ await self.character.fetch_all_links() - await self.character.add_trait(character_trait=trait) + await self.character.add_trait(trait) + return trait def cost_to_upgrade(self, trait: CharacterTrait, amount: int = 1) -> int: """Calculate the cost to upgrade a trait. @@ -145,7 +150,7 @@ async def downgrade_with_freebie( Returns: CharacterTrait: The downgraded trait. """ - if self._can_trait_be_downgraded(trait, amount): + if self.can_trait_be_downgraded(trait, amount): savings_from_downgrade = self.savings_from_downgrade(trait, amount) self.character.freebie_points = self.character.freebie_points + savings_from_downgrade @@ -169,7 +174,7 @@ async def downgrade_with_xp( Returns: CharacterTrait: The downgraded trait. """ - if self._can_trait_be_downgraded(trait, amount): + if self.can_trait_be_downgraded(trait, amount): savings_from_downgrade = self.savings_from_downgrade(trait, amount) await self.user.add_campaign_xp( @@ -190,7 +195,7 @@ async def upgrade_with_freebie(self, trait: CharacterTrait, amount: int = 1) -> Returns: CharacterTrait: The upgraded trait. """ - self._can_trait_be_upgraded(trait, amount) + self.can_trait_be_upgraded(trait, amount) cost_to_upgrade = self.cost_to_upgrade(trait, amount) @@ -219,7 +224,7 @@ async def upgrade_with_xp( Returns: CharacterTrait: The upgraded trait. """ - self._can_trait_be_upgraded(trait, amount) + self.can_trait_be_upgraded(trait, amount) cost_to_upgrade = self.cost_to_upgrade(trait, amount) diff --git a/src/valentina/discord/cogs/characters.py b/src/valentina/discord/cogs/characters.py index e3782f7..491de9a 100644 --- a/src/valentina/discord/cogs/characters.py +++ b/src/valentina/discord/cogs/characters.py @@ -50,7 +50,7 @@ present_embed, show_sheet, ) -from valentina.models import AWSService, Character, CharacterSheetSection, User +from valentina.models import AWSService, Character, CharacterSheetSection, CharacterTrait, User from valentina.utils import errors from valentina.utils.helpers import ( fetch_data_from_url, @@ -488,8 +488,14 @@ async def add_trait( ) if not is_confirmed: return - - await character.add_trait(category, name.title(), value, max_value=max_value) + trait = CharacterTrait( + name=name.title(), + category_name=category.name, + value=value, + max_value=max_value or 5, + character=str(character.id), + ) + await character.add_trait(trait) await interaction.edit_original_response(embed=confirmation_embed, view=None) diff --git a/src/valentina/discord/cogs/storyteller.py b/src/valentina/discord/cogs/storyteller.py index ab908e2..91156aa 100644 --- a/src/valentina/discord/cogs/storyteller.py +++ b/src/valentina/discord/cogs/storyteller.py @@ -55,7 +55,7 @@ sheet_embed, show_sheet, ) -from valentina.models import AWSService, Character, User +from valentina.models import AWSService, Character, CharacterTrait, User from valentina.utils.helpers import ( fetch_data_from_url, ) @@ -459,7 +459,14 @@ async def add_trait( if not is_confirmed: return - await character.add_trait(category, name.title(), value, max_value=max_value) + trait = CharacterTrait( + name=name.title(), + category_name=category.name, + value=value, + max_value=max_value, + character=str(character.id), + ) + await character.add_trait(trait) await interaction.edit_original_response(embed=confirmation_embed, view=None) diff --git a/src/valentina/models/character.py b/src/valentina/models/character.py index 47e0740..0dddb04 100644 --- a/src/valentina/models/character.py +++ b/src/valentina/models/character.py @@ -29,7 +29,7 @@ ) from valentina.models.aws import AWSService from valentina.utils import errors -from valentina.utils.helpers import get_max_trait_value, num_to_circles, time_now +from valentina.utils.helpers import num_to_circles, time_now from .note import Note @@ -269,28 +269,12 @@ async def add_image(self, extension: str, data: bytes) -> str: # pragma: no cov async def add_trait( self, - category: TraitCategory | None = None, - name: str | None = None, - value: int | None = 0, - max_value: int | None = None, - display_on_sheet: bool = True, - is_custom: bool = True, - character_trait: CharacterTrait | None = None, + trait: "CharacterTrait", ) -> "CharacterTrait": - """Create a new trait for the character. - - Add a new trait to the character's list of traits. Check if the trait already exists, - determine if it's a custom trait, and set the appropriate maximum value. Save the new - trait to the database and update the character's trait list. + """Associate a trait with the character. Args: - category (TraitCategory): The category of the trait. - name (str): The name of the trait. - value (int): The initial value of the trait. - max_value (int | None, optional): The maximum value for the trait. Defaults to None. - display_on_sheet (bool, optional): Whether to display the trait on the character sheet. Defaults to True. - is_custom (bool, optional): Whether the trait is custom. Defaults to True. - character_trait (CharacterTrait): An existing trait object to add to the character. Defaults to None. + trait (CharacterTrait): The trait to add to the character. Returns: CharacterTrait: The newly created trait object. @@ -300,50 +284,28 @@ async def add_trait( """ await self.fetch_all_links() - if character_trait: - if character_trait.character != str(self.id): - character_trait.character = str(self.id) - - await character_trait.save() - - if character_trait not in self.traits: - self.traits.append(character_trait) - await self.save() - return character_trait + for existing_trait in cast(list[CharacterTrait], self.traits): + # If the trait already exists in the character's trait list, return it + if trait.id == existing_trait.id: + await trait.save() + return trait - if not category or not name or not value: - msg = "Category, name, and value are required to create a new trait." - raise ValueError(msg) + # Because a user can manually create a trait with the same name, we need to check for that as well + if ( + trait.name.lower() == existing_trait.name.lower() + and trait.category_name == existing_trait.category_name + ): + msg = f"Trait named '{trait.name}' already exists in category '{trait.category_name}' for character '{self.name}'" + raise errors.TraitExistsError(msg) - # Check if the trait already exists - for trait in cast(list[CharacterTrait], self.traits): - if trait.name == name and trait.category_name == category.name.upper(): - raise errors.TraitExistsError - - # Check if the trait is custom - if name.lower() in [x.lower() for x in category.value.COMMON] + [ - x.lower() for x in getattr(category.value, self.char_class_name, []) - ]: - is_custom = False - max_value = get_max_trait_value(name, category.name) - - # Create the new trait - new_trait = CharacterTrait( - category_name=category.name, - character=str(self.id), - name=name, - value=value, - display_on_sheet=display_on_sheet, - is_custom=is_custom, - max_value=max_value or get_max_trait_value(name, category.name), - ) - await new_trait.save() + if trait.character != str(self.id): + trait.character = str(self.id) - # Add the new trait to the character - self.traits.append(new_trait) + await trait.save() + self.traits.append(trait) await self.save() - return new_trait + return trait async def associate_with_campaign(self, new_campaign: "Campaign") -> bool: """Associate a character with a campaign. diff --git a/src/valentina/webui/blueprints/character_create/route_create_full.py b/src/valentina/webui/blueprints/character_create/route_create_full.py index 19c7d9b..5511883 100644 --- a/src/valentina/webui/blueprints/character_create/route_create_full.py +++ b/src/valentina/webui/blueprints/character_create/route_create_full.py @@ -133,6 +133,10 @@ async def get(self) -> str: character_type=request.args.get("character_type", "player"), campaign_id=request.args.get("campaign_id", session.get("ACTIVE_CAMPAIGN_ID", "")), ), + error_msg=request.args.get("error_msg", ""), + success_msg=request.args.get("success_msg", ""), + info_msg=request.args.get("info_msg", ""), + warning_msg=request.args.get("warning_msg", ""), ) async def post(self) -> str | Response: @@ -454,6 +458,9 @@ async def post(self, character_id: str) -> str | Response: url_for( "character_view.view", character_id=character_id, - success_msg="Character created successfully!", + error_msg=request.args.get("error_msg", ""), + success_msg=request.args.get("success_msg", ""), + info_msg=request.args.get("info_msg", ""), + warning_msg=request.args.get("warning_msg", ""), ) ) diff --git a/src/valentina/webui/blueprints/character_create/route_rng.py b/src/valentina/webui/blueprints/character_create/route_rng.py index 999934b..ed0c8dc 100644 --- a/src/valentina/webui/blueprints/character_create/route_rng.py +++ b/src/valentina/webui/blueprints/character_create/route_rng.py @@ -67,6 +67,10 @@ async def get(self) -> str: remaining_xp=remaining_xp, rng_characters=characters, character_type=request.args.get("character_type", "player"), + error_msg=request.args.get("error_msg", ""), + success_msg=request.args.get("success_msg", ""), + info_msg=request.args.get("info_msg", ""), + warning_msg=request.args.get("warning_msg", ""), ) async def post(self) -> str | Response: diff --git a/src/valentina/webui/blueprints/character_edit/blueprint.py b/src/valentina/webui/blueprints/character_edit/blueprint.py index ac94bc3..9462d2d 100644 --- a/src/valentina/webui/blueprints/character_edit/blueprint.py +++ b/src/valentina/webui/blueprints/character_edit/blueprint.py @@ -8,11 +8,22 @@ blueprint.add_url_rule( "/character//spendfreebie", - view_func=SpendPoints.as_view("freebie", spend_type=SpendPointsType.FREEBIE), + view_func=SpendPoints.as_view( + SpendPointsType.FREEBIE.value, spend_type=SpendPointsType.FREEBIE + ), methods=["GET", "POST"], ) blueprint.add_url_rule( "/character//spendexperience", - view_func=SpendPoints.as_view("experience", spend_type=SpendPointsType.EXPERIENCE), + view_func=SpendPoints.as_view( + SpendPointsType.EXPERIENCE.value, spend_type=SpendPointsType.EXPERIENCE + ), + methods=["GET", "POST"], +) +blueprint.add_url_rule( + "/character//spendstoryteller", + view_func=SpendPoints.as_view( + SpendPointsType.STORYTELLER.value, spend_type=SpendPointsType.STORYTELLER + ), methods=["GET", "POST"], ) diff --git a/src/valentina/webui/blueprints/character_edit/route_spend_points.py b/src/valentina/webui/blueprints/character_edit/route_spend_points.py index d1094cc..77878e9 100644 --- a/src/valentina/webui/blueprints/character_edit/route_spend_points.py +++ b/src/valentina/webui/blueprints/character_edit/route_spend_points.py @@ -15,7 +15,7 @@ from valentina.webui import catalog from valentina.webui.utils.discord import post_to_audit_log from valentina.webui.utils.forms import ValentinaForm -from valentina.webui.utils.helpers import fetch_active_character, fetch_user, is_storyteller +from valentina.webui.utils.helpers import fetch_active_character, fetch_user class SpendPointsType(Enum): @@ -27,7 +27,7 @@ class SpendPointsType(Enum): class SpendPoints(MethodView): - """View to manage freebie point spending. This page uses HTMX to validate each trait as it is changed.""" + """View and manage upgrading/downgrading traits for a character. Depending on the route, different types of points can be spent. Blueprints to this class must specify the SpendPointsType as part of the view_func.""" decorators: ClassVar = [requires_authorization] @@ -40,6 +40,68 @@ def __init__(self, spend_type: SpendPointsType) -> None: ) self.spend_type = spend_type + async def _get_campaign_experience( + self, character: "Character", character_owner: User = None + ) -> int: + """Get the experience points for the character's campaign. + + Args: + character (Character): The character to check. + character_owner (User, optional): The character's owner. Defaults to None. + + Returns: + int: The experience points for the character's campaign. + """ + if not character_owner: + character_owner = await User.get(character.user_owner, fetch_links=False) + + campaign = await Campaign.get(character.campaign) + campaign_experience, _, _ = character_owner.fetch_campaign_xp(campaign) + return campaign_experience + + async def _downgrade_trait( + self, character: Character, trait: CharacterTrait, new_value: int + ) -> str: + """Downgrade a trait to a new value. + + Args: + character (Character): The character to upgrade the trait for. + trait (CharacterTrait): The trait to upgrade. + new_value (int): The new value for the trait. + + Returns: + str: The success message. + """ + user = await fetch_user() + trait_modifier = TraitModifier(character, user) + difference = trait.value - new_value + savings = trait_modifier.savings_from_downgrade(trait, difference) + + match self.spend_type: + case SpendPointsType.FREEBIE: + downgraded_trait = await trait_modifier.downgrade_with_freebie(trait, difference) + case SpendPointsType.EXPERIENCE: + campaign = await Campaign.get(character.campaign) + downgraded_trait = await trait_modifier.downgrade_with_xp( + trait, campaign, difference + ) + case SpendPointsType.STORYTELLER: + if trait_modifier.can_trait_be_downgraded(trait, difference): + trait.value = new_value + downgraded_trait = await character.add_trait(trait) + case _: + assert_never() + + player_message = ( + f" recouping {savings} {self.spend_type.value} points" + if self.spend_type != SpendPointsType.STORYTELLER + else "" + ) + await post_to_audit_log( + msg=f"Downgraded {character.name}'s {trait.name} to {downgraded_trait.value}{player_message}" + ) + return f"Downgraded {trait.name} to {downgraded_trait.value}{player_message}" + async def _parse_form_data( self, character: Character, form: dict ) -> tuple[CharacterTrait, int]: @@ -58,10 +120,9 @@ async def _parse_form_data( if form_key.lower().startswith("new_"): target_value = int(next(iter(form.values()))) name, category, max_value = form_key.split("_")[1:] - trait = CharacterTrait( - name=name, - category_name=category, + name=name.strip().title(), + category_name=category.strip().upper(), max_value=int(max_value), value=0, is_custom=False, @@ -72,16 +133,19 @@ async def _parse_form_data( custom_trait_name = next(iter(form.values())) target_value = 1 category = form_key.split("_")[1] - + if not custom_trait_name: + msg = "Trait name can not be empty" + raise ValueError(msg) trait = CharacterTrait( - name=custom_trait_name, - category_name=category, - max_value=get_max_trait_value(custom_trait_name, category), + name=custom_trait_name.strip().title(), + category_name=category.strip().upper(), + max_value=get_max_trait_value( + custom_trait_name.strip().title(), category.strip().upper() + ), value=0, is_custom=True, character=str(character.id), ) - else: target_value = int(next(iter(form.values()))) trait = await CharacterTrait.get(form_key) @@ -108,76 +172,26 @@ async def _upgrade_trait( match self.spend_type: case SpendPointsType.FREEBIE: - updated_trait = await trait_modifier.upgrade_with_freebie(trait, difference) + upgraded_trait = await trait_modifier.upgrade_with_freebie(trait, difference) case SpendPointsType.EXPERIENCE: campaign = await Campaign.get(character.campaign) - updated_trait = await trait_modifier.upgrade_with_xp(trait, campaign, difference) + upgraded_trait = await trait_modifier.upgrade_with_xp(trait, campaign, difference) case SpendPointsType.STORYTELLER: - # TODO: Add storyteller trait upgrading - pass + if trait_modifier.can_trait_be_upgraded(trait, difference): + trait.value = new_value + upgraded_trait = await character.add_trait(trait) case _: assert_never() - await post_to_audit_log( - msg=f"Upgraded {character.name}'s {trait.name} to {updated_trait.value} costing {cost} {self.spend_type.value} points" + player_message = ( + f" for {cost} {self.spend_type.value} points" + if self.spend_type != SpendPointsType.STORYTELLER + else "" ) - return f"Upgraded {trait.name} to {updated_trait.value} for {cost} {self.spend_type.value} points" - - async def _downgrade_trait( - self, character: Character, trait: CharacterTrait, new_value: int - ) -> str: - """Downgrade a trait to a new value. - - Args: - character (Character): The character to upgrade the trait for. - trait (CharacterTrait): The trait to upgrade. - new_value (int): The new value for the trait. - - Returns: - str: The success message. - """ - user = await fetch_user() - trait_modifier = TraitModifier(character, user) - difference = trait.value - new_value - savings = trait_modifier.savings_from_downgrade(trait, difference) - - match self.spend_type: - case SpendPointsType.FREEBIE: - downgraded_trait = await trait_modifier.downgrade_with_freebie(trait, difference) - case SpendPointsType.EXPERIENCE: - campaign = await Campaign.get(character.campaign) - downgraded_trait = await trait_modifier.downgrade_with_xp( - trait, campaign, difference - ) - case SpendPointsType.STORYTELLER: - # TODO: Add storyteller trait downgrading - pass - case _: - assert_never() - await post_to_audit_log( - msg=f"Downgraded {character.name}'s {trait.name} to {downgraded_trait.value} recouping {savings} {self.spend_type.value} points" + msg=f"Upgraded {character.name}'s {trait.name} to {upgraded_trait.value}{player_message}" ) - return f"Downgraded {trait.name} to {downgraded_trait.value} for {savings} {self.spend_type.value} points" - - async def _get_campaign_experience( - self, character: "Character", character_owner: User = None - ) -> int: - """Get the experience points for the character's campaign. - - Args: - character (Character): The character to check. - character_owner (User, optional): The character's owner. Defaults to None. - - Returns: - int: The experience points for the character's campaign. - """ - if not character_owner: - character_owner = await User.get(character.user_owner, fetch_links=False) - - campaign = await Campaign.get(character.campaign) - campaign_experience, _, _ = character_owner.fetch_campaign_xp(campaign) - return campaign_experience + return f"Upgraded {trait.name} to {upgraded_trait.value}{player_message}" async def get(self, character_id: str = "") -> str | Response: """Manage GET requests.""" @@ -186,7 +200,7 @@ async def get(self, character_id: str = "") -> str | Response: abort(HTTPStatus.BAD_REQUEST.value) character_owner = await User.get(character.user_owner, fetch_links=False) - if not is_storyteller and character_owner.id != session.get("USER_ID"): + if not session["IS_STORYTELLER"] and character_owner.id != session.get("USER_ID"): return redirect( # type: ignore [return-value] url_for( "character_view.view", @@ -194,6 +208,14 @@ async def get(self, character_id: str = "") -> str | Response: error_msg="You do not have permission to edit this character", ) ) + if not session["IS_STORYTELLER"] and self.spend_type == SpendPointsType.STORYTELLER: + return redirect( # type: ignore [return-value] + url_for( + "character_view.view", + character_id=character_id, + error_msg="Only storytellers can update characters without spending points", + ) + ) campaign_experience = ( await self._get_campaign_experience(character) @@ -214,6 +236,8 @@ async def get(self, character_id: str = "") -> str | Response: post_url=url_for(f"character_edit.{self.spend_type.value}", character_id=character_id), error_msg=request.args.get("error_msg", ""), success_msg=request.args.get("success_msg", ""), + info_msg=request.args.get("info_msg", ""), + warning_msg=request.args.get("warning_msg", ""), ) async def post(self, character_id: str = "") -> Response: @@ -223,61 +247,39 @@ async def post(self, character_id: str = "") -> Response: abort(HTTPStatus.BAD_REQUEST.value) form = await request.form - trait, target_value = await self._parse_form_data(character, form) - success_msg = "" try: - if trait.value < target_value: - success_msg = await self._upgrade_trait(character, trait, target_value) - elif trait.value > target_value: - success_msg = await self._downgrade_trait(character, trait, target_value) - except errors.TraitAtMaxValueError: - return Response( - headers={ - "HX-Redirect": url_for( - "character_edit.freebie", - character_id=str(character.id), - error_msg=f"{trait.name} is already at max value", - ) - } - ) - except errors.NotEnoughFreebiePointsError: - return Response( - headers={ - "HX-Redirect": url_for( - "character_edit.freebie", - character_id=str(character.id), - error_msg=f"Not enough freebie points to upgrade {trait.name} to {target_value}", - ) - } - ) - except errors.TraitExistsError: - return Response( - headers={ - "HX-Redirect": url_for( - "character_edit.freebie", - character_id=str(character.id), - error_msg=f'Trait {trait.name} already exists in {trait.category.name.title()}', - ) - } - ) - except errors.NotEnoughExperienceError: + trait, target_value = await self._parse_form_data(character, form) + except ValueError as e: return Response( headers={ "HX-Redirect": url_for( - "character_edit.experience", + f"character_edit.{self.spend_type.value}", character_id=str(character.id), - error_msg=f"Not enough experience points to upgrade {trait.name} to {target_value}", + error_msg=str(e), ) } ) - except errors.TraitAtMinValueError: + + success_msg = "" + try: + if trait.value < target_value: + success_msg = await self._upgrade_trait(character, trait, target_value) + elif trait.value > target_value: + success_msg = await self._downgrade_trait(character, trait, target_value) + except ( + errors.TraitAtMaxValueError, + errors.NotEnoughFreebiePointsError, + errors.TraitExistsError, + errors.NotEnoughExperienceError, + errors.TraitAtMinValueError, + ) as e: return Response( headers={ "HX-Redirect": url_for( - "character_edit.experience", + f"character_edit.{self.spend_type.value}", character_id=str(character.id), - error_msg=f"{trait.name} can not be lowered below 0", + error_msg=str(e), ) } ) diff --git a/src/valentina/webui/blueprints/character_edit/templates/character_edit/SpendPoints.jinja b/src/valentina/webui/blueprints/character_edit/templates/character_edit/SpendPoints.jinja index 44cee09..716be37 100644 --- a/src/valentina/webui/blueprints/character_edit/templates/character_edit/SpendPoints.jinja +++ b/src/valentina/webui/blueprints/character_edit/templates/character_edit/SpendPoints.jinja @@ -8,17 +8,21 @@ #} - {{ character.name }}: Spend {{ spend_type.value | title }} Points + Update {{ character.name }}
- Spend your {{ spend_type.value }} points to finalize your character. Any trait values changed downward will have their value added to your {{ spend_type.value }} points. -
-
- {% if spend_type.name == "FREEBIE" %} - You have {{ character.freebie_points }} freebie points to spend. - {% endif %} - {% if spend_type.name == "EXPERIENCE" %} - You have {{ campaign_experience }} experience points to spend. + {% if spend_type.name == "STORYTELLER" %} + Storytellers can update any character without needing experience. + {% else %} + Spend your {{ spend_type.value }} points to finalize your character. Any trait values changed downward will have their value added to your {{ spend_type.value }} points. +
+
+ {% if spend_type.name == "FREEBIE" %} + You have {{ character.freebie_points }} freebie points to spend. + {% endif %} + {% if spend_type.name == "EXPERIENCE" %} + You have {{ campaign_experience }} experience points to spend. + {% endif %} {% endif %}
diff --git a/src/valentina/webui/blueprints/character_view/route.py b/src/valentina/webui/blueprints/character_view/route.py index 2093b4c..8be83d5 100644 --- a/src/valentina/webui/blueprints/character_view/route.py +++ b/src/valentina/webui/blueprints/character_view/route.py @@ -178,8 +178,8 @@ async def get(self, character_id: str = "") -> str: sheet_builder = CharacterSheetBuilder(character=character) sheet_data = sheet_builder.fetch_sheet_character_traits(show_zeros=False) - storyteller_data = await is_storyteller() - profile_data = await sheet_builder.fetch_sheet_profile(storyteller_view=storyteller_data) + user_is_storyteller = await is_storyteller() + profile_data = await sheet_builder.fetch_sheet_profile(storyteller_view=user_is_storyteller) return catalog.render( "character_view.Main", @@ -190,6 +190,8 @@ async def get(self, character_id: str = "") -> str: campaign_experience=await self._get_campaign_experience(character, character_owner), error_msg=request.args.get("error_msg", ""), success_msg=request.args.get("success_msg", ""), + info_msg=request.args.get("info_msg", ""), + warning_msg=request.args.get("warning_msg", ""), ) diff --git a/src/valentina/webui/blueprints/character_view/templates/character_view/EditButtons.jinja b/src/valentina/webui/blueprints/character_view/templates/character_view/EditButtons.jinja index 6434f13..cf92f18 100644 --- a/src/valentina/webui/blueprints/character_view/templates/character_view/EditButtons.jinja +++ b/src/valentina/webui/blueprints/character_view/templates/character_view/EditButtons.jinja @@ -20,5 +20,8 @@ data-bs-placement="top" data-bs-title="{{ campaign_experience }} experience points remaining">Spend experience {% endif %} - + {% if session["IS_STORYTELLER"] %} + Storyteller Update Character + {% endif %} diff --git a/src/valentina/webui/blueprints/gameplay/route.py b/src/valentina/webui/blueprints/gameplay/route.py index 444ded8..6825ea3 100644 --- a/src/valentina/webui/blueprints/gameplay/route.py +++ b/src/valentina/webui/blueprints/gameplay/route.py @@ -249,4 +249,8 @@ async def get(self) -> str: campaign=await fetch_active_campaign(), dice_sizes=self.dice_size_values, form=gameplay_form, + error_msg=request.args.get("error_msg", ""), + success_msg=request.args.get("success_msg", ""), + info_msg=request.args.get("info_msg", ""), + warning_msg=request.args.get("warning_msg", ""), ) diff --git a/src/valentina/webui/blueprints/homepage/route.py b/src/valentina/webui/blueprints/homepage/route.py index 7243f2b..79a41ff 100644 --- a/src/valentina/webui/blueprints/homepage/route.py +++ b/src/valentina/webui/blueprints/homepage/route.py @@ -2,7 +2,7 @@ import random -from quart import session +from quart import request, session from quart.views import MethodView from valentina.constants import BOT_DESCRIPTIONS @@ -24,4 +24,10 @@ async def get(self) -> str: ) await update_session() - return catalog.render("homepage.Loggedin") + return catalog.render( + "homepage.Loggedin", + error_msg=request.args.get("error_msg", ""), + success_msg=request.args.get("success_msg", ""), + info_msg=request.args.get("info_msg", ""), + warning_msg=request.args.get("warning_msg", ""), + ) diff --git a/src/valentina/webui/blueprints/homepage/templates/homepage/Loggedin.jinja b/src/valentina/webui/blueprints/homepage/templates/homepage/Loggedin.jinja index afd869b..bfb887b 100644 --- a/src/valentina/webui/blueprints/homepage/templates/homepage/Loggedin.jinja +++ b/src/valentina/webui/blueprints/homepage/templates/homepage/Loggedin.jinja @@ -1,19 +1,58 @@ {# def #} - -{% set card_style = "card h-100 border border-2 shadow bg-light-subtle" %} -Welcome {{ session["USER_NAME"] }} -
- {# ----- STORYTELLER CHARACTERS ----- #} - {% if session['IS_STORYTELLER'] %} - + + + {% set card_style = "card h-100 border border-2 shadow bg-light-subtle" %} + + Welcome {{ session["USER_NAME"] }} +
+ {# ----- STORYTELLER CHARACTERS ----- #} + {% if session['IS_STORYTELLER'] %} + +
+
+

View storyteller characters

+
+
+ +
+ {# djlint:off J018 #} + Go +
+
+
+

Or create a new storyteller character

+
+ Create +
+
+
+ {% endif %} + {# ----- PLAYER CHARACTERS ----- #} + + {% set player_card_title = 'Player Characters' if session['IS_STORYTELLER'] else 'Your Characters' %} + +
-

View storyteller characters

+

+ View + {% if session['IS_STORYTELLER'] %} + all player + {% else %} + your + {% endif %} + characters +

- +
{# djlint:off J018 #}
- {% endif %} - {# ----- PLAYER CHARACTERS ----- #} - {% set player_card_title = 'Player Characters' if session['IS_STORYTELLER'] else 'Your Characters' %} - -
-
-

- View - {% if session['IS_STORYTELLER'] %} - all player - {% else %} - your - {% endif %} - characters -

-
-
- -
- {# djlint:off J018 #} -
Go + {# ----- CAMPAIGNS ----- #} + +

View the guild's campaigns

+
+
+
+ {# djlint:off J018 #} + Go
-
-

Or create a new character

-
- Create -
-
- - {# ----- CAMPAIGNS ----- #} - -

View the guild's campaigns

-
-
- -
- {# djlint:off J018 #} - Go + + {# ----- GAMEPLAY ----- #} + +

Roll dice for your characters.

+ Go +
-
- {# ----- GAMEPLAY ----- #} - -

Roll dice for your characters.

- Go -
-
diff --git a/src/valentina/webui/shared/PageLayout.jinja b/src/valentina/webui/shared/PageLayout.jinja index d870fb5..43ba6b3 100644 --- a/src/valentina/webui/shared/PageLayout.jinja +++ b/src/valentina/webui/shared/PageLayout.jinja @@ -46,20 +46,75 @@ {# include nav bar only when user is logged in #} {% if session["USER_ID"] is defined and session["GUILD_ID"] is defined %}{% endif %} +
- {%- if attrs.get("success-msg", "") %} - - {% endif -%} - {%- if attrs.get("error-msg", "") %} - - {% endif -%} - {%- if attrs.get("warning-msg", "") %} - - {% endif -%} - {%- if attrs.get("info-msg", "") %} - - {% endif -%} +
+ {%- if attrs.get("success-msg", "") %} + + {% endif -%} + {%- if attrs.get("error-msg", "") %} + + {% endif -%} + {%- if attrs.get("warning-msg", "") %} + + {% endif -%} + {%- if attrs.get("info-msg", "") %} + + {% endif -%} +
{{ content }}
+ + diff --git a/tests/controllers/test_character_sheet_builder.py b/tests/controllers/test_character_sheet_builder.py index 439c177..9bbf8c6 100644 --- a/tests/controllers/test_character_sheet_builder.py +++ b/tests/controllers/test_character_sheet_builder.py @@ -6,6 +6,7 @@ from tests.factories import * from valentina.constants import CharClass, CharSheetSection, TraitCategory from valentina.controllers import CharacterSheetBuilder +from valentina.models import CharacterTrait from valentina.utils import console @@ -19,7 +20,15 @@ async def test_fetch_sheet_character_traits(debug, trait_factory, character_fact # for section in CharSheetSection.get_members_in_order(): for cat in TraitCategory.get_members_in_order(): for trait_name in cat.get_all_class_trait_names(char_class=char_class): - await character.add_trait(category=cat, name=trait_name, value=1, max_value=5) + await character.add_trait( + CharacterTrait( + name=trait_name, + category_name=cat.name, + value=1, + max_value=5, + character=str(character.id), + ) + ) sheet_builder = CharacterSheetBuilder(character=character) sheet_data = sheet_builder.fetch_sheet_character_traits(show_zeros=False) diff --git a/tests/controllers/test_trait_modifier.py b/tests/controllers/test_trait_modifier.py index c5d3eb4..9689bb7 100644 --- a/tests/controllers/test_trait_modifier.py +++ b/tests/controllers/test_trait_modifier.py @@ -34,12 +34,12 @@ async def test_helper_functions( # WHEN the trait is upgraded with XP trait_modifier = TraitModifier(character=character, user=user) - trait_modifier._can_trait_be_upgraded(trait) + trait_modifier.can_trait_be_upgraded(trait) assert trait_modifier.cost_to_upgrade(trait) == 6 assert trait_modifier.savings_from_downgrade(trait) == 4 - assert trait_modifier._can_trait_be_downgraded(trait) - assert trait_modifier._can_trait_be_upgraded(trait) + assert trait_modifier.can_trait_be_downgraded(trait) + assert trait_modifier.can_trait_be_upgraded(trait) trait.value = 1 assert trait_modifier.cost_to_upgrade(trait) == 4 @@ -50,8 +50,8 @@ async def test_helper_functions( assert trait_modifier.savings_from_downgrade(trait) == 2 with pytest.raises(errors.TraitAtMinValueError): assert trait_modifier.savings_from_downgrade(trait, amount=2) - assert trait_modifier._can_trait_be_downgraded(trait) - assert trait_modifier._can_trait_be_upgraded(trait) + assert trait_modifier.can_trait_be_downgraded(trait) + assert trait_modifier.can_trait_be_upgraded(trait) trait.value = 0 assert trait_modifier.cost_to_upgrade(trait) == 2 @@ -59,16 +59,16 @@ async def test_helper_functions( assert trait_modifier.savings_from_downgrade(trait) with pytest.raises(errors.TraitAtMinValueError): - trait_modifier._can_trait_be_downgraded(trait) + trait_modifier.can_trait_be_downgraded(trait) - assert trait_modifier._can_trait_be_upgraded(trait) + assert trait_modifier.can_trait_be_upgraded(trait) trait.value = 5 with pytest.raises(errors.TraitAtMaxValueError): assert trait_modifier.cost_to_upgrade(trait) assert trait_modifier.savings_from_downgrade(trait) == 10 with pytest.raises(errors.TraitAtMaxValueError): - trait_modifier._can_trait_be_upgraded(trait) + trait_modifier.can_trait_be_upgraded(trait) @pytest.mark.drop_db diff --git a/tests/models/test_character_model.py b/tests/models/test_character_model.py index db03009..7243498 100644 --- a/tests/models/test_character_model.py +++ b/tests/models/test_character_model.py @@ -73,114 +73,57 @@ async def test_full_name(character_factory): @pytest.mark.drop_db -async def test_add_custom_trait(character_factory): - """Test the add_trait method.""" - # GIVEN a character - character = character_factory.build() - await character.insert() - - # WHEN adding a trait - trait = await character.add_trait(TraitCategory.BACKGROUNDS, "Something", 3, 5) - - # THEN the trait is added to the character - assert len(character.traits) == 1 - assert character.traits[0] == trait - - # AND the trait is saved to the database - all_traits = await CharacterTrait.find_all().to_list() - assert len(all_traits) == 1 - assert all_traits[0].name == "Something" - assert all_traits[0].value == 3 - assert all_traits[0].max_value == 5 - assert all_traits[0].category_name == TraitCategory.BACKGROUNDS.name - assert all_traits[0].character == str(character.id) - assert all_traits[0].is_custom - - -@pytest.mark.drop_db -async def test_add_trait(character_factory): - """Test the add_trait method.""" - # GIVEN a character - character = character_factory.build() - await character.insert() - - # WHEN adding a trait that exists in TraitCategory enum - trait = await character.add_trait(TraitCategory.PHYSICAL, "Strength", 2, 10) - - # THEN the trait is added to the character - assert len(character.traits) == 1 - assert character.traits[0] == trait - - # AND the trait is saved to the database - # With the max_value reset to the enum defaults - all_traits = await CharacterTrait.find_all().to_list() - assert len(all_traits) == 1 - assert all_traits[0].name == "Strength" - assert all_traits[0].value == 2 - assert all_traits[0].max_value == 5 - assert all_traits[0].category_name == TraitCategory.PHYSICAL.name - assert all_traits[0].character == str(character.id) - assert not all_traits[0].is_custom - - -@pytest.mark.no_db async def test_add_trait_already_exists(character_factory, trait_factory): """Test the add_trait method.""" # GIVEN a character with an existing trait trait = trait_factory.build( category_name=TraitCategory.PHYSICAL.name, name="Strength", value=2, max_value=10 ) + await trait.save() character = character_factory.build(traits=[trait]) + await character.save() - # WHEN adding a trait that already exists on the character + # When the same trait is added a second time, the existing trait is returned + existing_trait = await CharacterTrait.get(trait.id) + assert await character.add_trait(existing_trait) == trait + + # WHEN adding a new trait with the same name and category # THEN a TraitExistsError is raised with pytest.raises(errors.TraitExistsError): - await character.add_trait(TraitCategory.PHYSICAL, "Strength", 2, 10) - - -@pytest.mark.drop_db -async def test_add_trait_no_values(character_factory): - """Test the add_trait method.""" - # GIVEN a character - character = character_factory.build() - await character.insert() - - # WHEN adding a trait without required values - # THEN a ValueError is raised - with pytest.raises(ValueError, match="required to create a new trait"): - await character.add_trait(name="Strength", value=1) - with pytest.raises(ValueError, match="required to create a new trait"): - await character.add_trait(name="Strength", category="PHYSICAL") - with pytest.raises(ValueError, match="required to create a new trait"): - await character.add_trait(value=1, category="PHYSICAL") + await character.add_trait( + CharacterTrait( + name="Strength", + category_name="PHYSICAL", + value=4, + max_value=5, + character=str(character.id), + ) + ) @pytest.mark.drop_db -async def test_add_trait_charactertrait(character_factory, trait_factory) -> None: +async def test_add_trait(character_factory) -> None: """Test the add_trait method when providing a CharacterTrait.""" - trait = trait_factory.build( - category_name=TraitCategory.PHYSICAL.name, name="Strength", value=2, max_value=10 + trait = CharacterTrait( + category_name=TraitCategory.PHYSICAL.name, + name="Strength", + value=2, + max_value=10, + character="something", ) await trait.insert() character = character_factory.build() await character.insert() # WHEN adding a CharacterTrait object to the character - new_trait = await character.add_trait(character_trait=trait) + new_trait = await character.add_trait(trait) - # THEN the trait is added to the character + # THEN the trait is added to the character and the database char = await Character.get(character.id, fetch_links=True) assert len(char.traits) == 1 assert char.traits[0] == new_trait assert char.traits[0].character == str(char.id) - - # When adding a trait that already exists on the character - # assert that no errors are raised - new_trait = await char.add_trait(character_trait=trait) - c = await Character.get(character.id, fetch_links=True) - assert len(c.traits) == 1 - assert c.traits[0] == new_trait - assert c.traits[0].character == str(c.id) + assert await CharacterTrait.get(new_trait.id) == new_trait @pytest.mark.no_db