import uuid from django.core.exceptions import ImproperlyConfigured from django.core.paginator import Paginator from django.db.models import QuerySet 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 class AbortSave(Exception): pass class RestrictedViewMixin: """ This mixin overrides two helpers in order to pass the user object to the filters. get_queryset alters the objects returned for list views. get_form_kwargs passes the request object to the form class. Remaining permissions checks are in forms.py """ allow_empty = True queryset = None model = None paginate_by = None paginate_orphans = 0 context_object_name = None paginator_class = Paginator page_kwarg = "page" ordering = None def set_extra_args(self, user): """ This function is overriden to filter the objects by the requesting user. """ self.extra_permission_args = {} def get_queryset(self, **kwargs): """ This function is overriden to filter the objects by the requesting user. """ self.set_extra_args(self.request.user) if self.queryset is not None: queryset = self.queryset if isinstance(queryset, QuerySet): # queryset = queryset.all() queryset = queryset.filter( user=self.request.user, **self.extra_permission_args ) elif self.model is not None: queryset = self.model._default_manager.filter( user=self.request.user, **self.extra_permission_args ) else: raise ImproperlyConfigured( "%(cls)s is missing a QuerySet. Define " "%(cls)s.model, %(cls)s.queryset, or override " "%(cls)s.get_queryset()." % {"cls": self.__class__.__name__} ) if hasattr(self, "get_ordering"): ordering = self.get_ordering() if ordering: if isinstance(ordering, str): ordering = (ordering,) queryset = queryset.order_by(*ordering) return queryset def get_form_kwargs(self): """Passes the request object to the form class. This is necessary to only display members that belong to a given user""" kwargs = super().get_form_kwargs() kwargs["request"] = self.request return kwargs 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