Skip to content

Commit

Permalink
stop, destination-with-forms
Browse files Browse the repository at this point in the history
  • Loading branch information
hmpf committed Jan 24, 2025
1 parent f153d0d commit 1087430
Show file tree
Hide file tree
Showing 14 changed files with 244 additions and 163 deletions.
10 changes: 5 additions & 5 deletions src/argus/htmx/destination/urls.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from django.urls import path

from .views import destination_list, create_htmx, delete_htmx, update_htmx
from .views import destination_list, create_destination, delete_destination, update_destination

app_name = "htmx"
app_name = "destination"
urlpatterns = [
path("", destination_list, name="destination-list"),
path("htmx-create/", create_htmx, name="htmx-create"),
path("<int:pk>/htmx-delete/", delete_htmx, name="htmx-delete"),
path("<int:pk>/htmx-update/", update_htmx, name="htmx-update"),
path("<str:media>/create/", create_destination, name="destination-create"),
path("<int:pk>/delete/", delete_destination, name="destination-delete"),
path("<int:pk>-<str:media>/update/", update_destination, name="destination-update"),
]
221 changes: 138 additions & 83 deletions src/argus/htmx/destination/views.py
Original file line number Diff line number Diff line change
@@ -1,129 +1,184 @@
from typing import Optional, Sequence
from django.shortcuts import render, get_object_or_404
from __future__ import annotations

from django.views.decorators.http import require_http_methods
from collections import namedtuple
import logging
from typing import Optional, TYPE_CHECKING

from django.contrib import messages
from django.http import HttpResponse
from django.shortcuts import redirect, render, get_object_or_404
from django.views.decorators.http import require_http_methods

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 argus.notificationprofile.media import get_medium_object
from argus.notificationprofile.media.base import NotificationMedium, LabelForm

if TYPE_CHECKING:
from django.forms import Form


LOG = logging.getLogger(__name__)
Forms = namedtuple("Forms", ["label_form", "settings_form"])


# not a view
def get_forms(request, media: str, instance: DestinationConfig = None):
prefix = "destinationform"
medium = get_medium_object(media)

if instance and instance.pk:
prefix += f"-{instance.pk}-{media}"
label_name = medium.get_label(instance)
if label_name:
label_initial = {"label": label_name}
else:
prefix += f"-{media}"
label_initial = {}

data = None
for key in request.POST:
if key.startswith(prefix):
data = request.POST
break
label_form = LabelForm(data=data, user=request.user, initial=label_initial, instance=instance, prefix=prefix)
settings_form = medium.validate_settings(data, request.user, instance=instance, prefix=prefix)

if data:
label_form.is_valid()
return Forms(label_form, settings_form)


from .forms import DestinationFormCreate, DestinationFormUpdate
# not a view
def save_forms(user, media: str, label_form: LabelForm, settings_form: Form):
if label_form.instance.pk:
media = label_form.instance.media_id
if label_form.is_valid() and settings_form.is_valid():
obj = label_form.save(commit=False)
obj.user = user
obj.media_id = media
obj.settings = settings_form.cleaned_data
obj.save()
return obj
return None


@require_http_methods(["GET"])
def destination_list(request):
return _render_destination_list(request)


@require_http_methods(["POST"])
def create_htmx(request) -> HttpResponse:
form = DestinationFormCreate(request.POST or None, user=request.user)
@require_http_methods(["GET", "POST"])
def create_destination(request, media: str) -> HttpResponse:
label_form, settings_form = get_forms(request, media)
template = "htmx/destination/_content.html"
if form.is_valid():
form.save()
return _render_destination_list(request, template=template)
return _render_destination_list(request, create_form=form, template=template)
context = {
"label_form": label_form,
"settings_form": settings_form,
"media": media,
}
obj = save_forms(request.user, media, label_form, settings_form)
if obj:
medium = get_medium_object(media)
label = medium.get_label(obj)
message = f'Created new {media} destination "{label}"'
messages.success(request, message)
LOG.info(message)
return _render_destination_list(request, context=context, template=template)
# return redirect("htmx:destination-list")

Check notice

Code scanning / SonarCloud

Logging should not be vulnerable to injection attacks Low

Change this code to not log user-controlled data. See more on SonarQube Cloud
error_msg = f"Could not create new {media} destination"
messages.warning(request, error_msg)
LOG.warn(error_msg)
return _render_destination_list(request, context=context, template=template)


