diff --git a/core/forms.py b/core/forms.py index 5392800..99829e6 100644 --- a/core/forms.py +++ b/core/forms.py @@ -1,9 +1,41 @@ from django import forms from django.contrib.auth.forms import UserCreationForm +from django.core.exceptions import FieldDoesNotExist from .models import User +# from django.forms import ModelForm + + # Create your forms here. +class RestrictedFormMixin: + """ + This mixin is used to restrict the queryset of a form to the current user. + The request object is passed from the view. + Fieldargs is used to pass additional arguments to the queryset filter. + """ + + fieldargs = {} + + def __init__(self, *args, **kwargs): + # self.fieldargs = {} + self.request = kwargs.pop("request") + super().__init__(*args, **kwargs) + for field in self.fields: + # Check it's not something like a CharField which has no queryset + if not hasattr(self.fields[field], "queryset"): + continue + + model = self.fields[field].queryset.model + # Check if the model has a user field + try: + model._meta.get_field("user") + # Add the user to the queryset filters + self.fields[field].queryset = model.objects.filter( + user=self.request.user, **self.fieldargs.get(field, {}) + ) + except FieldDoesNotExist: + pass class NewUserForm(UserCreationForm): diff --git a/core/views/helpers.py b/core/views/helpers.py index 7816a13..002aeb4 100644 --- a/core/views/helpers.py +++ b/core/views/helpers.py @@ -8,8 +8,486 @@ # from siphashc import siphash # from sortedcontainers import SortedSet +import uuid + # from core import r from django.conf import settings +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 + +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, **kwargs): + """ + 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): + 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 + + 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") + + 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={"type": type}) + + 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 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): + 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 class SearchDenied: