import uuid from django.http import Http404, HttpResponse, HttpResponseBadRequest from django.urls import reverse from django.views.generic.detail import DetailView from django.views.generic.edit import CreateView, DeleteView, UpdateView from django.views.generic.list import ListView from rest_framework.parsers import FormParser from mixins.restrictions import RestrictedViewMixin class AbortSave(Exception): pass class ObjectNameMixin(object): def __init__(self, *args, **kwargs): if self.model is None: self.title = self.context_object_name.title() self.title_singular = self.context_object_name_singular.title() else: self.title_singular = self.model._meta.verbose_name.title() # Hook self.context_object_name_singular = self.title_singular.lower() # hook self.title = self.model._meta.verbose_name_plural.title() # Hooks self.context_object_name = self.title.lower() # hooks self.context_object_name = self.context_object_name.replace(" ", "") self.context_object_name_singular = ( self.context_object_name_singular.replace(" ", "") ) super().__init__(*args, **kwargs) class ObjectList(RestrictedViewMixin, ObjectNameMixin, ListView): allowed_types = ["modal", "widget", "window", "page"] window_content = "window-content/objects.html" list_template = None page_title = None page_subtitle = None list_url_name = None # WARNING: TAKEN FROM locals() list_url_args = ["type"] submit_url_name = None submit_url_args = ["type"] delete_all_url_name = None widget_options = None # copied from BaseListView def get(self, request, *args, **kwargs): type = kwargs.get("type", None) if not type: return HttpResponseBadRequest("No type specified") if type not in self.allowed_types: return HttpResponseBadRequest("Invalid type specified") self.request = request self.object_list = self.get_queryset(**kwargs) if isinstance(self.object_list, HttpResponse): return self.object_list if isinstance(self.object_list, HttpResponseBadRequest): return self.object_list allow_empty = self.get_allow_empty() self.template_name = f"wm/{type}.html" unique = str(uuid.uuid4())[:8] list_url_args = {} for arg in self.list_url_args: if arg in locals(): list_url_args[arg] = locals()[arg] elif arg in kwargs: list_url_args[arg] = kwargs[arg] orig_type = type if type == "page": type = "modal" if not allow_empty: # When pagination is enabled and object_list is a queryset, # it's better to do a cheap query than to load the unpaginated # queryset in memory. if self.get_paginate_by(self.object_list) is not None and hasattr( self.object_list, "exists" ): is_empty = not self.object_list.exists() else: is_empty = not self.object_list if is_empty: raise Http404("Empty list") submit_url_args = {} for arg in self.submit_url_args: if arg in locals(): submit_url_args[arg] = locals()[arg] elif arg in kwargs: submit_url_args[arg] = kwargs[arg] context = self.get_context_data() context["title"] = self.title + f" ({type})" context["title_singular"] = self.title_singular context["unique"] = unique context["window_content"] = self.window_content context["list_template"] = self.list_template context["page_title"] = self.page_title context["page_subtitle"] = self.page_subtitle context["type"] = type context["context_object_name"] = self.context_object_name context["context_object_name_singular"] = self.context_object_name_singular if self.submit_url_name is not None: context["submit_url"] = reverse( self.submit_url_name, kwargs=submit_url_args ) if self.list_url_name is not None: context["list_url"] = reverse(self.list_url_name, kwargs=list_url_args) if self.delete_all_url_name: context["delete_all_url"] = reverse(self.delete_all_url_name) if self.widget_options: context["widget_options"] = self.widget_options # Return partials for HTMX if self.request.htmx: if request.headers["HX-Target"] == self.context_object_name + "-table": self.template_name = self.list_template elif orig_type == "page": self.template_name = self.list_template else: context["window_content"] = self.list_template return self.render_to_response(context) class ObjectCreate(RestrictedViewMixin, ObjectNameMixin, CreateView): allowed_types = ["modal", "widget", "window", "page"] window_content = "window-content/object-form.html" parser_classes = [FormParser] page_title = None page_subtitle = None model = None submit_url_name = None submit_url_args = ["type"] request = None # Whether to hide the cancel button in the form hide_cancel = False def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.title = "Create " + self.context_object_name_singular def post_save(self, obj): pass def pre_save_mutate(self, user, obj): pass def form_valid(self, form): obj = form.save(commit=False) if self.request is None: raise Exception("Request is None") obj.user = self.request.user try: self.pre_save_mutate(self.request.user, obj) except AbortSave as e: context = {"message": f"Failed to save: {e}", "class": "danger"} return self.render_to_response(context) obj.save() form.save_m2m() self.post_save(obj) context = {"message": "Object created", "class": "success"} response = self.render_to_response(context) response["HX-Trigger"] = f"{self.context_object_name_singular}Event" return response def form_invalid(self, form): """If the form is invalid, render the invalid form.""" return self.get(self.request, **self.kwargs, form=form) def get(self, request, *args, **kwargs): type = kwargs.get("type", None) if not type: return HttpResponseBadRequest("No type specified") if type not in self.allowed_types: return HttpResponseBadRequest("Invalid type specified") self.template_name = f"wm/{type}.html" unique = str(uuid.uuid4())[:8] self.request = request self.kwargs = kwargs if type == "widget": self.hide_cancel = True if type == "page": type = "modal" self.object = None submit_url_args = {} for arg in self.submit_url_args: if arg in locals(): submit_url_args[arg] = locals()[arg] elif arg in kwargs: submit_url_args[arg] = kwargs[arg] submit_url = reverse(self.submit_url_name, kwargs=submit_url_args) context = self.get_context_data() form = kwargs.get("form", None) if form: context["form"] = form context["unique"] = unique context["window_content"] = self.window_content context["context_object_name"] = self.context_object_name context["context_object_name_singular"] = self.context_object_name_singular context["submit_url"] = submit_url context["type"] = type context["hide_cancel"] = self.hide_cancel if self.page_title: context["page_title"] = self.page_title if self.page_subtitle: context["page_subtitle"] = self.page_subtitle response = self.render_to_response(context) # response["HX-Trigger"] = f"{self.context_object_name_singular}Event" return response def post(self, request, *args, **kwargs): self.request = request self.template_name = "partials/notify.html" return super().post(request, *args, **kwargs) class ObjectRead(RestrictedViewMixin, ObjectNameMixin, DetailView): allowed_types = ["modal", "widget", "window", "page"] window_content = "window-content/object.html" detail_template = "partials/generic-detail.html" page_title = None page_subtitle = None model = None # submit_url_name = None detail_url_name = None # WARNING: TAKEN FROM locals() detail_url_args = ["type"] request = None def get(self, request, *args, **kwargs): type = kwargs.get("type", None) if not type: return HttpResponseBadRequest("No type specified") if type not in self.allowed_types: return HttpResponseBadRequest() self.template_name = f"wm/{type}.html" unique = str(uuid.uuid4())[:8] detail_url_args = {} for arg in self.detail_url_args: if arg in locals(): detail_url_args[arg] = locals()[arg] elif arg in kwargs: detail_url_args[arg] = kwargs[arg] self.request = request self.object = self.get_object(**kwargs) if isinstance(self.object, HttpResponse): return self.object orig_type = type if type == "page": type = "modal" context = self.get_context_data() context["title"] = self.title + f" ({type})" context["title_singular"] = self.title_singular context["unique"] = unique context["window_content"] = self.window_content context["detail_template"] = self.detail_template if self.page_title: context["page_title"] = self.page_title if self.page_subtitle: context["page_subtitle"] = self.page_subtitle context["type"] = type context["context_object_name"] = self.context_object_name context["context_object_name_singular"] = self.context_object_name_singular if self.detail_url_name is not None: context["detail_url"] = reverse( self.detail_url_name, kwargs=detail_url_args ) # Return partials for HTMX if self.request.htmx: if request.headers["HX-Target"] == self.context_object_name + "-info": self.template_name = self.detail_template elif orig_type == "page": self.template_name = self.detail_template else: context["window_content"] = self.detail_template return self.render_to_response(context) class ObjectUpdate(RestrictedViewMixin, ObjectNameMixin, UpdateView): allowed_types = ["modal", "widget", "window", "page"] window_content = "window-content/object-form.html" parser_classes = [FormParser] page_title = None page_subtitle = None model = None submit_url_name = None submit_url_args = ["type", "pk"] request = None # Whether pk is required in the get request pk_required = True # Whether to hide the cancel button in the form hide_cancel = False def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.title = "Update " + self.context_object_name_singular def post_save(self, obj): pass def form_valid(self, form): obj = form.save(commit=False) if self.request is None: raise Exception("Request is None") obj.save() form.save_m2m() self.post_save(obj) context = {"message": "Object updated", "class": "success"} response = self.render_to_response(context) response["HX-Trigger"] = f"{self.context_object_name_singular}Event" return response def form_invalid(self, form): """If the form is invalid, render the invalid form.""" return self.get(self.request, **self.kwargs, form=form) def get(self, request, *args, **kwargs): self.request = request type = kwargs.get("type", None) pk = kwargs.get("pk", None) if not type: return HttpResponseBadRequest("No type specified") if not pk: if self.pk_required: return HttpResponseBadRequest("No pk specified") if type not in self.allowed_types: return HttpResponseBadRequest("Invalid type specified") self.template_name = f"wm/{type}.html" unique = str(uuid.uuid4())[:8] if type == "widget": self.hide_cancel = True if type == "page": type = "modal" self.object = self.get_object() submit_url_args = {} for arg in self.submit_url_args: if arg in locals(): submit_url_args[arg] = locals()[arg] elif arg in kwargs: submit_url_args[arg] = kwargs[arg] submit_url = reverse(self.submit_url_name, kwargs=submit_url_args) context = self.get_context_data() form = kwargs.get("form", None) if form: context["form"] = form context["title"] = self.title + f" ({type})" context["title_singular"] = self.title_singular context["unique"] = unique context["window_content"] = self.window_content context["context_object_name"] = self.context_object_name context["context_object_name_singular"] = self.context_object_name_singular context["submit_url"] = submit_url context["type"] = type context["hide_cancel"] = self.hide_cancel if self.page_title: context["page_title"] = self.page_title if self.page_subtitle: context["page_subtitle"] = self.page_subtitle response = self.render_to_response(context) # response["HX-Trigger"] = f"{self.context_object_name_singular}Event" return response def post(self, request, *args, **kwargs): self.request = request self.template_name = "partials/notify.html" return super().post(request, *args, **kwargs) class ObjectDelete(RestrictedViewMixin, ObjectNameMixin, DeleteView): model = None template_name = "partials/notify.html" # Overriden to prevent success URL from being used def delete(self, request, *args, **kwargs): """ Call the delete() method on the fetched object and then redirect to the success URL. """ self.object = self.get_object() # success_url = self.get_success_url() self.object.delete() context = {"message": "Object deleted", "class": "success"} response = self.render_to_response(context) response["HX-Trigger"] = f"{self.context_object_name_singular}Event" return response # This will be used in newer Django versions, until then we get a warning def form_valid(self, form): """ Call the delete() method on the fetched object. """ self.object = self.get_object() self.object.delete() context = {"message": "Object deleted", "class": "success"} response = self.render_to_response(context) response["HX-Trigger"] = f"{self.context_object_name_singular}Event" return response