Skip to content

Commit a51d4e7

Browse files
committed
stop, destination-with-forms
1 parent f153d0d commit a51d4e7

File tree

14 files changed

+247
-163
lines changed

14 files changed

+247
-163
lines changed

src/argus/htmx/destination/urls.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from django.urls import path
22

3-
from .views import destination_list, create_htmx, delete_htmx, update_htmx
3+
from .views import destination_list, create_destination, delete_destination, update_destination
44

5-
app_name = "htmx"
5+
app_name = "destination"
66
urlpatterns = [
77
path("", destination_list, name="destination-list"),
8-
path("htmx-create/", create_htmx, name="htmx-create"),
9-
path("<int:pk>/htmx-delete/", delete_htmx, name="htmx-delete"),
10-
path("<int:pk>/htmx-update/", update_htmx, name="htmx-update"),
8+
path("<str:media>/create/", create_destination, name="destination-create"),
9+
path("<int:pk>/delete/", delete_destination, name="destination-delete"),
10+
path("<int:pk>-<str:media>/update/", update_destination, name="destination-update"),
1111
]
Lines changed: 141 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,129 +1,187 @@
1-
from typing import Optional, Sequence
2-
from django.shortcuts import render, get_object_or_404
1+
from __future__ import annotations
32

4-
from django.views.decorators.http import require_http_methods
3+
from collections import namedtuple
4+
import logging
5+
from typing import Optional, TYPE_CHECKING
6+
7+
from django.http import QueryDict
8+
from django.contrib import messages
59
from django.http import HttpResponse
10+
from django.shortcuts import redirect, render, get_object_or_404
11+
from django.views.decorators.http import require_http_methods
612

713
from argus.notificationprofile.models import DestinationConfig, Media
8-
from argus.notificationprofile.media import api_safely_get_medium_object
9-
from argus.notificationprofile.media.base import NotificationMedium
14+
from argus.notificationprofile.media import get_medium_object
15+
from argus.notificationprofile.media.base import NotificationMedium, LabelForm
16+
17+
if TYPE_CHECKING:
18+
from django.forms import Form
19+
20+
21+
LOG = logging.getLogger(__name__)
22+
Forms = namedtuple("Forms", ["label_form", "settings_form"])
23+
24+
25+
# not a view
26+
def get_forms(request, media: str, instance: DestinationConfig = None):
27+
prefix = "destinationform"
28+
medium = get_medium_object(media)
29+
30+
if instance and instance.pk:
31+
prefix += f"-{instance.pk}-{media}"
32+
label_name = medium.get_label(instance)
33+
if label_name:
34+
label_initial = {"label": label_name}
35+
else:
36+
prefix += f"-{media}"
37+
label_initial = {}
38+
39+
data = None
40+
for key in request.POST:
41+
if key.startswith(prefix):
42+
data = request.POST
43+
break
44+
label_form = LabelForm(data=data, user=request.user, initial=label_initial, instance=instance, prefix=prefix)
45+
settings_form = medium.validate_settings(data, request.user, instance=instance, prefix=prefix)
46+
47+
if data:
48+
label_form.is_valid()
49+
return Forms(label_form, settings_form)
50+
1051

11-
from .forms import DestinationFormCreate, DestinationFormUpdate
52+
# not a view
53+
def save_forms(user, media: str, label_form: LabelForm, settings_form: Form):
54+
if label_form.instance.pk:
55+
media = label_form.instance.media_id
56+
if label_form.is_valid() and settings_form.is_valid():
57+
obj = label_form.save(commit=False)
58+
obj.user = user
59+
obj.media_id = media
60+
obj.settings = settings_form.cleaned_data
61+
obj.save()
62+
return obj
63+
return None
1264

1365

1466
@require_http_methods(["GET"])
1567
def destination_list(request):
1668
return _render_destination_list(request)
1769

1870

19-
@require_http_methods(["POST"])
20-
def create_htmx(request) -> HttpResponse:
21-
form = DestinationFormCreate(request.POST or None, user=request.user)
71+
@require_http_methods(["GET", "POST"])
72+
def create_destination(request, media: str) -> HttpResponse:
73+
medium = get_medium_object(media)
74+
label_form, settings_form = get_forms(request, media)
2275
template = "htmx/destination/_content.html"
23-
if form.is_valid():
24-
form.save()
25-
return _render_destination_list(request, template=template)
26-
return _render_destination_list(request, create_form=form, template=template)
76+
context = {
77+
"label_form": label_form,
78+
"settings_form": settings_form,
79+
"media": media,
80+
}
81+
obj = save_forms(request.user, media, label_form, settings_form)
82+
if obj:
83+
label = medium.get_label(obj)
84+
message = f'Created new {media} destination "{label}"'
85+
messages.success(request, message)
86+
LOG.info(message)
87+
request.POST = QueryDict("")
88+
return _render_destination_list(request, context=context, template=template)
89+
# return redirect("htmx:destination-list")
90+
error_msg = f"Could not create new {media} destination"
91+
messages.warning(request, error_msg)
92+
LOG.warn(error_msg)
93+
return _render_destination_list(request, context=context, template=template)
2794

2895

2996
@require_http_methods(["POST"])
30-
def delete_htmx(request, pk: int) -> HttpResponse:
97+
def delete_destination(request, pk: int) -> HttpResponse:
3198
destination = get_object_or_404(request.user.destinations.all(), pk=pk)
3299
media = destination.media
33100
error_msg = None
101+
medium = get_medium_object(media.slug)
102+
destination_label = medium.get_label(destination)
34103
try:
35-
medium = api_safely_get_medium_object(destination.media.slug)
36104
medium.raise_if_not_deletable(destination)
37-
except NotificationMedium.NotDeletableError:
38-
error_msg = "That destination cannot be deleted."
105+
except NotificationMedium.NotDeletableError as e:
106+
# template?
107+
error_msg = ", ".join(e.args)
108+
message = f'Failed to delete {media} destination "{destination}": {error_msg}'
109+
messages.warning(request, message)
110+
LOG.warn(message)
39111
else:
40112
destination.delete()
113+
message = f'Deleted {media} destination "{destination_label}"'
114+
messages.success(request, message)
115+
LOG.info(message)
41116

42-
forms = _get_update_forms(request.user, media=media)
43-
44-
context = {
45-
"error_msg": error_msg,
46-
"forms": forms,
47-
"media": media,
48-
}
49-
return render(request, "htmx/destination/_collapse_with_forms.html", context=context)
117+
return redirect("htmx:destination-list")
50118

51119

52120
@require_http_methods(["POST"])
53-
def update_htmx(request, pk: int) -> HttpResponse:
54-
destination = DestinationConfig.objects.get(pk=pk)
55-
form = DestinationFormUpdate(request.POST or None, instance=destination, request=request.user)
121+
def update_destination(request, pk: int, media: str) -> HttpResponse:
122+
medium = get_medium_object(media)
56123
template = "htmx/destination/_form_list.html"
57-
if form.is_valid():
58-
form.save()
124+
destination = get_object_or_404(request.user.destinations.all(), pk=pk)
125+
label = medium.get_label(destination)
126+
forms = get_forms(request, media, instance=destination)
127+
obj = save_forms(request.user, media, *forms)
128+
if obj:
129+
label = medium.get_label(obj)
130+
message = f'Updated {media} destination "{label}"'
131+
messages.success(request, message)
132+
LOG.info(message)
133+
request.POST = QueryDict("")
59134
return _render_destination_list(request, template=template)
60-
update_forms = _get_update_forms(request.user)
61-
for index, update_form in enumerate(update_forms):
62-
if update_form.instance.pk == pk:
63-
update_forms[index] = form
135+
error_msg = f'Could not update {media} destination "{label}"'
136+
messages.warning(request, error_msg)
137+
LOG.warn(request, error_msg)
138+
all_forms = get_all_forms_grouped_by_media(request)
139+
update_forms = get_all_update_forms_for_media(request, media)
140+
for index, forms in enumerate(update_forms):
141+
if forms.label_form.instance.pk == pk:
142+
update_forms[index] = forms
64143
break
65-
return _render_destination_list(request, update_forms=update_forms, template=template)
144+
all_forms[media] = update_forms
145+
context = {
146+
"forms": all_forms,
147+
"label_form": forms.label_form,
148+
"settings_form": forms.settings_form,
149+
"media": media,
150+
}
151+
return _render_destination_list(request, context=context, template=template)
66152

