From 41585dadcc11871faa2c73936f9f17112251b16e Mon Sep 17 00:00:00 2001 From: Simon Oliver Tveit Date: Wed, 30 Oct 2024 14:11:21 +0100 Subject: [PATCH 01/11] Add view for destinations page --- .../{destinations => destination}/__init__.py | 0 src/argus/htmx/destination/urls.py | 8 +++++++ src/argus/htmx/destination/views.py | 13 +++++++++++ src/argus/htmx/destinations/urls.py | 23 ------------------- .../htmx/destination/destination_list.html | 1 + src/argus/htmx/urls.py | 2 +- 6 files changed, 23 insertions(+), 24 deletions(-) rename src/argus/htmx/{destinations => destination}/__init__.py (100%) create mode 100644 src/argus/htmx/destination/urls.py create mode 100644 src/argus/htmx/destination/views.py delete mode 100644 src/argus/htmx/destinations/urls.py create mode 100644 src/argus/htmx/templates/htmx/destination/destination_list.html diff --git a/src/argus/htmx/destinations/__init__.py b/src/argus/htmx/destination/__init__.py similarity index 100% rename from src/argus/htmx/destinations/__init__.py rename to src/argus/htmx/destination/__init__.py diff --git a/src/argus/htmx/destination/urls.py b/src/argus/htmx/destination/urls.py new file mode 100644 index 000000000..0b9520a98 --- /dev/null +++ b/src/argus/htmx/destination/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from .views import destination_list + +app_name = "htmx" +urlpatterns = [ + path("", destination_list, name="destination-list"), +] diff --git a/src/argus/htmx/destination/views.py b/src/argus/htmx/destination/views.py new file mode 100644 index 000000000..94875b720 --- /dev/null +++ b/src/argus/htmx/destination/views.py @@ -0,0 +1,13 @@ +from django.shortcuts import render + +from django.views.decorators.http import require_http_methods +from django.http import HttpResponse + + +@require_http_methods(["GET"]) +def destination_list(request) -> HttpResponse: + return _render_destination_list(request) + + +def _render_destination_list(request) -> HttpResponse: + return render(request, "htmx/destination/destination_list.html") diff --git a/src/argus/htmx/destinations/urls.py b/src/argus/htmx/destinations/urls.py deleted file mode 100644 index a84ddf0ef..000000000 --- a/src/argus/htmx/destinations/urls.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.http import HttpResponse -from django.template import Template, RequestContext -from django.urls import path -from django.views.decorators.http import require_GET - - -@require_GET -def placeholder(request): - template = Template( - """{% extends "htmx/base.html" %} - {% block main %} -

DESTINATION PLACEHOLDER

- {% endblock main %} - """ - ) - context = RequestContext(request) - return HttpResponse(template.render(context)) - - -app_name = "htmx" -urlpatterns = [ - path("", placeholder, name="destination-placeholder"), -] diff --git a/src/argus/htmx/templates/htmx/destination/destination_list.html b/src/argus/htmx/templates/htmx/destination/destination_list.html new file mode 100644 index 000000000..ac86942d4 --- /dev/null +++ b/src/argus/htmx/templates/htmx/destination/destination_list.html @@ -0,0 +1 @@ +{% extends "htmx/base.html" %} diff --git a/src/argus/htmx/urls.py b/src/argus/htmx/urls.py index 3b0e3715a..b01ab0d4b 100644 --- a/src/argus/htmx/urls.py +++ b/src/argus/htmx/urls.py @@ -5,7 +5,7 @@ from .incidents.urls import urlpatterns as incident_urls from .timeslots.urls import urlpatterns as timeslot_urls from .notificationprofiles.urls import urlpatterns as notificationprofile_urls -from .destinations.urls import urlpatterns as destination_urls +from .destination.urls import urlpatterns as destination_urls from .themes.urls import urlpatterns as theme_urls from .dateformat.urls import urlpatterns as dateformat_urls from .user.urls import urlpatterns as user_urls From b6e531f185b6979590684c4d76ff15921db6690b Mon Sep 17 00:00:00 2001 From: Simon Oliver Tveit Date: Wed, 30 Oct 2024 16:53:17 +0100 Subject: [PATCH 02/11] Add destinations link to navbar dropdown --- src/argus/htmx/templates/htmx/base.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/argus/htmx/templates/htmx/base.html b/src/argus/htmx/templates/htmx/base.html index 3b366edb3..e93af2b02 100644 --- a/src/argus/htmx/templates/htmx/base.html +++ b/src/argus/htmx/templates/htmx/base.html @@ -48,6 +48,9 @@
  • Preferences…
  • +
  • + Destinations +
  • {% csrf_token %} From 79f18286c8b741cf42e77b66ce7dca3dffe69646 Mon Sep 17 00:00:00 2001 From: Simon Oliver Tveit Date: Wed, 30 Oct 2024 16:02:23 +0100 Subject: [PATCH 03/11] Add form for creating a destination --- src/argus/htmx/destination/forms.py | 81 +++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/argus/htmx/destination/forms.py diff --git a/src/argus/htmx/destination/forms.py b/src/argus/htmx/destination/forms.py new file mode 100644 index 000000000..eb07e5c33 --- /dev/null +++ b/src/argus/htmx/destination/forms.py @@ -0,0 +1,81 @@ +from django import forms +from django.forms import ModelForm + +from argus.notificationprofile.models import DestinationConfig, Media +from argus.notificationprofile.serializers import RequestDestinationConfigSerializer +from argus.notificationprofile.media import api_safely_get_medium_object + + +class DestinationFormCreate(ModelForm): + settings = forms.CharField(required=True) + + def __init__(self, *args, **kwargs): + # Serializer request the request object + self.request = kwargs.pop("request", None) + super().__init__(*args, **kwargs) + + class Meta: + model = DestinationConfig + fields = ["label", "media", "settings"] + labels = { + "label": "Name", + } + + def clean(self): + super().clean() + settings_key = _get_settings_key_for_media(self.cleaned_data["media"]) + # Convert settings value (e.g. email address) to be compatible with JSONField + self.cleaned_data["settings"] = {settings_key: self.cleaned_data["settings"]} + self._init_serializer() + return self._validate_serializer() + + def save(self): + # self.serializer should be initiated and validated in clean() before save() is called + self.serializer.save(user=self.request.user) + + def _init_serializer(self): + serializer = RequestDestinationConfigSerializer( + data={ + "media": self.cleaned_data["media"], + "label": self.cleaned_data.get("label", ""), + "settings": self.cleaned_data["settings"], + }, + context={"request": self.request}, + ) + self.serializer = serializer + + def _validate_serializer(self): + media = self.cleaned_data["media"] + settings_key = _get_settings_key_for_media(media) + + # Add error messages from serializer to form + if not self.serializer.is_valid(): + for error_name, error_detail in self.serializer.errors.items(): + if error_name in ["media", "label", settings_key]: + if error_name == settings_key: + error_name = "settings" + self.add_error(error_name, error_detail) + # Serializer might add more data to the JSON dict + if settings := self.serializer.data.get("settings"): + self.cleaned_data["settings"] = settings + else: + # Serializer might add more data to the JSON dict + if settings := self.serializer.validated_data.get("settings"): + self.cleaned_data["settings"] = settings + + if label := self.cleaned_data.get("label"): + destination_filter = DestinationConfig.objects.filter(label=label) + if self.instance: + destination_filter = destination_filter.exclude(pk=self.instance.pk) + if destination_filter.exists(): + self.add_error("label", "Name must be unique per media") + + return self.cleaned_data + + +def _get_settings_key_for_media(media: Media) -> str: + """Returns the required settings key for the given media, + e.g. "email_address", "phone_number" + """ + medium = api_safely_get_medium_object(media.slug) + return medium.MEDIA_JSON_SCHEMA["required"][0] From ce46d6b194899344d230aa167e7e02fcff663699 Mon Sep 17 00:00:00 2001 From: Simon Oliver Tveit Date: Wed, 30 Oct 2024 16:08:10 +0100 Subject: [PATCH 04/11] Add view for creating a destination --- src/argus/htmx/destination/forms.py | 3 ++ src/argus/htmx/destination/views.py | 33 ++++++++++++++++--- .../htmx/destination/_create_form.html | 26 +++++++++++++++ .../htmx/destination/destination_list.html | 3 ++ 4 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 src/argus/htmx/templates/htmx/destination/_create_form.html diff --git a/src/argus/htmx/destination/forms.py b/src/argus/htmx/destination/forms.py index eb07e5c33..6ae2cc7d6 100644 --- a/src/argus/htmx/destination/forms.py +++ b/src/argus/htmx/destination/forms.py @@ -20,6 +20,9 @@ class Meta: labels = { "label": "Name", } + widgets = { + "media": forms.Select(attrs={"class": "select input-bordered w-full max-w-xs"}), + } def clean(self): super().clean() diff --git a/src/argus/htmx/destination/views.py b/src/argus/htmx/destination/views.py index 94875b720..5962d71dc 100644 --- a/src/argus/htmx/destination/views.py +++ b/src/argus/htmx/destination/views.py @@ -1,13 +1,36 @@ +from typing import Optional from django.shortcuts import render from django.views.decorators.http import require_http_methods from django.http import HttpResponse +from .forms import DestinationFormCreate -@require_http_methods(["GET"]) -def destination_list(request) -> HttpResponse: - return _render_destination_list(request) +@require_http_methods(["GET", "POST"]) +def destination_list(request): + if request.method == "GET": + return _render_destination_list(request) + elif request.method == "POST": + return destination_create(request) -def _render_destination_list(request) -> HttpResponse: - return render(request, "htmx/destination/destination_list.html") + +def destination_create(request) -> HttpResponse: + form = DestinationFormCreate(request.POST or None, request=request) + if form.is_valid(): + form.save() + return _render_destination_list(request) + return _render_destination_list(request, create_form=form) + + +def _render_destination_list(request, create_form: Optional[DestinationFormCreate] = None) -> HttpResponse: + """Function to render the destinations page. + + :param create_form: this is used to display the form for creating a new destination + with errors while retaining the user input. If you want a blank form, pass None.""" + if create_form is None: + create_form = DestinationFormCreate() + context = { + "create_form": create_form, + } + return render(request, "htmx/destination/destination_list.html", context=context) diff --git a/src/argus/htmx/templates/htmx/destination/_create_form.html b/src/argus/htmx/templates/htmx/destination/_create_form.html new file mode 100644 index 000000000..d90e08da4 --- /dev/null +++ b/src/argus/htmx/templates/htmx/destination/_create_form.html @@ -0,0 +1,26 @@ + + {% csrf_token %} +
    + Create destination + {% for field in create_form %} + + {% empty %} +

    Something went wrong

    + {% endfor %} + +
    + diff --git a/src/argus/htmx/templates/htmx/destination/destination_list.html b/src/argus/htmx/templates/htmx/destination/destination_list.html index ac86942d4..8dcf97a3f 100644 --- a/src/argus/htmx/templates/htmx/destination/destination_list.html +++ b/src/argus/htmx/templates/htmx/destination/destination_list.html @@ -1 +1,4 @@ {% extends "htmx/base.html" %} +{% block main %} + {% include "htmx/destination/_create_form.html" %} +{% endblock main %} From cb5cc940c96d4d2d1a8f86f4eabc421c96e43d04 Mon Sep 17 00:00:00 2001 From: Simon Oliver Tveit Date: Wed, 30 Oct 2024 16:13:57 +0100 Subject: [PATCH 05/11] Add view for deleting a destination Pretty much copied from the destinations API, needs some changes so it wont just crash to 500 when it raises a ValidationError This commit doesnt make it show in the frontend, the button for deleting a destination will be grouped with the forms for updating destinations --- src/argus/htmx/destination/urls.py | 3 +- src/argus/htmx/destination/views.py | 34 ++++++++++++++++--- .../htmx/destination/destination_list.html | 1 + 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/argus/htmx/destination/urls.py b/src/argus/htmx/destination/urls.py index 0b9520a98..c81649b56 100644 --- a/src/argus/htmx/destination/urls.py +++ b/src/argus/htmx/destination/urls.py @@ -1,8 +1,9 @@ from django.urls import path -from .views import destination_list +from .views import destination_list, destination_delete app_name = "htmx" urlpatterns = [ path("", destination_list, name="destination-list"), + path("/delete/", destination_delete, name="destination-delete"), ] diff --git a/src/argus/htmx/destination/views.py b/src/argus/htmx/destination/views.py index 5962d71dc..0743d0e2f 100644 --- a/src/argus/htmx/destination/views.py +++ b/src/argus/htmx/destination/views.py @@ -1,9 +1,12 @@ -from typing import Optional -from django.shortcuts import render +from typing import Optional, Sequence +from django.shortcuts import render, get_object_or_404 from django.views.decorators.http import require_http_methods from django.http import HttpResponse +from argus.notificationprofile.media import api_safely_get_medium_object +from argus.notificationprofile.media.base import NotificationMedium + from .forms import DestinationFormCreate @@ -23,14 +26,37 @@ def destination_create(request) -> HttpResponse: return _render_destination_list(request, create_form=form) -def _render_destination_list(request, create_form: Optional[DestinationFormCreate] = None) -> HttpResponse: +@require_http_methods(["POST"]) +def destination_delete(request, pk: int) -> HttpResponse: + destination = get_object_or_404(request.user.destinations.all(), pk=pk) + + try: + medium = api_safely_get_medium_object(destination.media.slug) + medium.raise_if_not_deletable(destination) + except NotificationMedium.NotDeletableError: + error_msg = "This destination cannot be deleted." + return _render_destination_list(request, errors=[error_msg]) + else: + destination.delete() + return _render_destination_list(request) + + +def _render_destination_list( + request, + create_form: Optional[DestinationFormCreate] = None, + errors: Optional[Sequence[str]] = None, +) -> HttpResponse: """Function to render the destinations page. :param create_form: this is used to display the form for creating a new destination - with errors while retaining the user input. If you want a blank form, pass None.""" + with errors while retaining the user input. If you want a blank form, pass None. + :param errors: a list of error messages to display on the page.""" if create_form is None: create_form = DestinationFormCreate() + if errors is None: + errors = [] context = { "create_form": create_form, + errors: errors, } return render(request, "htmx/destination/destination_list.html", context=context) diff --git a/src/argus/htmx/templates/htmx/destination/destination_list.html b/src/argus/htmx/templates/htmx/destination/destination_list.html index 8dcf97a3f..92a317d2f 100644 --- a/src/argus/htmx/templates/htmx/destination/destination_list.html +++ b/src/argus/htmx/templates/htmx/destination/destination_list.html @@ -1,4 +1,5 @@ {% extends "htmx/base.html" %} {% block main %} {% include "htmx/destination/_create_form.html" %} + {% for error in errors %}

    {{ error }}

    {% endfor %} {% endblock main %} From aa7c9e944385c08382cfaf5706592f5f1ff4a465 Mon Sep 17 00:00:00 2001 From: Simon Oliver Tveit Date: Wed, 30 Oct 2024 16:28:26 +0100 Subject: [PATCH 06/11] Add form for updating a destination --- src/argus/htmx/destination/forms.py | 42 +++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/argus/htmx/destination/forms.py b/src/argus/htmx/destination/forms.py index 6ae2cc7d6..9b8a63010 100644 --- a/src/argus/htmx/destination/forms.py +++ b/src/argus/htmx/destination/forms.py @@ -76,6 +76,48 @@ def _validate_serializer(self): return self.cleaned_data +class DestinationFormUpdate(DestinationFormCreate): + def __init__(self, *args, **kwargs): + if instance := kwargs.get("instance"): + settings_key = _get_settings_key_for_media(instance.media) + # Extract settings value (email address etc.) from JSONField + instance.settings = instance.settings.get(settings_key) + super().__init__(*args, **kwargs) + + class Meta: + model = DestinationConfig + fields = ["label", "media", "settings"] + labels = { + "label": "Name", + } + widgets = { + "media": forms.HiddenInput(), + } + + def _init_serializer(self): + # self.instance is modified in __init__, + # so get unmodified version here for the serializer + destination = DestinationConfig.objects.get(pk=self.instance.pk) + settings_key = _get_settings_key_for_media(destination.media) + data = {} + + if "label" in self.cleaned_data: + label = self.cleaned_data["label"] + if label != destination.label: + data["label"] = label + + settings = self.cleaned_data["settings"] + if settings.get(settings_key) != destination.settings.get(settings_key): + data["settings"] = settings + + self.serializer = RequestDestinationConfigSerializer( + destination, + data=data, + context={"request": self.request}, + partial=True, + ) + + def _get_settings_key_for_media(media: Media) -> str: """Returns the required settings key for the given media, e.g. "email_address", "phone_number" From 3e224406b13a0b1c6b2d8a74bf61240997b65ce3 Mon Sep 17 00:00:00 2001 From: Simon Oliver Tveit Date: Wed, 30 Oct 2024 16:32:50 +0100 Subject: [PATCH 07/11] Add subtemplate for rendering update/delete form --- .../htmx/destination/_delete_form.html | 7 ++++++ .../htmx/destination/_edit_form.html | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 src/argus/htmx/templates/htmx/destination/_delete_form.html create mode 100644 src/argus/htmx/templates/htmx/destination/_edit_form.html diff --git a/src/argus/htmx/templates/htmx/destination/_delete_form.html b/src/argus/htmx/templates/htmx/destination/_delete_form.html new file mode 100644 index 000000000..7c0d017c0 --- /dev/null +++ b/src/argus/htmx/templates/htmx/destination/_delete_form.html @@ -0,0 +1,7 @@ +
    + {% csrf_token %} + +
    diff --git a/src/argus/htmx/templates/htmx/destination/_edit_form.html b/src/argus/htmx/templates/htmx/destination/_edit_form.html new file mode 100644 index 000000000..982add570 --- /dev/null +++ b/src/argus/htmx/templates/htmx/destination/_edit_form.html @@ -0,0 +1,24 @@ +
    + {% csrf_token %} +
    + {% for hidden_field in form.hidden_fields %}{{ hidden_field }}{% endfor %} + {% for field in form.visible_fields %} + + {% empty %} +

    Something went wrong

    + {% endfor %} +
    + +
    From 270af6dce0ba6f234bd9bd7a3de040a19575438d Mon Sep 17 00:00:00 2001 From: Simon Oliver Tveit Date: Wed, 30 Oct 2024 16:30:12 +0100 Subject: [PATCH 08/11] Add view for updating a destination --- src/argus/htmx/destination/urls.py | 3 +- src/argus/htmx/destination/views.py | 55 ++++++++++++++++++- .../htmx/destination/destination_list.html | 13 +++++ 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/src/argus/htmx/destination/urls.py b/src/argus/htmx/destination/urls.py index c81649b56..a3f493a7e 100644 --- a/src/argus/htmx/destination/urls.py +++ b/src/argus/htmx/destination/urls.py @@ -1,9 +1,10 @@ from django.urls import path -from .views import destination_list, destination_delete +from .views import destination_list, destination_delete, destination_update app_name = "htmx" urlpatterns = [ path("", destination_list, name="destination-list"), path("/delete/", destination_delete, name="destination-delete"), + path("/", destination_update, name="destination-update"), ] diff --git a/src/argus/htmx/destination/views.py b/src/argus/htmx/destination/views.py index 0743d0e2f..3e00567b2 100644 --- a/src/argus/htmx/destination/views.py +++ b/src/argus/htmx/destination/views.py @@ -4,10 +4,11 @@ from django.views.decorators.http import require_http_methods from django.http import HttpResponse +from argus.notificationprofile.models import DestinationConfig, Media from argus.notificationprofile.media import api_safely_get_medium_object from argus.notificationprofile.media.base import NotificationMedium -from .forms import DestinationFormCreate +from .forms import DestinationFormCreate, DestinationFormUpdate @require_http_methods(["GET", "POST"]) @@ -41,22 +42,70 @@ def destination_delete(request, pk: int) -> HttpResponse: return _render_destination_list(request) +@require_http_methods(["POST"]) +def destination_update(request, pk: int) -> HttpResponse: + destination = DestinationConfig.objects.get(pk=pk) + form = DestinationFormUpdate(request.POST or None, instance=destination, request=request) + if form.is_valid(): + form.save() + return _render_destination_list(request) + + update_forms = _get_update_forms(request.user) + for index, update_form in enumerate(update_forms): + if update_form.instance.pk == pk: + update_forms[index] = form + break + return _render_destination_list(request, update_forms=update_forms) + + def _render_destination_list( request, create_form: Optional[DestinationFormCreate] = None, + update_forms: Optional[Sequence[DestinationFormUpdate]] = None, errors: Optional[Sequence[str]] = None, ) -> HttpResponse: """Function to render the destinations page. :param create_form: this is used to display the form for creating a new destination with errors while retaining the user input. If you want a blank form, pass None. - :param errors: a list of error messages to display on the page.""" + :param update_forms: list of update forms to display. Useful for rendering forms + with error messages while retaining the user input. + If this is None, the update forms will be generated from the user's destinations. + :param errors: a list of error messages to display on the page. Will not be tied to + any form fields.""" + if create_form is None: create_form = DestinationFormCreate() + if update_forms is None: + update_forms = _get_update_forms(request.user) if errors is None: errors = [] + grouped_forms = _group_update_forms_by_media(update_forms) context = { "create_form": create_form, - errors: errors, + "grouped_forms": grouped_forms, + "errors": errors, } return render(request, "htmx/destination/destination_list.html", context=context) + + +def _get_update_forms(user) -> list[DestinationFormUpdate]: + # Sort by oldest first + destinations = user.destinations.all().order_by("pk") + return [DestinationFormUpdate(instance=destination) for destination in destinations] + + +def _group_update_forms_by_media( + destination_forms: Sequence[DestinationFormUpdate], +) -> dict[Media, list[DestinationFormUpdate]]: + grouped_destinations = {} + + # Adding a media to the dict even if there are no destinations for it + # is useful so that the template can render a section for that media + for media in Media.objects.all(): + grouped_destinations[media] = [] + + for form in destination_forms: + grouped_destinations[form.instance.media].append(form) + + return grouped_destinations diff --git a/src/argus/htmx/templates/htmx/destination/destination_list.html b/src/argus/htmx/templates/htmx/destination/destination_list.html index 92a317d2f..9b2b78358 100644 --- a/src/argus/htmx/templates/htmx/destination/destination_list.html +++ b/src/argus/htmx/templates/htmx/destination/destination_list.html @@ -2,4 +2,17 @@ {% block main %} {% include "htmx/destination/_create_form.html" %} {% for error in errors %}

    {{ error }}

    {% endfor %} + {% for media, forms in grouped_forms.items %} +
    + {{ media.name }} ({{ forms|length }}) +
    + {% for form in forms %} +
    + {% include "htmx/destination/_edit_form.html" %} + {% include "htmx/destination/_delete_form.html" %} +
    + {% endfor %} +
    +
    + {% endfor %} {% endblock main %} From eef54d117202c3725b4661b79a9e6747a8ffdb68 Mon Sep 17 00:00:00 2001 From: Simon Oliver Tveit Date: Wed, 4 Dec 2024 14:07:24 +0100 Subject: [PATCH 09/11] Use htmx for POST endpoints Only update the elements that needs to be updated. --- src/argus/htmx/destination/urls.py | 7 ++-- src/argus/htmx/destination/views.py | 33 ++++++++++--------- .../templates/htmx/destination/_content.html | 5 +++ .../htmx/destination/_create_form.html | 4 ++- .../htmx/destination/_delete_form.html | 5 +-- .../htmx/destination/_edit_form.html | 5 +-- .../htmx/destination/_form_list.html | 15 +++++++++ .../htmx/destination/destination_list.html | 16 +-------- 8 files changed, 51 insertions(+), 39 deletions(-) create mode 100644 src/argus/htmx/templates/htmx/destination/_content.html create mode 100644 src/argus/htmx/templates/htmx/destination/_form_list.html diff --git a/src/argus/htmx/destination/urls.py b/src/argus/htmx/destination/urls.py index a3f493a7e..7ffea3ffa 100644 --- a/src/argus/htmx/destination/urls.py +++ b/src/argus/htmx/destination/urls.py @@ -1,10 +1,11 @@ from django.urls import path -from .views import destination_list, destination_delete, destination_update +from .views import destination_list, create_htmx, delete_htmx, update_htmx app_name = "htmx" urlpatterns = [ path("", destination_list, name="destination-list"), - path("/delete/", destination_delete, name="destination-delete"), - path("/", destination_update, name="destination-update"), + path("htmx-create/", create_htmx, name="htmx-create"), + path("/htmx-delete/", delete_htmx, name="htmx-delete"), + path("/htmx-update/", update_htmx, name="htmx-update"), ] diff --git a/src/argus/htmx/destination/views.py b/src/argus/htmx/destination/views.py index 3e00567b2..6cd52bff1 100644 --- a/src/argus/htmx/destination/views.py +++ b/src/argus/htmx/destination/views.py @@ -11,51 +11,51 @@ from .forms import DestinationFormCreate, DestinationFormUpdate -@require_http_methods(["GET", "POST"]) +@require_http_methods(["GET"]) def destination_list(request): - if request.method == "GET": - return _render_destination_list(request) - elif request.method == "POST": - return destination_create(request) + return _render_destination_list(request) -def destination_create(request) -> HttpResponse: +@require_http_methods(["POST"]) +def create_htmx(request) -> HttpResponse: form = DestinationFormCreate(request.POST or None, request=request) + template = "htmx/destination/_content.html" if form.is_valid(): form.save() - return _render_destination_list(request) - return _render_destination_list(request, create_form=form) + return _render_destination_list(request, template=template) + return _render_destination_list(request, create_form=form, template=template) @require_http_methods(["POST"]) -def destination_delete(request, pk: int) -> HttpResponse: +def delete_htmx(request, pk: int) -> HttpResponse: destination = get_object_or_404(request.user.destinations.all(), pk=pk) - + template = "htmx/destination/_form_list.html" try: medium = api_safely_get_medium_object(destination.media.slug) medium.raise_if_not_deletable(destination) except NotificationMedium.NotDeletableError: error_msg = "This destination cannot be deleted." - return _render_destination_list(request, errors=[error_msg]) + return _render_destination_list(request, errors=[error_msg], template=template) else: destination.delete() - return _render_destination_list(request) + return _render_destination_list(request, template=template) @require_http_methods(["POST"]) -def destination_update(request, pk: int) -> HttpResponse: +def update_htmx(request, pk: int) -> HttpResponse: destination = DestinationConfig.objects.get(pk=pk) form = DestinationFormUpdate(request.POST or None, instance=destination, request=request) + template = "htmx/destination/_form_list.html" if form.is_valid(): form.save() - return _render_destination_list(request) + return _render_destination_list(request, template=template) update_forms = _get_update_forms(request.user) for index, update_form in enumerate(update_forms): if update_form.instance.pk == pk: update_forms[index] = form break - return _render_destination_list(request, update_forms=update_forms) + return _render_destination_list(request, update_forms=update_forms, template=template) def _render_destination_list( @@ -63,6 +63,7 @@ def _render_destination_list( create_form: Optional[DestinationFormCreate] = None, update_forms: Optional[Sequence[DestinationFormUpdate]] = None, errors: Optional[Sequence[str]] = None, + template: str = "htmx/destination/destination_list.html", ) -> HttpResponse: """Function to render the destinations page. @@ -86,7 +87,7 @@ def _render_destination_list( "grouped_forms": grouped_forms, "errors": errors, } - return render(request, "htmx/destination/destination_list.html", context=context) + return render(request, template, context=context) def _get_update_forms(user) -> list[DestinationFormUpdate]: diff --git a/src/argus/htmx/templates/htmx/destination/_content.html b/src/argus/htmx/templates/htmx/destination/_content.html new file mode 100644 index 000000000..b9be838c4 --- /dev/null +++ b/src/argus/htmx/templates/htmx/destination/_content.html @@ -0,0 +1,5 @@ +
    + {% include "htmx/destination/_create_form.html" %} + {% for error in errors %}

    {{ error }}

    {% endfor %} + {% include "htmx/destination/_form_list.html" %} +
    diff --git a/src/argus/htmx/templates/htmx/destination/_create_form.html b/src/argus/htmx/templates/htmx/destination/_create_form.html index d90e08da4..03cfbdd2f 100644 --- a/src/argus/htmx/templates/htmx/destination/_create_form.html +++ b/src/argus/htmx/templates/htmx/destination/_create_form.html @@ -1,4 +1,6 @@ -
    + {% csrf_token %}
    Create destination diff --git a/src/argus/htmx/templates/htmx/destination/_delete_form.html b/src/argus/htmx/templates/htmx/destination/_delete_form.html index 7c0d017c0..cea67b243 100644 --- a/src/argus/htmx/templates/htmx/destination/_delete_form.html +++ b/src/argus/htmx/templates/htmx/destination/_delete_form.html @@ -1,5 +1,6 @@ - + {% csrf_token %}