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, 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 core.util import logs log = logs.get_logger(__name__) 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 get_queryset(self): """ This function is overriden to filter the objects by the requesting user. """ if self.queryset is not None: queryset = self.queryset if isinstance(queryset, QuerySet): # queryset = queryset.all() queryset = queryset.filter(user=self.request.user) elif self.model is not None: queryset = self.model._default_manager.filter(user=self.request.user) 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): 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 delete_all_url_name = None # copied from BaseListView def get(self, request, *args, **kwargs): self.request = request self.object_list = self.get_queryset() allow_empty = self.get_allow_empty() 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] list_url_args = {} for arg in self.list_url_args: list_url_args[arg] = locals()[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 = reverse(self.submit_url_name, kwargs={"type": type}) list_url = reverse(self.list_url_name, kwargs=list_url_args) 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["submit_url"] = submit_url context["list_url"] = list_url context["context_object_name"] = self.context_object_name context["context_object_name_singular"] = self.context_object_name_singular if self.delete_all_url_name: context["delete_all_url"] = reverse(self.delete_all_url_name) # Return partials for HTMX if self.request.htmx: if 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] model = None submit_url_name = None list_url_name = None # WARNING: TAKEN FROM locals() list_url_args = ["type"] request = None 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.user = self.request.user 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): self.request = request self.kwargs = 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] list_url_args = {} for arg in self.list_url_args: list_url_args[arg] = locals()[arg] if type == "page": type = "modal" self.object = None submit_url = reverse(self.submit_url_name, kwargs={"type": type}) list_url = reverse(self.list_url_name, kwargs=list_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["list_url"] = list_url context["type"] = type 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" model = None class ObjectUpdate(RestrictedViewMixin, ObjectNameMixin, UpdateView): allowed_types = ["modal", "widget", "window", "page"] window_content = "window-content/object-form.html" parser_classes = [FormParser] model = None submit_url_name = None request = None 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: 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 == "page": type = "modal" self.object = self.get_object() submit_url = reverse(self.submit_url_name, kwargs={"type": type, "pk": pk}) 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 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