-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
TP2000-1404: Reorganise measures and common code (#1266)
* Split up common views * Split up measures views and forms code * Add imports to module __init__ files * Fix test * Remove empty files * Tidy up common views imports * Revert imports * Revert import * Revert comment * Fix import
- Loading branch information
Showing
31 changed files
with
4,047 additions
and
3,885 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
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
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
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,3 @@ | ||
from .base import * | ||
from .mixins import * | ||
from .pages import * |
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,86 @@ | ||
from typing import Optional | ||
|
||
from django.contrib.auth.mixins import PermissionRequiredMixin | ||
from django.db import transaction | ||
from django.template.response import TemplateResponse | ||
from django.views.generic import DetailView | ||
from django.views.generic.edit import FormMixin | ||
from django_filters.views import FilterView | ||
|
||
from common.validators import UpdateType | ||
from workbaskets.views.mixins import WithCurrentWorkBasket | ||
|
||
from .mixins import BusinessRulesMixin | ||
from .mixins import TrackedModelDetailMixin | ||
from .mixins import WithPaginationListMixin | ||
|
||
|
||
def handler403(request, *args, **kwargs): | ||
return TemplateResponse(request=request, template="common/403.jinja", status=403) | ||
|
||
|
||
def handler500(request, *args, **kwargs): | ||
return TemplateResponse(request=request, template="common/500.jinja", status=500) | ||
|
||
|
||
class WithPaginationListView(WithPaginationListMixin, FilterView): | ||
"""Generic filtered list view enabling pagination.""" | ||
|
||
|
||
class TamatoListView(WithCurrentWorkBasket, WithPaginationListView): | ||
"""Base view class for listing tariff components including those in the | ||
current workbasket, with pagination.""" | ||
|
||
|
||
class TrackedModelDetailView( | ||
WithCurrentWorkBasket, | ||
TrackedModelDetailMixin, | ||
DetailView, | ||
): | ||
"""Base view class for displaying a single TrackedModel.""" | ||
|
||
|
||
class TrackedModelChangeView( | ||
WithCurrentWorkBasket, | ||
PermissionRequiredMixin, | ||
BusinessRulesMixin, | ||
): | ||
update_type: UpdateType | ||
success_path: Optional[str] = None | ||
|
||
@property | ||
def success_url(self): | ||
return self.object.get_url(self.success_path) | ||
|
||
def get_result_object(self, form): | ||
""" | ||
Overridable used to get a saved result. | ||
In the default case (this implementation) a new version of a | ||
TrackedModel instance is created. However, this function may be | ||
overridden to provide alternative behaviour, such as simply updating the | ||
TrackedModel instance. | ||
""" | ||
# compares changed data against model fields to prevent unexpected kwarg TypeError | ||
# e.g. `geographical_area_group` is a field on `MeasureUpdateForm` and included in cleaned data, | ||
# but isn't a field on `Measure` and would cause a TypeError on model save() | ||
model_fields = [f.name for f in self.model._meta.get_fields()] | ||
form_changed_data = [f for f in form.changed_data if f in model_fields] | ||
changed_data = {name: form.cleaned_data[name] for name in form_changed_data} | ||
|
||
return form.instance.new_version( | ||
workbasket=self.workbasket, | ||
update_type=self.update_type, | ||
**changed_data, | ||
) | ||
|
||
@transaction.atomic | ||
def form_valid(self, form): | ||
self.object = self.get_result_object(form) | ||
violations = self.form_violates(form) | ||
|
||
if violations: | ||
transaction.set_rollback(True) | ||
return self.form_invalid(form) | ||
|
||
return FormMixin.form_valid(self, form) |
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,164 @@ | ||
from typing import Optional | ||
from typing import Tuple | ||
from typing import Type | ||
|
||
from django.conf import settings | ||
from django.contrib.auth.mixins import UserPassesTestMixin | ||
from django.core.paginator import Paginator | ||
from django.db.models import Model | ||
from django.db.models import QuerySet | ||
from django.http import Http404 | ||
|
||
from common.business_rules import BusinessRule | ||
from common.business_rules import BusinessRuleViolation | ||
from common.models import TrackedModel | ||
from common.pagination import build_pagination_list | ||
|
||
|
||
class WithPaginationListMixin: | ||
"""Mixin that can be inherited by a ListView subclass to enable this | ||
project's pagination capabilities.""" | ||
|
||
paginator_class = Paginator | ||
paginate_by = settings.REST_FRAMEWORK["PAGE_SIZE"] | ||
|
||
def get_context_data(self, *, object_list=None, **kwargs): | ||
"""Adds a page link list to the context.""" | ||
data = super().get_context_data(object_list=object_list, **kwargs) | ||
page_obj = data["page_obj"] | ||
page_number = page_obj.number | ||
data["page_links"] = build_pagination_list( | ||
page_number, | ||
page_obj.paginator.num_pages, | ||
) | ||
return data | ||
|
||
|
||
class RequiresSuperuserMixin(UserPassesTestMixin): | ||
"""Only allow superusers to see this view.""" | ||
|
||
def test_func(self): | ||
return self.request.user.is_superuser | ||
|
||
|
||
class TrackedModelDetailMixin: | ||
"""Allows detail URLs to use <Identifying-Fields> instead of <pk>""" | ||
|
||
model: Type[TrackedModel] | ||
required_url_kwargs = None | ||
|
||
def get_object(self, queryset: Optional[QuerySet] = None) -> Model: | ||
""" | ||
Fetch the model instance by primary key or by identifying_fields in the | ||
URL. | ||
:param queryset Optional[QuerySet]: Get the object from this queryset | ||
:rtype: Model | ||
""" | ||
if queryset is None: | ||
queryset = self.get_queryset() | ||
|
||
required_url_kwargs = self.required_url_kwargs or self.model.identifying_fields | ||
|
||
if any(key not in self.kwargs for key in required_url_kwargs): | ||
raise AttributeError( | ||
f"{self.__class__.__name__} must be called with {', '.join(required_url_kwargs)} in the URLconf.", | ||
) | ||
|
||
queryset = queryset.filter(**self.kwargs) | ||
|
||
try: | ||
obj = queryset.get() | ||
except queryset.model.DoesNotExist: | ||
raise Http404(f"No {self.model.__name__} matching the query {self.kwargs}") | ||
|
||
return obj | ||
|
||
|
||
class BusinessRulesMixin: | ||
"""Check business rules on form_submission.""" | ||
|
||
validate_business_rules: Tuple[Type[BusinessRule], ...] = tuple() | ||
|
||
def form_violates(self, form, transaction=None) -> bool: | ||
""" | ||
If any of the specified business rules are violated, reshow the form | ||
with the violations as form errors. | ||
:param form: The submitted form | ||
:param transaction: The transaction containing the version of the object to be validated. Defaults to `self.object.transaction` | ||
""" | ||
violations = False | ||
transaction = transaction or self.object.transaction | ||
|
||
for rule in self.validate_business_rules: | ||
try: | ||
rule(transaction).validate(self.object) | ||
except BusinessRuleViolation as v: | ||
form.add_error(None, v.args[0]) | ||
violations = True | ||
|
||
return violations | ||
|
||
def form_valid(self, form): | ||
if self.form_violates(form): | ||
return self.form_invalid(form) | ||
|
||
return super().form_valid(form) | ||
|
||
|
||
class DescriptionDeleteMixin: | ||
"""Prevents the only description of the described object from being | ||
deleted.""" | ||
|
||
def form_valid(self, form): | ||
described_object = self.object.get_described_object() | ||
if described_object.get_descriptions().count() == 1: | ||
form.add_error( | ||
None, | ||
"This description cannot be deleted because at least one description record is mandatory.", | ||
) | ||
return self.form_invalid(form) | ||
return super().form_valid(form) | ||
|
||
|
||
class SortingMixin: | ||
""" | ||
Can be used to sort a queryset in a view using GET params. Checks the GET | ||
param against sort_by_fields to pass a valid field to .order_by(). If the | ||
GET param doesn't match the desired .order_by() field, a dictionary mapping | ||
can be added as custom_sorting. | ||
Example usage: | ||
class YourModelListView(SortingMixin, ListView): | ||
sort_by_fields = ["sid", "model", "valid_between"] | ||
custom_sorting = { | ||
"model": "model__polymorphic_ctype", | ||
} | ||
def get_queryset(self): | ||
self.queryset = YourModel.objects.all() | ||
return super().get_queryset() | ||
""" | ||
|
||
def get_ordering(self): | ||
sort_by = self.request.GET.get("sort_by") | ||
ordered = self.request.GET.get("ordered") | ||
assert hasattr( | ||
self, | ||
"sort_by_fields", | ||
), "SortingMixin requires class attribute sort_by_fields to be set" | ||
assert isinstance(self.sort_by_fields, list), "sort_by_fields must be a list" | ||
|
||
if sort_by and sort_by in self.sort_by_fields: | ||
if hasattr(self, "custom_sorting") and self.custom_sorting.get(sort_by): | ||
sort_by = self.custom_sorting.get(sort_by) | ||
|
||
if ordered == "desc": | ||
sort_by = f"-{sort_by}" | ||
|
||
return sort_by | ||
|
||
else: | ||
return None |
Oops, something went wrong.