@require_http_methods(["POST"])
def delete_htmx(request, pk: int) -> HttpResponse:
def delete_destination(request, pk: int) -> HttpResponse:
destination = get_object_or_404(request.user.destinations.all(), pk=pk)
media = destination.media
error_msg = None
medium = get_medium_object(media.slug)
destination_label = medium.get_label(destination)
try:
medium = api_safely_get_medium_object(destination.media.slug)
medium.raise_if_not_deletable(destination)
except NotificationMedium.NotDeletableError:
error_msg = "That destination cannot be deleted."
except NotificationMedium.NotDeletableError as e:
# template?
error_msg = ", ".join(e.args)
message = f'Failed to delete {media} destination "{destination}": {error_msg}'
messages.warning(request, message)
LOG.warn(message)
else:
destination.delete()
message = f'Deleted {media} destination "{destination_label}"'
messages.success(request, message)
LOG.info(message)

forms = _get_update_forms(request.user, media=media)

context = {
"error_msg": error_msg,
"forms": forms,
"media": media,
}
return render(request, "htmx/destination/_collapse_with_forms.html", context=context)
return redirect("htmx:destination-list")


@require_http_methods(["POST"])
def update_htmx(request, pk: int) -> HttpResponse:
destination = DestinationConfig.objects.get(pk=pk)
form = DestinationFormUpdate(request.POST or None, instance=destination, request=request.user)
def update_destination(request, pk: int, media: str) -> HttpResponse:
medium = get_medium_object(media)
template = "htmx/destination/_form_list.html"
if form.is_valid():
form.save()
destination = get_object_or_404(request.user.destinations.all(), pk=pk)
label = medium.get_label(destination)
forms = get_forms(request, media, instance=destination)
obj = save_forms(request.user, media, *forms)
if obj:
label = medium.get_label(obj)
message = f'Updated {media} destination "{label}"'
messages.success(request, message)
LOG.info(message)
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
error_msg = f'Could not update {media} destination "{label}"'

Check notice

Code scanning / SonarCloud

Logging should not be vulnerable to injection attacks Low

Change this code to not log user-controlled data. See more on SonarQube Cloud
messages.warning(request, error_msg)
LOG.warn(request, error_msg)
all_forms = get_all_forms_grouped_by_media(request)
update_forms = get_all_update_forms_for_media(request, media)
for index, forms in enumerate(update_forms):
if forms.label_form.instance.pk == pk:
update_forms[index] = forms
break
return _render_destination_list(request, update_forms=update_forms, template=template)
all_forms[media] = update_forms
context = {
"forms": all_forms,
"label_form": forms.label_form,
"settings_form": forms.settings_form,
"media": media,
}
return _render_destination_list(request, context=context, template=template)


