From 9c5eadebb7d49529103a1e798c282787d6128001 Mon Sep 17 00:00:00 2001 From: Cybernetic-Ransomware <71835339+Cybernetic-Ransomware@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:27:53 +0200 Subject: [PATCH] fix(ui): clean up small frontend correctness issues --- .../medical_notes/forms/type_basic_note.py | 16 +-- .../medical_notes/notification_list.html | 4 +- src/ahc/apps/medical_notes/tests.py | 117 ++++++++++++++++++ .../medical_notes/views/type_basic_note.py | 10 +- 4 files changed, 131 insertions(+), 16 deletions(-) diff --git a/src/ahc/apps/medical_notes/forms/type_basic_note.py b/src/ahc/apps/medical_notes/forms/type_basic_note.py index b4680a2..a267880 100644 --- a/src/ahc/apps/medical_notes/forms/type_basic_note.py +++ b/src/ahc/apps/medical_notes/forms/type_basic_note.py @@ -56,9 +56,6 @@ def __init__(self, *args, **kwargs): type_of_event_param = kwargs.pop("type_of_event_param", None) super().__init__(*args, **kwargs) - if animal_choices: - self.fields["additional_animals"].widget.choices = animal_choices - # Restrict the validation queryset so a posted UUID of a deceased or inaccessible # animal fails form validation, not just widget display. if profile is not None: @@ -67,6 +64,10 @@ def __init__(self, *args, **kwargs): qs = qs.exclude(id=exclude_id) self.fields["additional_animals"].queryset = qs + # Must come after queryset= above: its setter resets widget.choices to the default str(obj) iterator. + if animal_choices: + self.fields["additional_animals"].widget.choices = animal_choices + if type_of_event_param in set(event[0] for event in self.TYPES_OF_EVENTS): self.fields["type_of_event"].initial = type_of_event_param else: @@ -118,10 +119,6 @@ def __init__(self, *args, **kwargs): profile = kwargs.pop("profile", None) super().__init__(*args, **kwargs) - if animal_choices: - self.fields["animal"].widget.choices = animal_choices - self.fields["additional_animals"].widget.choices = animal_choices - # Restrict validation querysets so a posted UUID of a deceased or inaccessible # animal fails form validation, not just widget display. if profile is not None: @@ -129,6 +126,11 @@ def __init__(self, *args, **kwargs): self.fields["animal"].queryset = qs self.fields["additional_animals"].queryset = qs + # Must come after queryset= above: its setter resets widget.choices to the default str(obj) iterator. + if animal_choices: + self.fields["animal"].widget.choices = animal_choices + self.fields["additional_animals"].widget.choices = animal_choices + if not is_author: del self.fields["animal"] diff --git a/src/ahc/apps/medical_notes/templates/medical_notes/notification_list.html b/src/ahc/apps/medical_notes/templates/medical_notes/notification_list.html index 47031cd..139320d 100644 --- a/src/ahc/apps/medical_notes/templates/medical_notes/notification_list.html +++ b/src/ahc/apps/medical_notes/templates/medical_notes/notification_list.html @@ -65,13 +65,15 @@
No notifications set for this record yet.
{% endfor %} {% endblock %} diff --git a/src/ahc/apps/medical_notes/tests.py b/src/ahc/apps/medical_notes/tests.py index 7417627..8e78324 100644 --- a/src/ahc/apps/medical_notes/tests.py +++ b/src/ahc/apps/medical_notes/tests.py @@ -1324,3 +1324,120 @@ def test_carer_without_biometrics_can_create_other_note_types(self, client, seco response = client.get(f"/note/{animal.id}/create/?type_of_event=fast_note") assert response.status_code == 200 + + +@pytest.mark.integration +@pytest.mark.django_db +class TestRelatedAnimalsLabels: + """Regression for D-02: animal choice widgets must show full_name, not the model repr.""" + + @pytest.fixture + def two_owned_animals_for_notes(self, db, user_profile): + from ahc.apps.animals.models import Animal + + _, profile = user_profile + primary = Animal.objects.create(full_name="Maru", owner=profile) + other = Animal.objects.create(full_name="Chilli", owner=profile) + return primary, other, profile + + def test_create_note_form_shows_animal_full_name(self, client, user_profile, two_owned_animals_for_notes): + user, _ = user_profile + primary, other, _ = two_owned_animals_for_notes + client.force_login(user) + + response = client.get(f"/note/{primary.id}/create/", HTTP_HX_REQUEST="true") + + assert response.status_code == 200 + assert other.full_name.encode() in response.content + assert b"Animal object" not in response.content + + def test_edit_related_animals_shows_animal_full_name(self, client, user_profile, two_owned_animals_for_notes): + from ahc.apps.medical_notes.models.type_basic_note import MedicalRecord + + user, profile = user_profile + primary, other, _ = two_owned_animals_for_notes + note = MedicalRecord.objects.create( + animal=primary, author=profile, type_of_event="fast_note", short_description="x" + ) + client.force_login(user) + + response = client.get(reverse("note_animals_edit", kwargs={"pk": note.id})) + + assert response.status_code == 200 + assert primary.full_name.encode() in response.content + assert other.full_name.encode() in response.content + assert b"Animal object" not in response.content + + +@pytest.mark.integration +@pytest.mark.django_db +class TestNotificationListEmptyState: + """Regression for D-03: notification_list.html must show an empty state, not a blank page.""" + + def test_shows_empty_state_when_no_notifications(self, client, user_profile): + import uuid + + user, _ = user_profile + client.force_login(user) + + response = client.get(reverse("note_related_notifications"), {"mednote_uuid": str(uuid.uuid4())}) + + assert response.status_code == 200 + assert b"No notifications set for this record yet." in response.content + + def test_shows_notifications_when_present(self, client, user_profile, diet_note_shell): + """Stubs the queryset — EmailNotification.days_of_week (ArrayField) can't be written via SQLite.""" + user, _ = user_profile + fake_notification = SimpleNamespace( + pk=1, + description="Feed reminder", + is_active=True, + daily_timestamp=None, + timezone="Europe/London", + start_date=_date(2026, 1, 1), + end_date=None, + days_of_week=[False] * 7, + receiver_name="Owner", + message="Time to feed", + last_modification=None, + ) + client.force_login(user) + + with patch( + "ahc.apps.medical_notes.views.type_feeding_notes.notifications_for_mednote", + return_value=[fake_notification], + ): + response = client.get(reverse("note_related_notifications"), {"mednote_uuid": str(diet_note_shell.id)}) + + assert response.status_code == 200 + assert b"No notifications set for this record yet." not in response.content + assert b"Feed reminder" in response.content + + +@pytest.mark.integration +@pytest.mark.django_db +class TestCreateNoteFormViewLegend: + """Regression for F-01: the modal fieldset legend must not repeat the dialog title.""" + + @pytest.fixture + def owned_animal(self, db, user_profile): + from ahc.apps.animals.models import Animal + + _, profile = user_profile + return Animal.objects.create(full_name="Maru", owner=profile) + + @pytest.mark.parametrize( + "type_of_event", + ["", "medical_visit", "diet_note", "medicament_note", "fast_note"], + ) + def test_legend_is_generic_section_label(self, client, user_profile, owned_animal, type_of_event): + user, _ = user_profile + client.force_login(user) + + url = f"/note/{owned_animal.id}/create/" + if type_of_event: + url += f"?type_of_event={type_of_event}" + response = client.get(url, HTTP_HX_REQUEST="true") + + assert response.status_code == 200 + assert response.context["legend"] == "Note details" diff --git a/src/ahc/apps/medical_notes/views/type_basic_note.py b/src/ahc/apps/medical_notes/views/type_basic_note.py index 0f73cdb..36c196a 100644 --- a/src/ahc/apps/medical_notes/views/type_basic_note.py +++ b/src/ahc/apps/medical_notes/views/type_basic_note.py @@ -81,14 +81,8 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["form_name"] = str(self.form_class.__name__) context["form_action"] = self.request.get_full_path() - legend_map = { - "medical_visit": "Add vet visit", - "diet_note": "Diet note", - "biometric_record": "Biometric record", - "medicament_note": "Medicament note", - "fast_note": "Quick note", - } - context["legend"] = legend_map.get(self.request.GET.get("type_of_event", ""), "New note") + # The trigger's data-modal-title already gives the type-specific context, so this stays generic. + context["legend"] = "Note details" return context def form_valid(self, form):