Implement GM notes.

Closes #42
This commit is contained in:
Gabriel Augendre 2022-12-28 09:27:27 +01:00
parent fb0b97e8bb
commit f3f3b33c12
12 changed files with 115 additions and 4 deletions

View file

@ -0,0 +1,18 @@
# Generated by Django 4.1.4 on 2022-12-28 07:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("character", "0039_alter_character_profile_picture"),
]
operations = [
migrations.AddField(
model_name="character",
name="gm_notes",
field=models.TextField(blank=True, verbose_name="notes MJ"),
),
]

View file

@ -1 +1 @@
0038_character_profile_picture 0040_character_gm_notes

View file

@ -79,16 +79,35 @@ class CharacterManager(models.Manager):
class CharacterQuerySet(models.QuerySet): class CharacterQuerySet(models.QuerySet):
def managed_by(self, user): def managed_by(self, user):
"""
Return characters managed by the given user.
Characters are managed by a user if they own the character
or if they are the game master for a group in which the character plays.
"""
from party.models import Party from party.models import Party
return self.filter( return self.filter(
Q(player=user) | Q(parties__in=Party.objects.managed_by(user)) Q(player=user) | Q(parties__in=Party.objects.managed_by(user))
) )
def mastered_by(self, user):
"""Return characters in groups where the given user is the game master."""
from party.models import Party
return self.filter(parties__in=Party.objects.managed_by(user))
def owned_by(self, user): def owned_by(self, user):
"""Return characters either owned by the given user."""
return self.filter(player=user) return self.filter(player=user)
def friendly_to(self, user): def friendly_to(self, user):
"""
Return characters friendly to the given users.
Friendly characters are either owned by the given user
or in a party related to the given user.
"""
from party.models import Party from party.models import Party
return self.filter( return self.filter(
@ -229,6 +248,7 @@ class Character(models.Model):
) )
notes = models.TextField(blank=True, verbose_name="notes", default=DEFAULT_NOTES) notes = models.TextField(blank=True, verbose_name="notes", default=DEFAULT_NOTES)
gm_notes = models.TextField(blank=True, verbose_name="notes MJ")
damage_reduction = models.TextField(blank=True, verbose_name="réduction de dégâts") damage_reduction = models.TextField(blank=True, verbose_name="réduction de dégâts")
states = models.ManyToManyField(HarmfulState, blank=True, related_name="characters") states = models.ManyToManyField(HarmfulState, blank=True, related_name="characters")
@ -402,6 +422,10 @@ class Character(models.Model):
md = markdown.Markdown(extensions=["extra", "nl2br"]) md = markdown.Markdown(extensions=["extra", "nl2br"])
return md.convert(self.notes) return md.convert(self.notes)
def get_formatted_gm_notes(self) -> str:
md = markdown.Markdown(extensions=["extra", "nl2br"])
return md.convert(self.gm_notes)
def get_missing_states(self) -> Iterable[HarmfulState]: def get_missing_states(self) -> Iterable[HarmfulState]:
return HarmfulState.objects.exclude( return HarmfulState.objects.exclude(
pk__in=self.states.all().values_list("pk", flat=True) pk__in=self.states.all().values_list("pk", flat=True)
@ -410,6 +434,9 @@ class Character(models.Model):
def managed_by(self, user): def managed_by(self, user):
return self in Character.objects.managed_by(user) return self in Character.objects.managed_by(user)
def mastered_by(self, user):
return self in Character.objects.mastered_by(user)
def reset_stats(self): def reset_stats(self):
self.health_remaining = self.health_max self.health_remaining = self.health_max
self.mana_remaining = self.mana_max self.mana_remaining = self.mana_max

View file

@ -510,6 +510,9 @@
</div> </div>
{% include "character/snippets/character_details/paths_and_capabilities.html" %} {% include "character/snippets/character_details/paths_and_capabilities.html" %}
{% if character|mastered_by:user %}
{% include "character/snippets/character_details/gm_notes_display.html" %}
{% endif %}
{% if character|managed_by:user %} {% if character|managed_by:user %}
{% include "character/snippets/character_details/notes_display.html" %} {% include "character/snippets/character_details/notes_display.html" %}
{% endif %} {% endif %}

View file

@ -0,0 +1,15 @@
{% load character_extras %}
<div class="mt-3" id="gm-notes">
<h2>
Notes MJ
<a hx-get="{% url "character:gm_notes_change" pk=character.pk %}"
hx-target="#gm-notes"
hx-swap="outerHTML"
class="btn btn-primary btn-sm"
>
<i class="fa-solid fa-pen-to-square"></i> Edit
</a>
</h2>
<div class="alert alert-info">Le joueur ne peut pas voir ces notes.</div>
{{ character.get_formatted_gm_notes|safe }}
</div>

View file

@ -0,0 +1,17 @@
<div class="mt-3" id="gm-notes">
<form>
<h2>
Notes MJ
<a hx-post="{% url "character:gm_notes_change" pk=character.pk %}"
hx-target="#gm-notes"
hx-swap="innerHTML"
class="btn btn-primary btn-sm"
>
<i class="fa-solid fa-save"></i> Save
</a>
</h2>
<div class="alert alert-info">Ces notes ne sont pas visibles par le joueur.</div>
{% csrf_token %}
<textarea class="form-control" name="gm_notes" rows="10">{{ character.gm_notes }}</textarea>
</form>
</div>

View file

@ -10,5 +10,9 @@
<i class="fa-solid fa-pen-to-square"></i> Edit <i class="fa-solid fa-pen-to-square"></i> Edit
</a> </a>
</h2> </h2>
<div class="alert alert-info">
Le {% if character|mastered_by:user %}joueur{% else %}MJ{% endif %}
peut également voir et modifier ces notes.
</div>
{{ character.get_formatted_notes|safe }} {{ character.get_formatted_notes|safe }}
</div> </div>

View file

@ -10,6 +10,10 @@
<i class="fa-solid fa-save"></i> Save <i class="fa-solid fa-save"></i> Save
</a> </a>
</h2> </h2>
<div class="alert alert-info">
Ces notes ne sont visibles que par le joueur et les MJ
des groupes auquel ce personnage appartient.
</div>
{% csrf_token %} {% csrf_token %}
<textarea class="form-control" name="notes" rows="25">{{ character.notes }}</textarea> <textarea class="form-control" name="notes" rows="25">{{ character.notes }}</textarea>
</form> </form>

View file

@ -43,3 +43,8 @@ def max_rank(path: Path, character: Character) -> int:
@register.filter @register.filter
def managed_by(character: Character, user: User) -> bool: def managed_by(character: Character, user: User) -> bool:
return character.managed_by(user) return character.managed_by(user)
@register.filter
def mastered_by(character: Character, user: User) -> bool:
return character.mastered_by(user)

View file

@ -11,13 +11,15 @@ def test_can_access_own_character(client):
player = User.objects.create_user("username", password="password") player = User.objects.create_user("username", password="password")
notes = "Some notes" notes = "Some notes"
character = baker.make(Character, player=player, notes=notes) gm_notes = "Some GM notes"
character = baker.make(Character, player=player, notes=notes, gm_notes=gm_notes)
client.force_login(player) client.force_login(player)
res = client.get(character.get_absolute_url()) res = client.get(character.get_absolute_url())
assert res.status_code == 200 assert res.status_code == 200
body = res.content.decode("utf-8") body = res.content.decode("utf-8")
assert notes in body assert notes in body
assert gm_notes not in body
@pytest.mark.django_db @pytest.mark.django_db
@ -38,7 +40,10 @@ def test_can_access_character_in_party(client):
character = baker.make(Character, player=player) character = baker.make(Character, player=player)
notes = "Some notes" notes = "Some notes"
friend_character = baker.make(Character, player=friend, notes=notes) gm_notes = "Some GM notes"
friend_character = baker.make(
Character, player=friend, notes=notes, gm_notes=gm_notes
)
party = baker.make(Party) party = baker.make(Party)
party.characters.add(character) party.characters.add(character)
party.characters.add(friend_character) party.characters.add(friend_character)
@ -48,6 +53,7 @@ def test_can_access_character_in_party(client):
body = res.content.decode("utf-8") body = res.content.decode("utf-8")
assert notes not in body assert notes not in body
assert gm_notes not in body
@pytest.mark.django_db @pytest.mark.django_db
@ -56,7 +62,8 @@ def test_game_master_can_access_character_in_party(client):
gm = User.objects.create_user("gm", password="password") gm = User.objects.create_user("gm", password="password")
notes = "Some notes" notes = "Some notes"
character = baker.make(Character, player=player, notes=notes) gm_notes = "Some GM notes"
character = baker.make(Character, player=player, notes=notes, gm_notes=gm_notes)
party = baker.make(Party, game_master=gm) party = baker.make(Party, game_master=gm)
party.characters.add(character) party.characters.add(character)
client.force_login(gm) client.force_login(gm)
@ -65,3 +72,4 @@ def test_game_master_can_access_character_in_party(client):
body = res.content.decode("utf-8") body = res.content.decode("utf-8")
assert notes in body assert notes in body
assert gm_notes in body

View file

@ -24,6 +24,11 @@ urlpatterns = [
name="luck_points_change", name="luck_points_change",
), ),
path("<int:pk>/notes_change/", views.character_notes_change, name="notes_change"), path("<int:pk>/notes_change/", views.character_notes_change, name="notes_change"),
path(
"<int:pk>/gm_notes_change/",
views.character_gm_notes_change,
name="gm_notes_change",
),
path("<int:pk>/get_defense/", views.character_get_defense, name="get_defense"), path("<int:pk>/get_defense/", views.character_get_defense, name="get_defense"),
path( path(
"<int:pk>/defense_misc_change/", "<int:pk>/defense_misc_change/",

View file

@ -290,6 +290,11 @@ def character_notes_change(request, pk: int):
return update_text_field(request, pk, "notes") return update_text_field(request, pk, "notes")
@login_required
def character_gm_notes_change(request, pk: int):
return update_text_field(request, pk, "gm_notes")
@login_required @login_required
def character_equipment_change(request, pk: int): def character_equipment_change(request, pk: int):
field = "equipment" field = "equipment"