def _render_destination_list(
request,
create_form: Optional[DestinationFormCreate] = None,
update_forms: Optional[Sequence[DestinationFormUpdate]] = None,
context: Optional[dict] = None,
template: str = "htmx/destination/destination_list.html",
) -> 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 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."""

if create_form is None:
create_form = DestinationFormCreate()
if update_forms is None:
update_forms = _get_update_forms(request.user)
grouped_forms = _group_update_forms_by_media(update_forms)
context = {
"create_form": create_form,
"grouped_forms": grouped_forms,
"page_title": "Destinations",
}
"""Function to render the destinations page"""

if not context:
context = {}
if "forms" not in context:
context["forms"] = get_all_forms_grouped_by_media(request)
context["page_title"] = "Destinations"
return render(request, template, context=context)


def _get_update_forms(user, media: Media = None) -> list[DestinationFormUpdate]:
def get_all_update_forms_for_media(request, media: str) -> list[Forms]:
"""Get a list of update forms for the user's destinations.
:param media: if provided, only return destinations for this media.
:param media: Only return destinations for this media.
"""
if media:
destinations = user.destinations.filter(media=media)
else:
destinations = user.destinations.all()
# Sort by oldest first
destinations = destinations.order_by("pk")
return [DestinationFormUpdate(instance=destination) for destination in destinations]
destinations = request.user.destinations.filter(media_id=media).order_by("pk")

return [get_forms(request, media, 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
def get_all_forms_grouped_by_media(request):
forms = {}
for media in Media.objects.all():
grouped_destinations[media] = []

for form in destination_forms:
grouped_destinations[form.instance.media].append(form)

return grouped_destinations


def _replace_form_in_list(forms: list[DestinationFormUpdate], form: DestinationFormUpdate):
for index, f in enumerate(forms):
if f.instance.pk == form.instance.pk:
forms[index] = form
break
create_form = get_forms(request, media.slug)
update_forms = get_all_update_forms_for_media(request, media.slug)
forms[media] = {
"create_form": create_form,
"update_forms": update_forms,
}
return forms
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
<details class="collapse bg-base-200 collapse-arrow" open="">
<summary class="collapse-title text-xl font-medium">{{ media.name }} ({{ forms|length }})</summary>
<div class="collapse-content">
<summary class="collapse-title text-xl font-medium">{{ media.name }} ({{ forms.update_forms|length }})</summary>
<div class="collapse-content flex flex-col gap-4">
{% if error_msg %}<p class="text-error">{{ error_msg }}</p>{% endif %}
{% for form in forms %}
<div class="flex w-full h-fit items-center justify-center">
{% with forms=forms.create_form %}
{% include "htmx/destination/_create_form.html" %}
{% endwith %}
</div>
{% for forms in forms.update_forms %}
<div class="flex w-full h-fit items-center justify-center">
{% include "htmx/destination/_edit_form.html" %}
{% include "htmx/destination/_delete_form.html" %}
Expand Down
1 change: 0 additions & 1 deletion src/argus/htmx/templates/htmx/destination/_content.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
<div id="destination-content" class="flex flex-col items-center gap-4">
{% include "htmx/destination/_create_form.html" %}
{% include "htmx/destination/_form_list.html" %}
</div>
43 changes: 11 additions & 32 deletions src/argus/htmx/templates/htmx/destination/_create_form.html
Original file line number Diff line number Diff line change
@@ -1,32 +1,11 @@
<form hx-post="{% url 'htmx:htmx-create' %}"
hx-trigger="submit"
hx-target="#destination-content"
hx-swap="outerHTML"
class="max-w-4xl w-full">
{% csrf_token %}
<fieldset class="p-2 border rounded-box border-primary items-center gap-4 flex items-end justify-center">
<legend class="menu-title">Create destination</legend>
{% for field in create_form %}
<label class="form-control max-w-xs mb-auto">
<div class="label">
<span class="label-text">{{ field.label }}</span>
</div>
{% if field.name == "media" %}
{{ field }}
{% else %}
<div class="input input-bordered flex items-center gap-2">{{ field }}</div>
{% endif %}
<div class="label">
<span class="label-text-alt min-h-4">
{% if field.errors %}
{% for error in field.errors %}<p class="text-error">{{ error }}</p>{% endfor %}
{% endif %}
</span>
</div>
</label>
{% empty %}
<p>Something went wrong</p>
{% endfor %}
<input type="submit" value="Create" class="btn btn-primary">
</fieldset>
</form>
{% with label_form=forms.label_form settings_form=forms.settings_form %}
<form {% if label_form.prefix %}id="{{ label_form.prefix }}"{% endif %}
action="{% url 'htmx:destination-create' media=media %}"
hx-post="{% url 'htmx:destination-create' media=media %}"
hx-trigger="submit"
hx-target="#destination-content"
hx-swap="outerHTML"
class="max-w-4xl w-full">
{% include "./_destination_form.html" %}
</form>
{% endwith %}
18 changes: 9 additions & 9 deletions src/argus/htmx/templates/htmx/destination/_delete_form.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<form hx-post="{% url 'htmx:htmx-delete' form.instance.id %}"
hx-trigger="submit"
hx-target="closest details"
hx-swap="outerHTML">
{% csrf_token %}
<fieldset class="menu menu-horizontal menu-md items-center gap-4">
<input type="submit" value="Delete" class="btn btn-secondary">
</fieldset>
</form>
{% with form=forms.label_form %}
<form action="{% url 'htmx:destination-delete' form.instance.id %}"
method="post">
{% csrf_token %}
<fieldset class="menu menu-horizontal menu-md items-center gap-4">
<input type="submit" value="Delete" class="btn btn-secondary">
</fieldset>
</form>
{% endwith %}
15 changes: 15 additions & 0 deletions src/argus/htmx/templates/htmx/destination/_destination_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{% with update=label_form.instance.pk %}
{% csrf_token %}
<fieldset class="p-2 border rounded-box border{% if not update %}-primary{% endif %} items-center gap-4 flex flex-col items-end justify-center">
{% if not label_form.instance.pk %}<legend class="menu-title">Create destination</legend>{% endif %}
{% include "./_non_field_form_errors.html" %}
<div class="items-center gap-4 flex flex-row items-end justify-center">
{% include "./_form_fields.html" %}
{% if update %}
<input type="submit" value="Update" class="btn btn-primary">
{% else %}
<input type="submit" value="Create" class="btn btn-primary">
{% endif %}
</div>
</fieldset>
{% endwith %}
Loading

0 comments on commit 1087430

Please sign in to comment.