67153

68154
def _render_destination_list(
69155
request,
70-
create_form: Optional[DestinationFormCreate] = None,
71-
update_forms: Optional[Sequence[DestinationFormUpdate]] = None,
156+
context: Optional[dict] = None,
72157
template: str = "htmx/destination/destination_list.html",
73158
) -> HttpResponse:
74-
"""Function to render the destinations page.
75-
76-
:param create_form: this is used to display the form for creating a new destination
77-
with errors while retaining the user input. If you want a blank form, pass None.
78-
:param update_forms: list of update forms to display. Useful for rendering forms
79-
with error messages while retaining the user input.
80-
If this is None, the update forms will be generated from the user's destinations."""
81-
82-
if create_form is None:
83-
create_form = DestinationFormCreate()
84-
if update_forms is None:
85-
update_forms = _get_update_forms(request.user)
86-
grouped_forms = _group_update_forms_by_media(update_forms)
87-
context = {
88-
"create_form": create_form,
89-
"grouped_forms": grouped_forms,
90-
"page_title": "Destinations",
91-
}
159+
"""Function to render the destinations page"""
160+
161+
if not context:
162+
context = {}
163+
if "forms" not in context:
164+
context["forms"] = get_all_forms_grouped_by_media(request)
165+
context["page_title"] = "Destinations"
92166
return render(request, template, context=context)
93167

94168

95-
def _get_update_forms(user, media: Media = None) -> list[DestinationFormUpdate]:
169+
def get_all_update_forms_for_media(request, media: str) -> list[Forms]:
96170
"""Get a list of update forms for the user's destinations.
97-
:param media: if provided, only return destinations for this media.
171+
:param media: Only return destinations for this media.
98172
"""
99-
if media:
100-
destinations = user.destinations.filter(media=media)
101-
else:
102-
destinations = user.destinations.all()
103-
# Sort by oldest first
104-
destinations = destinations.order_by("pk")
105-
return [DestinationFormUpdate(instance=destination) for destination in destinations]
173+
destinations = request.user.destinations.filter(media_id=media).order_by("pk")
106174

175+
return [get_forms(request, media, instance=destination) for destination in destinations]
107176

108-
def _group_update_forms_by_media(
109-
destination_forms: Sequence[DestinationFormUpdate],
110-
) -> dict[Media, list[DestinationFormUpdate]]:
111-
grouped_destinations = {}
112177

113-
# Adding a media to the dict even if there are no destinations for it
114-
# is useful so that the template can render a section for that media
178+
def get_all_forms_grouped_by_media(request):
179+
forms = {}
115180
for media in Media.objects.all():
116-
grouped_destinations[media] = []
117-
118-
for form in destination_forms:
119-
grouped_destinations[form.instance.media].append(form)
120-
121-
return grouped_destinations
122-
123-
124-
def _replace_form_in_list(forms: list[DestinationFormUpdate], form: DestinationFormUpdate):
125-
for index, f in enumerate(forms):
126-
if f.instance.pk == form.instance.pk:
127-
forms[index] = form
128-
break
181+
create_form = get_forms(request, media.slug)
182+
update_forms = get_all_update_forms_for_media(request, media.slug)
183+
forms[media] = {
184+
"create_form": create_form,
185+
"update_forms": update_forms,
186+
}
129187
return forms

