-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Heavily reuses the destinations API which is not ideal.
- Loading branch information
Showing
15 changed files
with
1,026 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Add HTMX version of the destinations page |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
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", | ||
} | ||
widgets = { | ||
"media": forms.Select(attrs={"class": "select input-bordered w-full max-w-xs"}), | ||
} | ||
|
||
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 | ||
|
||
|
||
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" | ||
""" | ||
medium = api_safely_get_medium_object(media.slug) | ||
return medium.MEDIA_JSON_SCHEMA["required"][0] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
from django.urls import path | ||
|
||
from .views import destination_list, create_htmx, delete_htmx, update_htmx | ||
|
||
app_name = "htmx" | ||
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"), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
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.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, DestinationFormUpdate | ||
|
||
|
||
@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, request=request) | ||
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) | ||
|
||
|
||
@require_http_methods(["POST"]) | ||
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], template=template) | ||
else: | ||
destination.delete() | ||
return _render_destination_list(request, template=template) | ||
|
||
|
||
@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) | ||
template = "htmx/destination/_form_list.html" | ||
if form.is_valid(): | ||
form.save() | ||
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, template=template) | ||
|
||
|
||
def _render_destination_list( | ||
request, | ||
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. | ||
: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. | ||
: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, | ||
"grouped_forms": grouped_forms, | ||
"errors": errors, | ||
} | ||
return render(request, template, 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 |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.