src/argus/htmx/templates/htmx/destination/_collapse_with_forms.html

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
<details class="collapse bg-base-200 collapse-arrow" open="">
2-
<summary class="collapse-title text-xl font-medium">{{ media.name }} ({{ forms|length }})</summary>
3-
<div class="collapse-content">
2+
<summary class="collapse-title text-xl font-medium">{{ media.name }} ({{ forms.update_forms|length }})</summary>
3+
<div class="collapse-content flex flex-col gap-4">
44
{% if error_msg %}<p class="text-error">{{ error_msg }}</p>{% endif %}
5-
{% for form in forms %}
5+
<div class="flex w-full h-fit items-center justify-center">
6+
{% with forms=forms.create_form %}
7+
{% include "htmx/destination/_create_form.html" %}
8+
{% endwith %}
9+
</div>
10+
{% for forms in forms.update_forms %}
611
<div class="flex w-full h-fit items-center justify-center">
712
{% include "htmx/destination/_edit_form.html" %}
813
{% include "htmx/destination/_delete_form.html" %}
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
<div id="destination-content" class="flex flex-col items-center gap-4">
2-
{% include "htmx/destination/_create_form.html" %}
32
{% include "htmx/destination/_form_list.html" %}
43
</div>
Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,11 @@
1-
<form hx-post="{% url 'htmx:htmx-create' %}"
2-
hx-trigger="submit"
3-
hx-target="#destination-content"
4-
hx-swap="outerHTML"
5-
class="max-w-4xl w-full">
6-
{% csrf_token %}
7-
<fieldset class="p-2 border rounded-box border-primary items-center gap-4 flex items-end justify-center">
8-
<legend class="menu-title">Create destination</legend>
9-
{% for field in create_form %}
10-
<label class="form-control max-w-xs mb-auto">
11-
<div class="label">
12-
<span class="label-text">{{ field.label }}</span>
13-
</div>
14-
{% if field.name == "media" %}
15-
{{ field }}
16-
{% else %}
17-
<div class="input input-bordered flex items-center gap-2">{{ field }}</div>
18-
{% endif %}
19-
<div class="label">
20-
<span class="label-text-alt min-h-4">
21-
{% if field.errors %}
22-
{% for error in field.errors %}<p class="text-error">{{ error }}</p>{% endfor %}
23-
{% endif %}
24-
</span>
25-
</div>
26-
</label>
27-
{% empty %}
28-
<p>Something went wrong</p>
29-
{% endfor %}
30-
<input type="submit" value="Create" class="btn btn-primary">
31-
</fieldset>
32-
</form>
1+
{% with label_form=forms.label_form settings_form=forms.settings_form %}
2+
<form {% if label_form.prefix %}id="{{ label_form.prefix }}"{% endif %}
3+
action="{% url 'htmx:destination-create' media=media %}"
4+
hx-post="{% url 'htmx:destination-create' media=media %}"
5+
hx-trigger="submit"
6+
hx-target="#destination-content"
7+
hx-swap="outerHTML"
8+
class="max-w-4xl w-full">
9+
{% include "./_destination_form.html" %}
10+
</form>
11+
{% endwith %}
Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
<form hx-post="{% url 'htmx:htmx-delete' form.instance.id %}"
2-
hx-trigger="submit"
3-
hx-target="closest details"
4-
hx-swap="outerHTML">
5-
{% csrf_token %}
6-
<fieldset class="menu menu-horizontal menu-md items-center gap-4">
7-
<input type="submit" value="Delete" class="btn btn-secondary">
8-
</fieldset>
9-
</form>
1+
{% with form=forms.label_form %}
2+
<form action="{% url 'htmx:destination-delete' form.instance.id %}"
3+
method="post">
4+
{% csrf_token %}
5+
<fieldset class="menu menu-horizontal menu-md items-center gap-4">
6+
<input type="submit" value="Delete" class="btn btn-secondary">
7+
</fieldset>
8+
</form>
9+
{% endwith %}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{% with update=label_form.instance.pk %}
2+
{% csrf_token %}
3+
<fieldset class="p-2 border rounded-box border{% if not update %}-primary{% endif %} items-center gap-4 flex flex-col items-end justify-center">
4+
{% if not label_form.instance.pk %}<legend class="menu-title">Create destination</legend>{% endif %}
5+
{% include "./_non_field_form_errors.html" %}
6+
<div class="items-center gap-4 flex flex-row items-end justify-center">
7+
{% include "./_form_fields.html" %}
8+
{% if update %}
9+
<input type="submit" value="Update" class="btn btn-primary">
10+
{% else %}
11+
<input type="submit" value="Create" class="btn btn-primary">
12+
{% endif %}
13+
</div>
14+
</fieldset>
15+
{% endwith %}

0 commit comments

Comments
 (0)