From 4ae5b381980312121a42e87b5bf17dac33c65060 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 15 Nov 2021 17:09:17 +0100 Subject: [PATCH 01/13] Refactoring * renames model ResponsibilityData into Responsibility * renames model LegalData into Legal * moves form->object saving logic into model * refactors NewDocumentForm into special types for intervention, compensation, eco account and ema * --- compensation/forms/forms.py | 6 +- compensation/forms/modalForms.py | 104 ++++----------------- compensation/models.py | 112 ++++++++++++++++++++++- compensation/utils/quality.py | 2 +- compensation/views/compensation_views.py | 5 +- compensation/views/eco_account_views.py | 5 +- ema/forms.py | 12 ++- ema/tests/test_views.py | 4 +- ema/views.py | 4 +- intervention/admin.py | 6 +- intervention/forms/forms.py | 6 +- intervention/forms/modalForms.py | 10 +- intervention/models.py | 48 ++++++++-- intervention/utils/quality.py | 4 +- intervention/views.py | 4 +- konova/forms.py | 33 ++----- konova/tests/test_views.py | 10 +- 17 files changed, 220 insertions(+), 155 deletions(-) diff --git a/compensation/forms/forms.py b/compensation/forms/forms.py index 212efa63..0616f4ff 100644 --- a/compensation/forms/forms.py +++ b/compensation/forms/forms.py @@ -16,7 +16,7 @@ from codelist.models import KonovaCode from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID from compensation.models import Compensation, EcoAccount from intervention.inputs import GenerateInput -from intervention.models import Intervention, ResponsibilityData, LegalData +from intervention.models import Intervention, Responsibility, Legal from konova.forms import BaseForm, SimpleGeomForm from user.models import UserActionLogEntry, UserAction @@ -371,13 +371,13 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix # Process the geometry form geometry = geom_form.save(action) - responsible = ResponsibilityData.objects.create( + responsible = Responsibility.objects.create( handler=handler, conservation_file_number=conservation_file_number, conservation_office=conservation_office, ) - legal = LegalData.objects.create( + legal = Legal.objects.create( registration_date=registration_date ) diff --git a/compensation/forms/modalForms.py b/compensation/forms/modalForms.py index 2cc10643..999fc7ab 100644 --- a/compensation/forms/modalForms.py +++ b/compensation/forms/modalForms.py @@ -9,19 +9,17 @@ from bootstrap_modal_forms.utils import is_ajax from dal import autocomplete from django import forms from django.contrib import messages -from django.db import transaction from django.http import HttpRequest, HttpResponseRedirect from django.shortcuts import render from django.utils.translation import pgettext_lazy as _con, gettext_lazy as _ from codelist.models import KonovaCode from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_COMPENSATION_ACTION_ID -from compensation.models import Payment, CompensationState, UnitChoices, CompensationAction +from compensation.models import CompensationDocument, EcoAccountDocument from konova.contexts import BaseContext -from konova.forms import BaseModalForm -from konova.models import DeadlineType, Deadline +from konova.forms import BaseModalForm, NewDocumentForm +from konova.models import DeadlineType from konova.utils.message_templates import FORM_INVALID -from user.models import UserActionLogEntry, UserAction class NewPaymentForm(BaseModalForm): @@ -99,27 +97,8 @@ class NewPaymentForm(BaseModalForm): return super_valid def save(self): - with transaction.atomic(): - created_action = UserActionLogEntry.objects.create( - user=self.user, - action=UserAction.CREATED, - ) - edited_action = UserActionLogEntry.objects.create( - user=self.user, - action=UserAction.EDITED, - comment=_("Added payment"), - ) - pay = Payment.objects.create( - created=created_action, - amount=self.cleaned_data.get("amount", -1), - due_on=self.cleaned_data.get("due", None), - comment=self.cleaned_data.get("comment", None), - intervention=self.intervention, - ) - self.intervention.log.add(edited_action) - self.intervention.modified = edited_action - self.intervention.save() - return pay + pay = self.instance.add_payment(self) + return pay class NewStateModalForm(BaseModalForm): @@ -167,24 +146,7 @@ class NewStateModalForm(BaseModalForm): self.form_caption = _("Insert data for the new state") def save(self, is_before_state: bool = False): - with transaction.atomic(): - user_action = UserActionLogEntry.objects.create( - user=self.user, - action=UserAction.EDITED, - comment=_("Added state") - ) - self.instance.log.add(user_action) - self.instance.modified = user_action - self.instance.save() - - state = CompensationState.objects.create( - biotope_type=self.cleaned_data["biotope_type"], - surface=self.cleaned_data["surface"], - ) - if is_before_state: - self.instance.before_states.add(state) - else: - self.instance.after_states.add(state) + state = self.instance.add_state(self, is_before_state) return state def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None): @@ -287,26 +249,7 @@ class NewDeadlineModalForm(BaseModalForm): self.form_caption = _("Insert data for the new deadline") def save(self): - with transaction.atomic(): - created_action = UserActionLogEntry.objects.create( - user=self.user, - action=UserAction.CREATED - ) - deadline = Deadline.objects.create( - type=self.cleaned_data["type"], - date=self.cleaned_data["date"], - comment=self.cleaned_data["comment"], - created=created_action, - ) - edited_action = UserActionLogEntry.objects.create( - user=self.user, - action=UserAction.EDITED, - comment=_("Added deadline") - ) - self.instance.modified = edited_action - self.instance.save() - self.instance.log.add(edited_action) - self.instance.deadlines.add(deadline) + deadline = self.instance.add_new_deadline(self) return deadline @@ -318,6 +261,7 @@ class NewActionModalForm(BaseModalForm): (not in the process logic in Konova, but in the real world). """ + from compensation.models import UnitChoices action_type = forms.ModelChoiceField( label=_("Action Type"), label_suffix="", @@ -381,25 +325,13 @@ class NewActionModalForm(BaseModalForm): self.form_caption = _("Insert data for the new action") def save(self): - with transaction.atomic(): - user_action = UserActionLogEntry.objects.create( - user=self.user, - action=UserAction.CREATED, - ) - comp_action = CompensationAction.objects.create( - action_type=self.cleaned_data["action_type"], - amount=self.cleaned_data["amount"], - unit=self.cleaned_data["unit"], - comment=self.cleaned_data["comment"], - created=user_action, - ) - edited_action = UserActionLogEntry.objects.create( - user=self.user, - action=UserAction.EDITED, - comment=_("Added action"), - ) - self.instance.modified = edited_action - self.instance.save() - self.instance.log.add(edited_action) - self.instance.actions.add(comp_action) - return comp_action \ No newline at end of file + action = self.instance.add_new_action(self) + return action + + +class NewCompensationDocumentForm(NewDocumentForm): + document_model = CompensationDocument + + +class NewEcoAccountDocumentForm(NewDocumentForm): + document_model = EcoAccountDocument \ No newline at end of file diff --git a/compensation/models.py b/compensation/models.py index a0cf80de..9763fa7b 100644 --- a/compensation/models.py +++ b/compensation/models.py @@ -11,19 +11,20 @@ from django.contrib.auth.models import User from django.contrib.gis.db import models from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator +from django.db import transaction from django.db.models import Sum, QuerySet from django.utils.translation import gettext_lazy as _ from codelist.models import KonovaCode +from intervention.models import Intervention, Responsibility from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID from compensation.managers import CompensationStateManager, EcoAccountDeductionManager, CompensationActionManager, \ EcoAccountManager, CompensationManager from compensation.utils.quality import CompensationQualityChecker, EcoAccountQualityChecker -from intervention.models import Intervention, ResponsibilityData, LegalData from konova.models import BaseObject, BaseResource, Geometry, UuidModel, AbstractDocument, \ - generate_document_file_upload_path, RecordableObject, ShareableObject + generate_document_file_upload_path, RecordableObject, ShareableObject, Deadline from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE -from user.models import UserActionLogEntry +from user.models import UserActionLogEntry, UserAction class Payment(BaseResource): @@ -133,7 +134,7 @@ class AbstractCompensation(BaseObject): """ responsible = models.OneToOneField( - ResponsibilityData, + Responsibility, on_delete=models.SET_NULL, null=True, blank=True, @@ -151,6 +152,73 @@ class AbstractCompensation(BaseObject): class Meta: abstract = True + def add_new_deadline(self, form) -> Deadline: + """ Adds a new deadline to the abstract compensation + + Args: + form (NewDeadlineModalForm): The form holding all relevant data + + Returns: + + """ + form_data = form.cleaned_data + user = form.user + with transaction.atomic(): + created_action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.CREATED + ) + deadline = Deadline.objects.create( + type=form_data["type"], + date=form_data["date"], + comment=form_data["comment"], + created=created_action, + ) + edited_action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.EDITED, + comment=_("Added deadline") + ) + self.modified = edited_action + self.save() + self.log.add(edited_action) + self.deadlines.add(deadline) + return deadline + + def add_new_action(self, form) -> CompensationAction: + """ Adds a new action to the compensation + + Args: + form (NewActionModalForm): The form holding all relevant data + + Returns: + + """ + form_data = form.cleaned_data + user = form.user + with transaction.atomic(): + user_action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.CREATED, + ) + comp_action = CompensationAction.objects.create( + action_type=form_data["action_type"], + amount=form_data["amount"], + unit=form_data["unit"], + comment=form_data["comment"], + created=user_action, + ) + edited_action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.EDITED, + comment=_("Added action"), + ) + self.modified = edited_action + self.save() + self.log.add(edited_action) + self.actions.add(comp_action) + return comp_action + def get_surface_after_states(self) -> float: """ Calculates the compensation's/account's surface @@ -293,6 +361,38 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin): ) return docs + def add_state(self, form, is_before_state: bool) -> CompensationState: + """ Adds a new compensation state to the compensation + + Args: + form (NewStateModalForm): The form, holding all relevant data + is_before_state (bool): Whether this is a new before_state or after_state + + Returns: + + """ + form_data = form.cleaned_data + user = form.user + with transaction.atomic(): + user_action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.EDITED, + comment=_("Added state") + ) + self.log.add(user_action) + self.modified = user_action + self.save() + + state = CompensationState.objects.create( + biotope_type=form_data["biotope_type"], + surface=form_data["surface"], + ) + if is_before_state: + self.before_states.add(state) + else: + self.after_states.add(state) + return state + class CompensationDocument(AbstractDocument): """ @@ -347,6 +447,7 @@ class EcoAccount(AbstractCompensation, ShareableObject, RecordableObject): An eco account is a kind of 'prepaid' compensation. It can be compared to an account that already has been filled with some kind of currency. From this account one is able to deduct currency for current projects. """ + from intervention.models import Legal deductable_surface = models.FloatField( blank=True, null=True, @@ -355,7 +456,7 @@ class EcoAccount(AbstractCompensation, ShareableObject, RecordableObject): ) legal = models.OneToOneField( - LegalData, + Legal, on_delete=models.SET_NULL, null=True, blank=True, @@ -537,6 +638,7 @@ class EcoAccountDeduction(BaseResource): """ A deduction object for eco accounts """ + from intervention.models import Intervention account = models.ForeignKey( EcoAccount, on_delete=models.SET_NULL, diff --git a/compensation/utils/quality.py b/compensation/utils/quality.py index 28002db4..b622fcdd 100644 --- a/compensation/utils/quality.py +++ b/compensation/utils/quality.py @@ -61,7 +61,7 @@ class EcoAccountQualityChecker(CompensationQualityChecker): super().run_check() def _check_legal_data(self): - """ Checks the data quality for LegalData + """ Checks the data quality for Legal Returns: diff --git a/compensation/views/compensation_views.py b/compensation/views/compensation_views.py index 02bdde01..a2a57b67 100644 --- a/compensation/views/compensation_views.py +++ b/compensation/views/compensation_views.py @@ -5,7 +5,8 @@ from django.shortcuts import render from django.utils.translation import gettext_lazy as _ from compensation.forms.forms import NewCompensationForm, EditCompensationForm -from compensation.forms.modalForms import NewStateModalForm, NewDeadlineModalForm, NewActionModalForm +from compensation.forms.modalForms import NewStateModalForm, NewDeadlineModalForm, NewActionModalForm, \ + NewCompensationDocumentForm from compensation.models import Compensation, CompensationState, CompensationAction, CompensationDocument from compensation.tables import CompensationTable from intervention.models import Intervention @@ -258,7 +259,7 @@ def new_document_view(request: HttpRequest, id: str): """ comp = get_object_or_404(Compensation, id=id) - form = NewDocumentForm(request.POST or None, request.FILES or None, instance=comp, user=request.user) + form = NewCompensationDocumentForm(request.POST or None, request.FILES or None, instance=comp, user=request.user) return form.process_request( request, msg_success=_("Document added") diff --git a/compensation/views/eco_account_views.py b/compensation/views/eco_account_views.py index 521963fc..2c5340ab 100644 --- a/compensation/views/eco_account_views.py +++ b/compensation/views/eco_account_views.py @@ -15,7 +15,8 @@ from django.http import HttpRequest, Http404, JsonResponse from django.shortcuts import render, get_object_or_404, redirect from compensation.forms.forms import NewEcoAccountForm, EditEcoAccountForm -from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm +from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm, \ + NewEcoAccountDocumentForm from compensation.models import EcoAccount, EcoAccountDocument, CompensationState, CompensationAction from compensation.tables import EcoAccountTable from intervention.forms.modalForms import NewDeductionModalForm, ShareInterventionModalForm @@ -453,7 +454,7 @@ def new_document_view(request: HttpRequest, id: str): """ acc = get_object_or_404(EcoAccount, id=id) - form = NewDocumentForm(request.POST or None, request.FILES or None, instance=acc, user=request.user) + form = NewEcoAccountDocumentForm(request.POST or None, request.FILES or None, instance=acc, user=request.user) return form.process_request( request, msg_success=_("Document added") diff --git a/ema/forms.py b/ema/forms.py index 85a9094b..f0f15163 100644 --- a/ema/forms.py +++ b/ema/forms.py @@ -13,9 +13,9 @@ from django.urls import reverse, reverse_lazy from django.utils.translation import gettext_lazy as _ from compensation.forms.forms import AbstractCompensationForm, CompensationResponsibleFormMixin -from ema.models import Ema -from intervention.models import ResponsibilityData -from konova.forms import SimpleGeomForm +from ema.models import Ema, EmaDocument +from intervention.models import Responsibility +from konova.forms import SimpleGeomForm, NewDocumentForm from user.models import UserActionLogEntry, UserAction @@ -66,7 +66,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin): # Process the geometry form geometry = geom_form.save(action) - responsible = ResponsibilityData.objects.create( + responsible = Responsibility.objects.create( handler=handler, conservation_file_number=conservation_file_number, conservation_office=conservation_office, @@ -154,3 +154,7 @@ class EditEmaForm(NewEmaForm): # Add the log entry to the main objects log list self.instance.log.add(action) return self.instance + + +class NewEmaDocumentForm(NewDocumentForm): + document_model = EmaDocument \ No newline at end of file diff --git a/ema/tests/test_views.py b/ema/tests/test_views.py index 07d761c8..45092883 100644 --- a/ema/tests/test_views.py +++ b/ema/tests/test_views.py @@ -11,7 +11,7 @@ from django.test.client import Client from compensation.tests.test_views import CompensationViewTestCase from ema.models import Ema -from intervention.models import ResponsibilityData +from intervention.models import Responsibility from konova.models import Geometry from konova.settings import DEFAULT_GROUP, ETS_GROUP from user.models import UserActionLogEntry, UserAction @@ -66,7 +66,7 @@ class EmaViewTestCase(CompensationViewTestCase): action=UserAction.CREATED, ) # Create responsible data object - responsibility_data = ResponsibilityData.objects.create() + responsibility_data = Responsibility.objects.create() geometry = Geometry.objects.create() cls.ema = Ema.objects.create( identifier="TEST", diff --git a/ema/views.py b/ema/views.py index 496c55b1..783dc7c7 100644 --- a/ema/views.py +++ b/ema/views.py @@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _ from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm from compensation.models import CompensationAction, CompensationState -from ema.forms import NewEmaForm, EditEmaForm +from ema.forms import NewEmaForm, EditEmaForm, NewEmaDocumentForm from ema.tables import EmaTable from intervention.forms.modalForms import ShareInterventionModalForm from konova.contexts import BaseContext @@ -343,7 +343,7 @@ def document_new_view(request: HttpRequest, id: str): """ ema = get_object_or_404(Ema, id=id) - form = NewDocumentForm(request.POST or None, request.FILES or None, instance=ema, user=request.user) + form = NewEmaDocumentForm(request.POST or None, request.FILES or None, instance=ema, user=request.user) return form.process_request( request, msg_success=_("Document added") diff --git a/intervention/admin.py b/intervention/admin.py index de11ee27..f65cb332 100644 --- a/intervention/admin.py +++ b/intervention/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from intervention.models import Intervention, ResponsibilityData, LegalData, Revocation, InterventionDocument +from intervention.models import Intervention, Responsibility, Legal, Revocation, InterventionDocument from konova.admin import AbstractDocumentAdmin @@ -46,7 +46,7 @@ class RevocationAdmin(admin.ModelAdmin): admin.site.register(Intervention, InterventionAdmin) -admin.site.register(ResponsibilityData, ResponsibilityAdmin) -admin.site.register(LegalData, LegalAdmin) +admin.site.register(Responsibility, ResponsibilityAdmin) +admin.site.register(Legal, LegalAdmin) admin.site.register(Revocation, RevocationAdmin) admin.site.register(InterventionDocument, InterventionDocumentAdmin) diff --git a/intervention/forms/forms.py b/intervention/forms/forms.py index f7453ccd..d8f88979 100644 --- a/intervention/forms/forms.py +++ b/intervention/forms/forms.py @@ -17,7 +17,7 @@ from codelist.models import KonovaCode from codelist.settings import CODELIST_PROCESS_TYPE_ID, CODELIST_LAW_ID, \ CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID from intervention.inputs import GenerateInput -from intervention.models import Intervention, LegalData, ResponsibilityData +from intervention.models import Intervention, Legal, Responsibility from konova.forms import BaseForm, SimpleGeomForm from user.models import UserActionLogEntry, UserAction @@ -220,7 +220,7 @@ class NewInterventionForm(BaseForm): ) # Create legal data object (without M2M laws first) - legal_data = LegalData.objects.create( + legal_data = Legal.objects.create( registration_date=registration_date, binding_date=binding_date, process_type=_type, @@ -229,7 +229,7 @@ class NewInterventionForm(BaseForm): legal_data.laws.set(laws) # Create responsible data object - responsibility_data = ResponsibilityData.objects.create( + responsibility_data = Responsibility.objects.create( registration_office=registration_office, conservation_office=conservation_office, registration_file_number=registration_file_number, diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py index fb6ee083..ef6fdf9e 100644 --- a/intervention/forms/modalForms.py +++ b/intervention/forms/modalForms.py @@ -14,8 +14,8 @@ from django.utils.translation import gettext_lazy as _ from compensation.models import EcoAccount, EcoAccountDeduction from intervention.inputs import TextToClipboardInput -from intervention.models import Revocation, RevocationDocument, Intervention -from konova.forms import BaseModalForm +from intervention.models import Revocation, RevocationDocument, Intervention, InterventionDocument +from konova.forms import BaseModalForm, NewDocumentForm from konova.utils.general import format_german_float from konova.utils.messenger import Messenger from konova.utils.user_checks import is_default_group_only @@ -409,4 +409,8 @@ class NewDeductionModalForm(BaseModalForm): surface=self.cleaned_data["surface"], created=user_action_create, ) - return deduction \ No newline at end of file + return deduction + + +class NewInterventionDocumentForm(NewDocumentForm): + document_model = InterventionDocument diff --git a/intervention/models.py b/intervention/models.py index 6043e422..d025cddd 100644 --- a/intervention/models.py +++ b/intervention/models.py @@ -9,7 +9,9 @@ import shutil from django.contrib.auth.models import User from django.contrib.gis.db import models +from django.db import transaction from django.db.models import QuerySet +from django.utils.translation import gettext_lazy as _ from codelist.models import KonovaCode from codelist.settings import CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_LAW_ID, \ @@ -19,10 +21,10 @@ from intervention.utils.quality import InterventionQualityChecker from konova.models import BaseObject, Geometry, UuidModel, BaseResource, AbstractDocument, \ generate_document_file_upload_path, RecordableObject, CheckableObject, ShareableObject from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT -from user.models import UserActionLogEntry +from user.models import UserActionLogEntry, UserAction -class ResponsibilityData(UuidModel): +class Responsibility(UuidModel): """ Holds intervention data about responsible organizations and their file numbers for this case @@ -68,7 +70,7 @@ class Revocation(BaseResource): Holds revocation data e.g. for intervention objects """ date = models.DateField(null=True, blank=True, help_text="Revocation from") - legal = models.ForeignKey("LegalData", null=False, blank=False, on_delete=models.CASCADE, help_text="Refers to 'Widerspruch am'", related_name="revocations") + legal = models.ForeignKey("Legal", null=False, blank=False, on_delete=models.CASCADE, help_text="Refers to 'Widerspruch am'", related_name="revocations") comment = models.TextField(null=True, blank=True) def delete(self, *args, **kwargs): @@ -137,7 +139,7 @@ class RevocationDocument(AbstractDocument): pass -class LegalData(UuidModel): +class Legal(UuidModel): """ Holds intervention legal data such as important dates, laws or responsible handler """ @@ -175,14 +177,14 @@ class Intervention(BaseObject, ShareableObject, RecordableObject, CheckableObjec Interventions are e.g. construction sites where nature used to be. """ responsible = models.OneToOneField( - ResponsibilityData, + Responsibility, on_delete=models.SET_NULL, null=True, blank=True, help_text="Holds data on responsible organizations ('Zulassungsbehörde', 'Eintragungsstelle')" ) legal = models.OneToOneField( - LegalData, + Legal, on_delete=models.SET_NULL, null=True, blank=True, @@ -322,6 +324,40 @@ class Intervention(BaseObject, ShareableObject, RecordableObject, CheckableObjec for comp in comps: comp.log.add(log_entry) + def add_payment(self, form): + """ Adds a new payment to the intervention + + Args: + form (NewPaymentForm): The form holding the data + + Returns: + + """ + from compensation.models import Payment + form_data = form.cleaned_data + user = form.user + with transaction.atomic(): + created_action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.CREATED, + ) + edited_action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.EDITED, + comment=_("Added payment"), + ) + pay = Payment.objects.create( + created=created_action, + amount=form_data.get("amount", -1), + due_on=form_data.get("due", None), + comment=form_data.get("comment", None), + intervention=self, + ) + self.log.add(edited_action) + self.modified = edited_action + self.save() + return pay + class InterventionDocument(AbstractDocument): """ diff --git a/intervention/utils/quality.py b/intervention/utils/quality.py index b4717b18..e15b4ca4 100644 --- a/intervention/utils/quality.py +++ b/intervention/utils/quality.py @@ -24,7 +24,7 @@ class InterventionQualityChecker(AbstractQualityChecker): self.valid = len(self.messages) == 0 def _check_responsible_data(self): - """ Checks data quality of related ResponsibilityData + """ Checks data quality of related Responsibility Args: self.messages (dict): Holds error messages @@ -55,7 +55,7 @@ class InterventionQualityChecker(AbstractQualityChecker): self._add_missing_attr_name(_("Responsible data")) def _check_legal_data(self): - """ Checks data quality of related LegalData + """ Checks data quality of related Legal Args: self.messages (dict): Holds error messages diff --git a/intervention/views.py b/intervention/views.py index c2a87d6e..f79ae5f4 100644 --- a/intervention/views.py +++ b/intervention/views.py @@ -5,7 +5,7 @@ from django.shortcuts import render from intervention.forms.forms import NewInterventionForm, EditInterventionForm from intervention.forms.modalForms import ShareInterventionModalForm, NewRevocationModalForm, \ - CheckModalForm, NewDeductionModalForm + CheckModalForm, NewDeductionModalForm, NewInterventionDocumentForm from intervention.models import Intervention, Revocation, InterventionDocument, RevocationDocument from intervention.tables import InterventionTable from konova.contexts import BaseContext @@ -123,7 +123,7 @@ def new_document_view(request: HttpRequest, id: str): """ intervention = get_object_or_404(Intervention, id=id) - form = NewDocumentForm(request.POST or None, request.FILES or None, instance=intervention, user=request.user) + form = NewInterventionDocumentForm(request.POST or None, request.FILES or None, instance=intervention, user=request.user) return form.process_request( request, msg_success=_("Document added") diff --git a/konova/forms.py b/konova/forms.py index 82d46c8a..3087c5fc 100644 --- a/konova/forms.py +++ b/konova/forms.py @@ -21,11 +21,8 @@ from django.shortcuts import render from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from compensation.models import EcoAccount, Compensation, EcoAccountDocument, CompensationDocument -from ema.models import Ema, EmaDocument -from intervention.models import Intervention, Revocation, RevocationDocument, InterventionDocument from konova.contexts import BaseContext -from konova.models import BaseObject, Geometry +from konova.models import BaseObject, Geometry, RecordableObject from konova.settings import DEFAULT_SRID from konova.utils.message_templates import FORM_INVALID from user.models import UserActionLogEntry, UserAction @@ -382,13 +379,10 @@ class NewDocumentForm(BaseModalForm): } ) ) - document_instance_map = { - Intervention: InterventionDocument, - Compensation: CompensationDocument, - EcoAccount: EcoAccountDocument, - Revocation: RevocationDocument, - Ema: EmaDocument, - } + document_model = None + + class Meta: + abstract = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -398,11 +392,7 @@ class NewDocumentForm(BaseModalForm): self.form_attrs = { "enctype": "multipart/form-data", # important for file upload } - self.document_type = self.document_instance_map.get( - self.instance.__class__, - None - ) - if not self.document_type: + if not self.document_model: raise NotImplementedError("Unsupported document type for {}".format(self.instance.__class__)) def save(self): @@ -411,7 +401,7 @@ class NewDocumentForm(BaseModalForm): user=self.user, action=UserAction.CREATED, ) - doc = self.document_type.objects.create( + doc = self.document_model.objects.create( created=action, title=self.cleaned_data["title"], comment=self.cleaned_data["comment"], @@ -456,13 +446,7 @@ class RecordModalForm(BaseModalForm): self.form_title = _("Unrecord data") self.form_caption = _("I, {} {}, confirm that this data must be unrecorded.").format(self.user.first_name, self.user.last_name) - implemented_cls_logic = { - Intervention, - EcoAccount, - Ema, - } - instance_name = self.instance.__class__ - if instance_name not in implemented_cls_logic: + if not isinstance(self.instance, RecordableObject): raise NotImplementedError def is_valid(self): @@ -471,6 +455,7 @@ class RecordModalForm(BaseModalForm): Returns: """ + from intervention.models import Intervention super_val = super().is_valid() if self.instance.recorded: # If user wants to unrecord an already recorded dataset, we do not need to perform custom checks diff --git a/konova/tests/test_views.py b/konova/tests/test_views.py index 682a43f1..b3880623 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -15,7 +15,7 @@ from django.urls import reverse from codelist.models import KonovaCode from compensation.models import Compensation, CompensationState, CompensationAction, EcoAccount -from intervention.models import LegalData, ResponsibilityData, Intervention +from intervention.models import Legal, Responsibility, Intervention from konova.management.commands.setup_data import GROUPS_DATA from konova.models import Geometry from konova.settings import DEFAULT_GROUP @@ -103,9 +103,9 @@ class BaseTestCase(TestCase): action=UserAction.CREATED, ) # Create legal data object (without M2M laws first) - legal_data = LegalData.objects.create() + legal_data = Legal.objects.create() # Create responsible data object - responsibility_data = ResponsibilityData.objects.create() + responsibility_data = Responsibility.objects.create() geometry = Geometry.objects.create() # Finally create main object, holding the other objects intervention = Intervention.objects.create( @@ -162,8 +162,8 @@ class BaseTestCase(TestCase): ) geometry = Geometry.objects.create() # Create responsible data object - lega_data = LegalData.objects.create() - responsible_data = ResponsibilityData.objects.create() + lega_data = Legal.objects.create() + responsible_data = Responsibility.objects.create() # Finally create main object, holding the other objects eco_account = EcoAccount.objects.create( identifier="TEST", From 26ae6bc96b0fe9129edb09d4513ecbebefb6504a Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 15 Nov 2021 17:19:06 +0100 Subject: [PATCH 02/13] Refactoring * splits intervention/models.py into subpackage --- intervention/models/__init__.py | 12 ++ .../{models.py => models/intervention.py} | 169 +----------------- intervention/models/legal.py | 46 +++++ intervention/models/responsibility.py | 53 ++++++ intervention/models/revocation.py | 87 +++++++++ 5 files changed, 207 insertions(+), 160 deletions(-) create mode 100644 intervention/models/__init__.py rename intervention/{models.py => models/intervention.py} (59%) create mode 100644 intervention/models/legal.py create mode 100644 intervention/models/responsibility.py create mode 100644 intervention/models/revocation.py diff --git a/intervention/models/__init__.py b/intervention/models/__init__.py new file mode 100644 index 00000000..97b96910 --- /dev/null +++ b/intervention/models/__init__.py @@ -0,0 +1,12 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 15.11.21 + +""" + +from .intervention import * +from .legal import * +from .revocation import * +from .responsibility import * diff --git a/intervention/models.py b/intervention/models/intervention.py similarity index 59% rename from intervention/models.py rename to intervention/models/intervention.py index d025cddd..9b69764e 100644 --- a/intervention/models.py +++ b/intervention/models/intervention.py @@ -2,174 +2,23 @@ Author: Michel Peltriaux Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 17.11.20 +Created on: 15.11.21 """ -import shutil - from django.contrib.auth.models import User -from django.contrib.gis.db import models -from django.db import transaction +from django.db import models, transaction from django.db.models import QuerySet from django.utils.translation import gettext_lazy as _ -from codelist.models import KonovaCode -from codelist.settings import CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_LAW_ID, \ - CODELIST_PROCESS_TYPE_ID from intervention.managers import InterventionManager +from intervention.models.legal import Legal +from intervention.models.responsibility import Responsibility +from intervention.models.revocation import RevocationDocument from intervention.utils.quality import InterventionQualityChecker -from konova.models import BaseObject, Geometry, UuidModel, BaseResource, AbstractDocument, \ - generate_document_file_upload_path, RecordableObject, CheckableObject, ShareableObject -from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT -from user.models import UserActionLogEntry, UserAction - - -class Responsibility(UuidModel): - """ - Holds intervention data about responsible organizations and their file numbers for this case - - """ - registration_office = models.ForeignKey( - KonovaCode, - on_delete=models.SET_NULL, - null=True, - related_name="+", - blank=True, - limit_choices_to={ - "code_lists__in": [CODELIST_REGISTRATION_OFFICE_ID], - "is_selectable": True, - "is_archived": False, - } - ) - registration_file_number = models.CharField(max_length=1000, blank=True, null=True) - conservation_office = models.ForeignKey( - KonovaCode, - on_delete=models.SET_NULL, - null=True, - related_name="+", - blank=True, - limit_choices_to={ - "code_lists__in": [CODELIST_CONSERVATION_OFFICE_ID], - "is_selectable": True, - "is_archived": False, - } - ) - conservation_file_number = models.CharField(max_length=1000, blank=True, null=True) - handler = models.CharField(max_length=500, null=True, blank=True, help_text="Refers to 'Eingriffsverursacher' or 'Maßnahmenträger'") - - def __str__(self): - return "ZB: {} | ETS: {} | Handler: {}".format( - self.registration_office, - self.conservation_office, - self.handler - ) - - -class Revocation(BaseResource): - """ - Holds revocation data e.g. for intervention objects - """ - date = models.DateField(null=True, blank=True, help_text="Revocation from") - legal = models.ForeignKey("Legal", null=False, blank=False, on_delete=models.CASCADE, help_text="Refers to 'Widerspruch am'", related_name="revocations") - comment = models.TextField(null=True, blank=True) - - def delete(self, *args, **kwargs): - # Make sure related objects are being removed as well - if self.document: - self.document.delete(*args, **kwargs) - super().delete() - - -class RevocationDocument(AbstractDocument): - """ - Specializes document upload for revocations with certain path - """ - instance = models.OneToOneField( - Revocation, - on_delete=models.CASCADE, - related_name="document", - ) - file = models.FileField( - upload_to=generate_document_file_upload_path, - max_length=1000, - ) - - @property - def intervention(self): - """ - Shortcut for opening the related intervention - - Returns: - intervention (Intervention) - """ - return self.instance.legal.intervention - - def delete(self, *args, **kwargs): - """ - Custom delete functionality for RevocationDocuments. - Removes the folder from the file system if there are no further documents for this entry. - - Args: - *args (): - **kwargs (): - - Returns: - - """ - revoc_docs, other_intervention_docs = self.intervention.get_documents() - - # Remove the file itself - super().delete(*args, **kwargs) - - # Always remove 'revocation' folder if the one revocation we just processed is the only one left - folder_path = self.file.path.split("/") - if revoc_docs.count() == 0: - try: - shutil.rmtree("/".join(folder_path[:-1])) - except FileNotFoundError: - # Revocation subfolder seems to be missing already - pass - - if other_intervention_docs.count() == 0: - # If there are no further documents for the intervention, we can simply remove the whole folder as well! - try: - shutil.rmtree("/".join(folder_path[:-2])) - except FileNotFoundError: - # Folder seems to be missing already - pass - - -class Legal(UuidModel): - """ - Holds intervention legal data such as important dates, laws or responsible handler - """ - # Refers to "zugelassen am" - registration_date = models.DateField(null=True, blank=True, help_text="Refers to 'Zugelassen am'") - - # Refers to "Bestandskraft am" - binding_date = models.DateField(null=True, blank=True, help_text="Refers to 'Bestandskraft am'") - - process_type = models.ForeignKey( - KonovaCode, - on_delete=models.SET_NULL, - null=True, - related_name="+", - blank=True, - limit_choices_to={ - "code_lists__in": [CODELIST_PROCESS_TYPE_ID], - "is_selectable": True, - "is_archived": False, - } - ) - laws = models.ManyToManyField( - KonovaCode, - blank=True, - limit_choices_to={ - "code_lists__in": [CODELIST_LAW_ID], - "is_selectable": True, - "is_archived": False, - } - ) +from konova.models import generate_document_file_upload_path, AbstractDocument, Geometry, BaseObject, ShareableObject, \ + RecordableObject, CheckableObject +from konova.settings import LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT, DEFAULT_SRID_RLP +from user.models import UserAction, UserActionLogEntry class Intervention(BaseObject, ShareableObject, RecordableObject, CheckableObject): diff --git a/intervention/models/legal.py b/intervention/models/legal.py new file mode 100644 index 00000000..032bb015 --- /dev/null +++ b/intervention/models/legal.py @@ -0,0 +1,46 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 15.11.21 + +""" +from django.db import models + +from codelist.models import KonovaCode +from codelist.settings import CODELIST_LAW_ID, CODELIST_PROCESS_TYPE_ID +from konova.models import UuidModel + + +class Legal(UuidModel): + """ + Holds intervention legal data such as important dates, laws or responsible handler + """ + # Refers to "zugelassen am" + registration_date = models.DateField(null=True, blank=True, help_text="Refers to 'Zugelassen am'") + + # Refers to "Bestandskraft am" + binding_date = models.DateField(null=True, blank=True, help_text="Refers to 'Bestandskraft am'") + + process_type = models.ForeignKey( + KonovaCode, + on_delete=models.SET_NULL, + null=True, + related_name="+", + blank=True, + limit_choices_to={ + "code_lists__in": [CODELIST_PROCESS_TYPE_ID], + "is_selectable": True, + "is_archived": False, + } + ) + laws = models.ManyToManyField( + KonovaCode, + blank=True, + limit_choices_to={ + "code_lists__in": [CODELIST_LAW_ID], + "is_selectable": True, + "is_archived": False, + } + ) + diff --git a/intervention/models/responsibility.py b/intervention/models/responsibility.py new file mode 100644 index 00000000..19234acd --- /dev/null +++ b/intervention/models/responsibility.py @@ -0,0 +1,53 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 15.11.21 + +""" +from django.db import models + +from codelist.models import KonovaCode +from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_REGISTRATION_OFFICE_ID +from konova.models import UuidModel + + +class Responsibility(UuidModel): + """ + Holds intervention data about responsible organizations and their file numbers for this case + + """ + registration_office = models.ForeignKey( + KonovaCode, + on_delete=models.SET_NULL, + null=True, + related_name="+", + blank=True, + limit_choices_to={ + "code_lists__in": [CODELIST_REGISTRATION_OFFICE_ID], + "is_selectable": True, + "is_archived": False, + } + ) + registration_file_number = models.CharField(max_length=1000, blank=True, null=True) + conservation_office = models.ForeignKey( + KonovaCode, + on_delete=models.SET_NULL, + null=True, + related_name="+", + blank=True, + limit_choices_to={ + "code_lists__in": [CODELIST_CONSERVATION_OFFICE_ID], + "is_selectable": True, + "is_archived": False, + } + ) + conservation_file_number = models.CharField(max_length=1000, blank=True, null=True) + handler = models.CharField(max_length=500, null=True, blank=True, help_text="Refers to 'Eingriffsverursacher' or 'Maßnahmenträger'") + + def __str__(self): + return "ZB: {} | ETS: {} | Handler: {}".format( + self.registration_office, + self.conservation_office, + self.handler + ) diff --git a/intervention/models/revocation.py b/intervention/models/revocation.py new file mode 100644 index 00000000..5eb70e5e --- /dev/null +++ b/intervention/models/revocation.py @@ -0,0 +1,87 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 15.11.21 + +""" + +import shutil + +from django.contrib.gis.db import models +from konova.models import BaseResource, AbstractDocument, generate_document_file_upload_path + + +class Revocation(BaseResource): + """ + Holds revocation data e.g. for intervention objects + """ + date = models.DateField(null=True, blank=True, help_text="Revocation from") + legal = models.ForeignKey("Legal", null=False, blank=False, on_delete=models.CASCADE, help_text="Refers to 'Widerspruch am'", related_name="revocations") + comment = models.TextField(null=True, blank=True) + + def delete(self, *args, **kwargs): + # Make sure related objects are being removed as well + if self.document: + self.document.delete(*args, **kwargs) + super().delete() + + +class RevocationDocument(AbstractDocument): + """ + Specializes document upload for revocations with certain path + """ + instance = models.OneToOneField( + Revocation, + on_delete=models.CASCADE, + related_name="document", + ) + file = models.FileField( + upload_to=generate_document_file_upload_path, + max_length=1000, + ) + + @property + def intervention(self): + """ + Shortcut for opening the related intervention + + Returns: + intervention (Intervention) + """ + return self.instance.legal.intervention + + def delete(self, *args, **kwargs): + """ + Custom delete functionality for RevocationDocuments. + Removes the folder from the file system if there are no further documents for this entry. + + Args: + *args (): + **kwargs (): + + Returns: + + """ + revoc_docs, other_intervention_docs = self.intervention.get_documents() + + # Remove the file itself + super().delete(*args, **kwargs) + + # Always remove 'revocation' folder if the one revocation we just processed is the only one left + folder_path = self.file.path.split("/") + if revoc_docs.count() == 0: + try: + shutil.rmtree("/".join(folder_path[:-1])) + except FileNotFoundError: + # Revocation subfolder seems to be missing already + pass + + if other_intervention_docs.count() == 0: + # If there are no further documents for the intervention, we can simply remove the whole folder as well! + try: + shutil.rmtree("/".join(folder_path[:-2])) + except FileNotFoundError: + # Folder seems to be missing already + pass + From 65f02c51117edb008b458a04e5e68ac1f9f20297 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 15 Nov 2021 17:41:52 +0100 Subject: [PATCH 03/13] Refactoring * splits ema/models.py into subpackage * splits konova/models.py into subpackage * splits user/models.py into subpackage --- ema/models/__init__.py | 9 ++ ema/{models.py => models/ema.py} | 7 ++ konova/models/__init__.py | 11 ++ konova/models/deadline.py | 49 +++++++++ konova/models/document.py | 84 +++++++++++++++ konova/models/geometry.py | 18 ++++ konova/{models.py => models/object.py} | 126 +--------------------- user/models/__init__.py | 10 ++ user/models/konova_user.py | 19 ++++ user/models/notification.py | 34 ++++++ user/{models.py => models/user_action.py} | 45 ++------ 11 files changed, 252 insertions(+), 160 deletions(-) create mode 100644 ema/models/__init__.py rename ema/{models.py => models/ema.py} (96%) create mode 100644 konova/models/__init__.py create mode 100644 konova/models/deadline.py create mode 100644 konova/models/document.py create mode 100644 konova/models/geometry.py rename konova/{models.py => models/object.py} (75%) create mode 100644 user/models/__init__.py create mode 100644 user/models/konova_user.py create mode 100644 user/models/notification.py rename user/{models.py => models/user_action.py} (65%) diff --git a/ema/models/__init__.py b/ema/models/__init__.py new file mode 100644 index 00000000..77fe9d7b --- /dev/null +++ b/ema/models/__init__.py @@ -0,0 +1,9 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 15.11.21 + +""" + +from .ema import * diff --git a/ema/models.py b/ema/models/ema.py similarity index 96% rename from ema/models.py rename to ema/models/ema.py index b35cda32..5c7e9fce 100644 --- a/ema/models.py +++ b/ema/models/ema.py @@ -1,3 +1,10 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 15.11.21 + +""" import shutil from django.db import models diff --git a/konova/models/__init__.py b/konova/models/__init__.py new file mode 100644 index 00000000..c60ecaa9 --- /dev/null +++ b/konova/models/__init__.py @@ -0,0 +1,11 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 15.11.21 + +""" +from .object import * +from .deadline import * +from .document import * +from .geometry import * diff --git a/konova/models/deadline.py b/konova/models/deadline.py new file mode 100644 index 00000000..7cad8a65 --- /dev/null +++ b/konova/models/deadline.py @@ -0,0 +1,49 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 15.11.21 + +""" +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from konova.models import BaseResource + + +class DeadlineType(models.TextChoices): + """ + Django 3.x way of handling enums for models + """ + FINISHED = "finished", _("Finished") + MAINTAIN = "maintain", _("Maintain") + CONTROL = "control", _("Control") + OTHER = "other", _("Other") + + +class Deadline(BaseResource): + """ + Defines a deadline, which can be used to define dates with a semantic meaning + """ + + type = models.CharField(max_length=255, null=True, blank=True, choices=DeadlineType.choices) + date = models.DateField(null=True, blank=True) + comment = models.TextField(null=True, blank=True) + + def __str__(self): + return self.type + + @property + def type_humanized(self): + """ Returns humanized version of enum + + Used for template rendering + + Returns: + + """ + choices = DeadlineType.choices + for choice in choices: + if choice[0] == self.type: + return choice[1] + return None diff --git a/konova/models/document.py b/konova/models/document.py new file mode 100644 index 00000000..11b6c06d --- /dev/null +++ b/konova/models/document.py @@ -0,0 +1,84 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 15.11.21 + +""" +import os + +from django.db import models + +from konova.models import BaseResource +from konova.settings import INTERVENTION_REVOCATION_DOC_PATH + + +def generate_document_file_upload_path(instance, filename): + """ Generates the file upload path for certain document instances + + Documents derived from AbstractDocument need specific upload paths for their related models. + + Args: + instance (): The document instance + filename (): The filename + + Returns: + + """ + from compensation.models import CompensationDocument, EcoAccountDocument + from ema.models import EmaDocument + from intervention.models import InterventionDocument, RevocationDocument + from konova.settings import ECO_ACCOUNT_DOC_PATH, EMA_DOC_PATH, \ + COMPENSATION_DOC_PATH, \ + INTERVENTION_DOC_PATH + + # Map document types to paths on the hard drive + path_map = { + InterventionDocument: INTERVENTION_DOC_PATH, + CompensationDocument: COMPENSATION_DOC_PATH, + EmaDocument: EMA_DOC_PATH, + RevocationDocument: INTERVENTION_REVOCATION_DOC_PATH, + EcoAccountDocument: ECO_ACCOUNT_DOC_PATH, + } + path = path_map.get(instance.__class__, None) + if path is None: + raise NotImplementedError("Unidentified document type: {}".format(instance.__class__)) + + # RevocationDocument needs special treatment, since these files need to be stored in a subfolder of the related + # instance's (Revocation) legaldata interventions folder + if instance.__class__ is RevocationDocument: + path = path.format(instance.intervention.id) + else: + path = path.format(instance.instance.id) + return path + filename + + +class AbstractDocument(BaseResource): + """ + Documents can be attached to compensation or intervention for uploading legal documents or pictures. + """ + title = models.CharField(max_length=500, null=True, blank=True) + date_of_creation = models.DateField() + file = models.FileField() + comment = models.TextField() + + class Meta: + abstract = True + + def delete(self, using=None, keep_parents=False): + """ Custom delete function to remove the real file from the hard drive + + Args: + using (): + keep_parents (): + + Returns: + + """ + try: + os.remove(self.file.file.name) + except FileNotFoundError: + # File seems to missing anyway - continue! + pass + super().delete(using=using, keep_parents=keep_parents) + diff --git a/konova/models/geometry.py b/konova/models/geometry.py new file mode 100644 index 00000000..736e1b3b --- /dev/null +++ b/konova/models/geometry.py @@ -0,0 +1,18 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 15.11.21 + +""" +from django.contrib.gis.db.models import MultiPolygonField + +from konova.models import BaseResource + + +class Geometry(BaseResource): + """ + Outsourced geometry model so multiple versions of the same object can refer to the same geometry if it is not changed + """ + from konova.settings import DEFAULT_SRID + geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID) \ No newline at end of file diff --git a/konova/models.py b/konova/models/object.py similarity index 75% rename from konova/models.py rename to konova/models/object.py index 7dbe4f73..7546e700 100644 --- a/konova/models.py +++ b/konova/models/object.py @@ -2,25 +2,21 @@ Author: Michel Peltriaux Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 17.11.20 +Created on: 15.11.21 """ -import os + import uuid from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist from django.utils import timezone from django.utils.timezone import now -from django.utils.translation import gettext_lazy as _ -from django.contrib.gis.db.models import MultiPolygonField from django.db import models, transaction - from compensation.settings import COMPENSATION_IDENTIFIER_TEMPLATE, COMPENSATION_IDENTIFIER_LENGTH, \ ECO_ACCOUNT_IDENTIFIER_TEMPLATE, ECO_ACCOUNT_IDENTIFIER_LENGTH from ema.settings import EMA_ACCOUNT_IDENTIFIER_LENGTH, EMA_ACCOUNT_IDENTIFIER_TEMPLATE from intervention.settings import INTERVENTION_IDENTIFIER_LENGTH, INTERVENTION_IDENTIFIER_TEMPLATE -from konova.settings import INTERVENTION_REVOCATION_DOC_PATH from konova.utils import generators from konova.utils.generators import generate_random_string from user.models import UserActionLogEntry, UserAction @@ -224,122 +220,6 @@ class BaseObject(BaseResource): return definitions[self.__class__]["template"].format(_str) -class DeadlineType(models.TextChoices): - """ - Django 3.x way of handling enums for models - """ - FINISHED = "finished", _("Finished") - MAINTAIN = "maintain", _("Maintain") - CONTROL = "control", _("Control") - OTHER = "other", _("Other") - - -class Deadline(BaseResource): - """ - Defines a deadline, which can be used to define dates with a semantic meaning - """ - - type = models.CharField(max_length=255, null=True, blank=True, choices=DeadlineType.choices) - date = models.DateField(null=True, blank=True) - comment = models.TextField(null=True, blank=True) - - def __str__(self): - return self.type - - @property - def type_humanized(self): - """ Returns humanized version of enum - - Used for template rendering - - Returns: - - """ - choices = DeadlineType.choices - for choice in choices: - if choice[0] == self.type: - return choice[1] - return None - - -def generate_document_file_upload_path(instance, filename): - """ Generates the file upload path for certain document instances - - Documents derived from AbstractDocument need specific upload paths for their related models. - - Args: - instance (): The document instance - filename (): The filename - - Returns: - - """ - from compensation.models import CompensationDocument, EcoAccountDocument - from ema.models import EmaDocument - from intervention.models import InterventionDocument, RevocationDocument - from konova.settings import ECO_ACCOUNT_DOC_PATH, EMA_DOC_PATH, \ - COMPENSATION_DOC_PATH, \ - INTERVENTION_DOC_PATH - - # Map document types to paths on the hard drive - path_map = { - InterventionDocument: INTERVENTION_DOC_PATH, - CompensationDocument: COMPENSATION_DOC_PATH, - EmaDocument: EMA_DOC_PATH, - RevocationDocument: INTERVENTION_REVOCATION_DOC_PATH, - EcoAccountDocument: ECO_ACCOUNT_DOC_PATH, - } - path = path_map.get(instance.__class__, None) - if path is None: - raise NotImplementedError("Unidentified document type: {}".format(instance.__class__)) - - # RevocationDocument needs special treatment, since these files need to be stored in a subfolder of the related - # instance's (Revocation) legaldata interventions folder - if instance.__class__ is RevocationDocument: - path = path.format(instance.intervention.id) - else: - path = path.format(instance.instance.id) - return path + filename - - -class AbstractDocument(BaseResource): - """ - Documents can be attached to compensation or intervention for uploading legal documents or pictures. - """ - title = models.CharField(max_length=500, null=True, blank=True) - date_of_creation = models.DateField() - file = models.FileField() - comment = models.TextField() - - class Meta: - abstract = True - - def delete(self, using=None, keep_parents=False): - """ Custom delete function to remove the real file from the hard drive - - Args: - using (): - keep_parents (): - - Returns: - - """ - try: - os.remove(self.file.file.name) - except FileNotFoundError: - # File seems to missing anyway - continue! - pass - super().delete(using=using, keep_parents=keep_parents) - - -class Geometry(BaseResource): - """ - Outsourced geometry model so multiple versions of the same object can refer to the same geometry if it is not changed - """ - from konova.settings import DEFAULT_SRID - geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID) - - class RecordableObject(models.Model): """ Wraps record related fields and functionality @@ -518,4 +398,4 @@ class ShareableObject(models.Model): self.generate_access_token(make_unique, rec_depth) else: self.access_token = token - self.save() + self.save() \ No newline at end of file diff --git a/user/models/__init__.py b/user/models/__init__.py new file mode 100644 index 00000000..85a98b67 --- /dev/null +++ b/user/models/__init__.py @@ -0,0 +1,10 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 15.11.21 + +""" +from .user_action import * +from .konova_user import * +from .notification import * diff --git a/user/models/konova_user.py b/user/models/konova_user.py new file mode 100644 index 00000000..b100e02f --- /dev/null +++ b/user/models/konova_user.py @@ -0,0 +1,19 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 15.11.21 + +""" +from django.contrib.auth.models import User +from django.db import models + + +class KonovaUserExtension(models.Model): + """ Extension model for additional ksp features + + Extends the default user model for some extras + + """ + user = models.OneToOneField(User, on_delete=models.CASCADE) + notifications = models.ManyToManyField("user.UserNotification", related_name="+") diff --git a/user/models/notification.py b/user/models/notification.py new file mode 100644 index 00000000..c86b84b7 --- /dev/null +++ b/user/models/notification.py @@ -0,0 +1,34 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 15.11.21 + +""" +from django.db import models + +from user.enums import UserNotificationEnum + + +class UserNotification(models.Model): + """ Notifications for users + + """ + id = models.CharField( + max_length=500, + null=False, + blank=False, + choices=UserNotificationEnum.as_choices(drop_empty_choice=True), + primary_key=True, + ) + name = models.CharField( + max_length=500, + null=False, + blank=False, + unique=True, + help_text="Human readable name" + ) + is_active = models.BooleanField(default=True, help_text="Can be toggle to enable/disable this notification for all users") + + def __str__(self): + return self.name \ No newline at end of file diff --git a/user/models.py b/user/models/user_action.py similarity index 65% rename from user/models.py rename to user/models/user_action.py index a85159f5..a37aab54 100644 --- a/user/models.py +++ b/user/models/user_action.py @@ -1,44 +1,15 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 15.11.21 + +""" import uuid -from django.utils.translation import gettext_lazy as _ from django.contrib.auth.models import User from django.db import models - -from user.enums import UserNotificationEnum - - -class UserNotification(models.Model): - """ Notifications for users - - """ - id = models.CharField( - max_length=500, - null=False, - blank=False, - choices=UserNotificationEnum.as_choices(drop_empty_choice=True), - primary_key=True, - ) - name = models.CharField( - max_length=500, - null=False, - blank=False, - unique=True, - help_text="Human readable name" - ) - is_active = models.BooleanField(default=True, help_text="Can be toggle to enable/disable this notification for all users") - - def __str__(self): - return self.name - - -class KonovaUserExtension(models.Model): - """ Extension model for additional ksp features - - Extends the default user model for some extras - - """ - user = models.OneToOneField(User, on_delete=models.CASCADE) - notifications = models.ManyToManyField(UserNotification, related_name="+") +from django.utils.translation import gettext_lazy as _ class UserAction(models.TextChoices): From 6dd13179b5e4caf90727ee2dad1fdf111151368b Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 16 Nov 2021 08:29:18 +0100 Subject: [PATCH 04/13] Refactoring * splits compensation/models.py into subpackage * renames base objects by adding suffix Mixin --- compensation/models.py | 670 ---------------------------- compensation/models/__init__.py | 12 + compensation/models/action.py | 66 +++ compensation/models/compensation.py | 336 ++++++++++++++ compensation/models/eco_account.py | 248 ++++++++++ compensation/models/payment.py | 38 ++ compensation/models/state.py | 36 ++ ema/models/ema.py | 4 +- intervention/models/intervention.py | 9 +- konova/forms.py | 6 +- konova/models/object.py | 10 +- 11 files changed, 751 insertions(+), 684 deletions(-) delete mode 100644 compensation/models.py create mode 100644 compensation/models/__init__.py create mode 100644 compensation/models/action.py create mode 100644 compensation/models/compensation.py create mode 100644 compensation/models/eco_account.py create mode 100644 compensation/models/payment.py create mode 100644 compensation/models/state.py diff --git a/compensation/models.py b/compensation/models.py deleted file mode 100644 index 9763fa7b..00000000 --- a/compensation/models.py +++ /dev/null @@ -1,670 +0,0 @@ -""" -Author: Michel Peltriaux -Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 17.11.20 - -""" -import shutil - -from django.contrib.auth.models import User -from django.contrib.gis.db import models -from django.core.exceptions import ValidationError -from django.core.validators import MinValueValidator -from django.db import transaction -from django.db.models import Sum, QuerySet -from django.utils.translation import gettext_lazy as _ - -from codelist.models import KonovaCode -from intervention.models import Intervention, Responsibility -from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID -from compensation.managers import CompensationStateManager, EcoAccountDeductionManager, CompensationActionManager, \ - EcoAccountManager, CompensationManager -from compensation.utils.quality import CompensationQualityChecker, EcoAccountQualityChecker -from konova.models import BaseObject, BaseResource, Geometry, UuidModel, AbstractDocument, \ - generate_document_file_upload_path, RecordableObject, ShareableObject, Deadline -from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE -from user.models import UserActionLogEntry, UserAction - - -class Payment(BaseResource): - """ - Holds data on a payment for an intervention (alternative to a classic compensation) - """ - amount = models.FloatField(validators=[MinValueValidator(limit_value=0.00)]) - due_on = models.DateField(null=True, blank=True) - comment = models.TextField( - null=True, - blank=True, - help_text="Refers to german money transfer 'Verwendungszweck'", - ) - intervention = models.ForeignKey( - Intervention, - null=True, - blank=True, - on_delete=models.CASCADE, - related_name='payments' - ) - - class Meta: - ordering = [ - "-amount", - ] - - -class CompensationState(UuidModel): - """ - Compensations must define the state of an area before and after the compensation. - """ - biotope_type = models.ForeignKey( - KonovaCode, - on_delete=models.SET_NULL, - null=True, - blank=True, - limit_choices_to={ - "code_lists__in": [CODELIST_BIOTOPES_ID], - "is_selectable": True, - "is_archived": False, - } - ) - surface = models.FloatField() - - objects = CompensationStateManager() - - def __str__(self): - return "{} | {} m²".format(self.biotope_type, self.surface) - - -class UnitChoices(models.TextChoices): - """ - Predefines units for selection - """ - cm = "cm", _("cm") - m = "m", _("m") - km = "km", _("km") - qm = "qm", _("m²") - ha = "ha", _("ha") - st = "pcs", _("Pieces") # pieces - - -class CompensationAction(BaseResource): - """ - Compensations include actions like planting trees, refreshing rivers and so on. - """ - action_type = models.ForeignKey( - KonovaCode, - on_delete=models.SET_NULL, - null=True, - blank=True, - limit_choices_to={ - "code_lists__in": [CODELIST_COMPENSATION_ACTION_ID], - "is_selectable": True, - "is_archived": False, - } - ) - amount = models.FloatField() - unit = models.CharField(max_length=100, null=True, blank=True, choices=UnitChoices.choices) - comment = models.TextField(blank=True, null=True, help_text="Additional comment") - - objects = CompensationActionManager() - - def __str__(self): - return "{} | {} {}".format(self.action_type, self.amount, self.unit) - - @property - def unit_humanize(self): - """ Returns humanized version of enum - - Used for template rendering - - Returns: - - """ - choices = UnitChoices.choices - for choice in choices: - if choice[0] == self.unit: - return choice[1] - return None - - -class AbstractCompensation(BaseObject): - """ - Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation, - EMA or EcoAccount. - - """ - responsible = models.OneToOneField( - Responsibility, - on_delete=models.SET_NULL, - null=True, - blank=True, - help_text="Holds data on responsible organizations ('Zulassungsbehörde', 'Eintragungsstelle') and handler", - ) - - before_states = models.ManyToManyField(CompensationState, blank=True, related_name='+', help_text="Refers to 'Ausgangszustand Biotop'") - after_states = models.ManyToManyField(CompensationState, blank=True, related_name='+', help_text="Refers to 'Zielzustand Biotop'") - actions = models.ManyToManyField(CompensationAction, blank=True, help_text="Refers to 'Maßnahmen'") - - deadlines = models.ManyToManyField("konova.Deadline", blank=True, related_name="+") - - geometry = models.ForeignKey(Geometry, null=True, blank=True, on_delete=models.SET_NULL) - - class Meta: - abstract = True - - def add_new_deadline(self, form) -> Deadline: - """ Adds a new deadline to the abstract compensation - - Args: - form (NewDeadlineModalForm): The form holding all relevant data - - Returns: - - """ - form_data = form.cleaned_data - user = form.user - with transaction.atomic(): - created_action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.CREATED - ) - deadline = Deadline.objects.create( - type=form_data["type"], - date=form_data["date"], - comment=form_data["comment"], - created=created_action, - ) - edited_action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.EDITED, - comment=_("Added deadline") - ) - self.modified = edited_action - self.save() - self.log.add(edited_action) - self.deadlines.add(deadline) - return deadline - - def add_new_action(self, form) -> CompensationAction: - """ Adds a new action to the compensation - - Args: - form (NewActionModalForm): The form holding all relevant data - - Returns: - - """ - form_data = form.cleaned_data - user = form.user - with transaction.atomic(): - user_action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.CREATED, - ) - comp_action = CompensationAction.objects.create( - action_type=form_data["action_type"], - amount=form_data["amount"], - unit=form_data["unit"], - comment=form_data["comment"], - created=user_action, - ) - edited_action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.EDITED, - comment=_("Added action"), - ) - self.modified = edited_action - self.save() - self.log.add(edited_action) - self.actions.add(comp_action) - return comp_action - - def get_surface_after_states(self) -> float: - """ Calculates the compensation's/account's surface - - Returns: - sum_surface (float) - """ - return self._calc_surface(self.after_states.all()) - - def get_surface_before_states(self) -> float: - """ Calculates the compensation's/account's surface - - Returns: - sum_surface (float) - """ - return self._calc_surface(self.before_states.all()) - - def _calc_surface(self, qs: QuerySet): - """ Calculates the surface sum of a given queryset - - Args: - qs (QuerySet): The queryset containing CompensationState entries - - Returns: - - """ - return qs.aggregate(Sum("surface"))["surface__sum"] or 0 - - def quality_check(self) -> CompensationQualityChecker: - """ Performs data quality check - - Returns: - checker (CompensationQualityChecker): Holds validity data and error messages - """ - checker = CompensationQualityChecker(self) - checker.run_check() - return checker - - -class CEFMixin(models.Model): - """ Provides CEF flag as Mixin - - """ - is_cef = models.BooleanField( - blank=True, - null=True, - default=False, - help_text="Flag if compensation is a 'CEF-Maßnahme'" - ) - - class Meta: - abstract = True - - -class CoherenceMixin(models.Model): - """ Provides coherence keeping flag as Mixin - - """ - is_coherence_keeping = models.BooleanField( - blank=True, - null=True, - default=False, - help_text="Flag if compensation is a 'Kohärenzsicherung'" - ) - - class Meta: - abstract = True - - -class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin): - """ - Regular compensation, linked to an intervention - """ - intervention = models.ForeignKey( - Intervention, - on_delete=models.CASCADE, - null=True, - blank=True, - related_name='compensations' - ) - - objects = CompensationManager() - - def __str__(self): - return "{}".format(self.identifier) - - def save(self, *args, **kwargs): - if self.identifier is None or len(self.identifier) == 0: - # Create new identifier is none was given - self.identifier = self.generate_new_identifier() - - # Before saving, make sure a given identifier has not been taken already in the meanwhile - while Compensation.objects.filter(identifier=self.identifier).exclude(id=self.id).exists(): - self.identifier = self.generate_new_identifier() - super().save(*args, **kwargs) - - def is_shared_with(self, user: User): - """ Access check - - Checks whether a given user has access to this object - - Args: - user (User): The user to be checked - - Returns: - - """ - # Compensations inherit their shared state from the interventions - return self.intervention.is_shared_with(user) - - def get_LANIS_link(self) -> str: - """ Generates a link for LANIS depending on the geometry - - Returns: - - """ - try: - geom = self.geometry.geom.transform(DEFAULT_SRID_RLP, clone=True) - x = geom.centroid.x - y = geom.centroid.y - zoom_lvl = 16 - except AttributeError: - # If no geometry has been added, yet. - x = 1 - y = 1 - zoom_lvl = 6 - return LANIS_LINK_TEMPLATE.format( - zoom_lvl, - x, - y, - ) - - def get_documents(self) -> QuerySet: - """ Getter for all documents of a compensation - - Returns: - docs (QuerySet): The queryset of all documents - """ - docs = CompensationDocument.objects.filter( - instance=self - ) - return docs - - def add_state(self, form, is_before_state: bool) -> CompensationState: - """ Adds a new compensation state to the compensation - - Args: - form (NewStateModalForm): The form, holding all relevant data - is_before_state (bool): Whether this is a new before_state or after_state - - Returns: - - """ - form_data = form.cleaned_data - user = form.user - with transaction.atomic(): - user_action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.EDITED, - comment=_("Added state") - ) - self.log.add(user_action) - self.modified = user_action - self.save() - - state = CompensationState.objects.create( - biotope_type=form_data["biotope_type"], - surface=form_data["surface"], - ) - if is_before_state: - self.before_states.add(state) - else: - self.after_states.add(state) - return state - - -class CompensationDocument(AbstractDocument): - """ - Specializes document upload for revocations with certain path - """ - instance = models.ForeignKey( - Compensation, - on_delete=models.CASCADE, - related_name="documents", - ) - file = models.FileField( - upload_to=generate_document_file_upload_path, - max_length=1000, - ) - - def delete(self, *args, **kwargs): - """ - Custom delete functionality for CompensationDocuments. - Removes the folder from the file system if there are no further documents for this entry. - - Args: - *args (): - **kwargs (): - - Returns: - - """ - comp_docs = self.instance.get_documents() - - folder_path = None - if comp_docs.count() == 1: - # The only file left for this compensation is the one which is currently processed and will be deleted - # Make sure that the compensation folder itself is deleted as well, not only the file - # Therefore take the folder path from the file path - folder_path = self.file.path.split("/")[:-1] - folder_path = "/".join(folder_path) - - # Remove the file itself - super().delete(*args, **kwargs) - - # If a folder path has been set, we need to delete the whole folder! - if folder_path is not None: - try: - shutil.rmtree(folder_path) - except FileNotFoundError: - # Folder seems to be missing already... - pass - - -class EcoAccount(AbstractCompensation, ShareableObject, RecordableObject): - """ - An eco account is a kind of 'prepaid' compensation. It can be compared to an account that already has been filled - with some kind of currency. From this account one is able to deduct currency for current projects. - """ - from intervention.models import Legal - deductable_surface = models.FloatField( - blank=True, - null=True, - help_text="Amount of deductable surface - can be lower than the total surface due to deduction limitations", - default=0, - ) - - legal = models.OneToOneField( - Legal, - on_delete=models.SET_NULL, - null=True, - blank=True, - help_text="Holds data on legal dates or law" - ) - - objects = EcoAccountManager() - - def __str__(self): - return "{}".format(self.identifier) - - def clean(self): - # Deductable surface can not be larger than added states after surface - after_state_sum = self.get_state_after_surface_sum() - if self.deductable_surface > after_state_sum: - raise ValidationError(_("Deductable surface can not be larger than existing surfaces in after states")) - - # Deductable surface can not be lower than amount of already deducted surfaces - # User needs to contact deducting user in case of further problems - deducted_sum = self.get_deductions_surface() - if self.deductable_surface < deducted_sum: - raise ValidationError( - _("Deductable surface can not be smaller than the sum of already existing deductions. Please contact the responsible users for the deductions!") - ) - - def save(self, *args, **kwargs): - if self.identifier is None or len(self.identifier) == 0: - # Create new identifier if none was given - self.identifier = self.generate_new_identifier() - - # Before saving, make sure the given identifier is not used, yet - while EcoAccount.objects.filter(identifier=self.identifier).exclude(id=self.id).exists(): - self.identifier = self.generate_new_identifier() - super().save(*args, **kwargs) - - @property - def deductions_surface_sum(self) -> float: - """ Shortcut for get_deductions_surface. - - Can be used in templates - - Returns: - sum_surface (float) - """ - return self.get_deductions_surface() - - def get_deductions_surface(self) -> float: - """ Calculates the account's deductions surface sum - - Returns: - sum_surface (float) - """ - return self.deductions.all().aggregate(Sum("surface"))["surface__sum"] or 0 - - def get_state_after_surface_sum(self) -> float: - """ Calculates the account's after state surface sum - - Returns: - sum_surface (float) - """ - return self.after_states.all().aggregate(Sum("surface"))["surface__sum"] or 0 - - def get_available_rest(self) -> (float, float): - """ Calculates available rest surface of the eco account - - Args: - - Returns: - ret_val_total (float): Total amount - ret_val_relative (float): Amount as percentage (0-100) - """ - deductions = self.deductions.filter( - intervention__deleted=None, - ) - deductions_surfaces = deductions.aggregate(Sum("surface"))["surface__sum"] or 0 - available_surfaces = self.deductable_surface or deductions_surfaces ## no division by zero - ret_val_total = available_surfaces - deductions_surfaces - - if available_surfaces > 0: - ret_val_relative = int((ret_val_total / available_surfaces) * 100) - else: - ret_val_relative = 0 - - return ret_val_total, ret_val_relative - - def get_LANIS_link(self) -> str: - """ Generates a link for LANIS depending on the geometry - - Returns: - - """ - try: - geom = self.geometry.geom.transform(DEFAULT_SRID_RLP, clone=True) - x = geom.centroid.x - y = geom.centroid.y - zoom_lvl = 16 - except AttributeError: - # If no geometry has been added, yet. - x = 1 - y = 1 - zoom_lvl = 6 - return LANIS_LINK_TEMPLATE.format( - zoom_lvl, - x, - y, - ) - - def quality_check(self) -> EcoAccountQualityChecker: - """ Quality check - - Returns: - ret_msgs (EcoAccountQualityChecker): Holds validity and error messages - """ - checker = EcoAccountQualityChecker(self) - checker.run_check() - return checker - - def get_documents(self) -> QuerySet: - """ Getter for all documents of an EcoAccount - - Returns: - docs (QuerySet): The queryset of all documents - """ - docs = EcoAccountDocument.objects.filter( - instance=self - ) - return docs - - -class EcoAccountDocument(AbstractDocument): - """ - Specializes document upload for revocations with certain path - """ - instance = models.ForeignKey( - EcoAccount, - on_delete=models.CASCADE, - related_name="documents", - ) - file = models.FileField( - upload_to=generate_document_file_upload_path, - max_length=1000, - ) - - def delete(self, *args, **kwargs): - """ - Custom delete functionality for EcoAccountDocuments. - Removes the folder from the file system if there are no further documents for this entry. - - Args: - *args (): - **kwargs (): - - Returns: - - """ - acc_docs = self.instance.get_documents() - - folder_path = None - if acc_docs.count() == 1: - # The only file left for this eco account is the one which is currently processed and will be deleted - # Make sure that the compensation folder itself is deleted as well, not only the file - # Therefore take the folder path from the file path - folder_path = self.file.path.split("/")[:-1] - folder_path = "/".join(folder_path) - - # Remove the file itself - super().delete(*args, **kwargs) - - # If a folder path has been set, we need to delete the whole folder! - if folder_path is not None: - try: - shutil.rmtree(folder_path) - except FileNotFoundError: - # Folder seems to be missing already... - pass - - -class EcoAccountDeduction(BaseResource): - """ - A deduction object for eco accounts - """ - from intervention.models import Intervention - account = models.ForeignKey( - EcoAccount, - on_delete=models.SET_NULL, - null=True, - blank=True, - help_text="Deducted from", - related_name="deductions", - ) - surface = models.FloatField( - null=True, - blank=True, - help_text="Amount deducted (m²)", - validators=[ - MinValueValidator(limit_value=0.00), - ] - ) - intervention = models.ForeignKey( - Intervention, - on_delete=models.CASCADE, - null=True, - blank=True, - help_text="Deducted for", - related_name="deductions", - ) - - objects = EcoAccountDeductionManager() - - def __str__(self): - return "{} of {}".format(self.surface, self.account) diff --git a/compensation/models/__init__.py b/compensation/models/__init__.py new file mode 100644 index 00000000..9f289bd9 --- /dev/null +++ b/compensation/models/__init__.py @@ -0,0 +1,12 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 16.11.21 + +""" +from .action import * +from .state import * +from .compensation import * +from .eco_account import * +from .payment import * diff --git a/compensation/models/action.py b/compensation/models/action.py new file mode 100644 index 00000000..35d4c3f7 --- /dev/null +++ b/compensation/models/action.py @@ -0,0 +1,66 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 16.11.21 + +""" +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from codelist.models import KonovaCode +from codelist.settings import CODELIST_COMPENSATION_ACTION_ID +from compensation.managers import CompensationActionManager +from konova.models import BaseResource + + +class UnitChoices(models.TextChoices): + """ + Predefines units for selection + """ + cm = "cm", _("cm") + m = "m", _("m") + km = "km", _("km") + qm = "qm", _("m²") + ha = "ha", _("ha") + st = "pcs", _("Pieces") # pieces + + +class CompensationAction(BaseResource): + """ + Compensations include actions like planting trees, refreshing rivers and so on. + """ + action_type = models.ForeignKey( + KonovaCode, + on_delete=models.SET_NULL, + null=True, + blank=True, + limit_choices_to={ + "code_lists__in": [CODELIST_COMPENSATION_ACTION_ID], + "is_selectable": True, + "is_archived": False, + } + ) + amount = models.FloatField() + unit = models.CharField(max_length=100, null=True, blank=True, choices=UnitChoices.choices) + comment = models.TextField(blank=True, null=True, help_text="Additional comment") + + objects = CompensationActionManager() + + def __str__(self): + return "{} | {} {}".format(self.action_type, self.amount, self.unit) + + @property + def unit_humanize(self): + """ Returns humanized version of enum + + Used for template rendering + + Returns: + + """ + choices = UnitChoices.choices + for choice in choices: + if choice[0] == self.unit: + return choice[1] + return None diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py new file mode 100644 index 00000000..7a33e1b3 --- /dev/null +++ b/compensation/models/compensation.py @@ -0,0 +1,336 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 16.11.21 + +""" +import shutil + +from django.contrib.auth.models import User +from django.db import models, transaction +from django.db.models import QuerySet, Sum +from django.utils.translation import gettext_lazy as _ + +from compensation.managers import CompensationManager +from compensation.models import CompensationState, CompensationAction +from compensation.utils.quality import CompensationQualityChecker +from intervention.models import Responsibility, Intervention +from konova.models import BaseObject, AbstractDocument, Geometry, Deadline, generate_document_file_upload_path +from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE +from user.models import UserActionLogEntry, UserAction + + +class AbstractCompensation(BaseObject): + """ + Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation, + EMA or EcoAccount. + + """ + responsible = models.OneToOneField( + Responsibility, + on_delete=models.SET_NULL, + null=True, + blank=True, + help_text="Holds data on responsible organizations ('Zulassungsbehörde', 'Eintragungsstelle') and handler", + ) + + before_states = models.ManyToManyField(CompensationState, blank=True, related_name='+', help_text="Refers to 'Ausgangszustand Biotop'") + after_states = models.ManyToManyField(CompensationState, blank=True, related_name='+', help_text="Refers to 'Zielzustand Biotop'") + actions = models.ManyToManyField(CompensationAction, blank=True, help_text="Refers to 'Maßnahmen'") + + deadlines = models.ManyToManyField("konova.Deadline", blank=True, related_name="+") + + geometry = models.ForeignKey(Geometry, null=True, blank=True, on_delete=models.SET_NULL) + + class Meta: + abstract = True + + def add_new_deadline(self, form) -> Deadline: + """ Adds a new deadline to the abstract compensation + + Args: + form (NewDeadlineModalForm): The form holding all relevant data + + Returns: + + """ + form_data = form.cleaned_data + user = form.user + with transaction.atomic(): + created_action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.CREATED + ) + deadline = Deadline.objects.create( + type=form_data["type"], + date=form_data["date"], + comment=form_data["comment"], + created=created_action, + ) + edited_action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.EDITED, + comment=_("Added deadline") + ) + self.modified = edited_action + self.save() + self.log.add(edited_action) + self.deadlines.add(deadline) + return deadline + + def add_new_action(self, form) -> CompensationAction: + """ Adds a new action to the compensation + + Args: + form (NewActionModalForm): The form holding all relevant data + + Returns: + + """ + form_data = form.cleaned_data + user = form.user + with transaction.atomic(): + user_action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.CREATED, + ) + comp_action = CompensationAction.objects.create( + action_type=form_data["action_type"], + amount=form_data["amount"], + unit=form_data["unit"], + comment=form_data["comment"], + created=user_action, + ) + edited_action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.EDITED, + comment=_("Added action"), + ) + self.modified = edited_action + self.save() + self.log.add(edited_action) + self.actions.add(comp_action) + return comp_action + + def get_surface_after_states(self) -> float: + """ Calculates the compensation's/account's surface + + Returns: + sum_surface (float) + """ + return self._calc_surface(self.after_states.all()) + + def get_surface_before_states(self) -> float: + """ Calculates the compensation's/account's surface + + Returns: + sum_surface (float) + """ + return self._calc_surface(self.before_states.all()) + + def _calc_surface(self, qs: QuerySet): + """ Calculates the surface sum of a given queryset + + Args: + qs (QuerySet): The queryset containing CompensationState entries + + Returns: + + """ + return qs.aggregate(Sum("surface"))["surface__sum"] or 0 + + def quality_check(self) -> CompensationQualityChecker: + """ Performs data quality check + + Returns: + checker (CompensationQualityChecker): Holds validity data and error messages + """ + checker = CompensationQualityChecker(self) + checker.run_check() + return checker + + +class CEFMixin(models.Model): + """ Provides CEF flag as Mixin + + """ + is_cef = models.BooleanField( + blank=True, + null=True, + default=False, + help_text="Flag if compensation is a 'CEF-Maßnahme'" + ) + + class Meta: + abstract = True + + +class CoherenceMixin(models.Model): + """ Provides coherence keeping flag as Mixin + + """ + is_coherence_keeping = models.BooleanField( + blank=True, + null=True, + default=False, + help_text="Flag if compensation is a 'Kohärenzsicherung'" + ) + + class Meta: + abstract = True + + +class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin): + """ + Regular compensation, linked to an intervention + """ + intervention = models.ForeignKey( + Intervention, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name='compensations' + ) + + objects = CompensationManager() + + def __str__(self): + return "{}".format(self.identifier) + + def save(self, *args, **kwargs): + if self.identifier is None or len(self.identifier) == 0: + # Create new identifier is none was given + self.identifier = self.generate_new_identifier() + + # Before saving, make sure a given identifier has not been taken already in the meanwhile + while Compensation.objects.filter(identifier=self.identifier).exclude(id=self.id).exists(): + self.identifier = self.generate_new_identifier() + super().save(*args, **kwargs) + + def is_shared_with(self, user: User): + """ Access check + + Checks whether a given user has access to this object + + Args: + user (User): The user to be checked + + Returns: + + """ + # Compensations inherit their shared state from the interventions + return self.intervention.is_shared_with(user) + + def get_LANIS_link(self) -> str: + """ Generates a link for LANIS depending on the geometry + + Returns: + + """ + try: + geom = self.geometry.geom.transform(DEFAULT_SRID_RLP, clone=True) + x = geom.centroid.x + y = geom.centroid.y + zoom_lvl = 16 + except AttributeError: + # If no geometry has been added, yet. + x = 1 + y = 1 + zoom_lvl = 6 + return LANIS_LINK_TEMPLATE.format( + zoom_lvl, + x, + y, + ) + + def get_documents(self) -> QuerySet: + """ Getter for all documents of a compensation + + Returns: + docs (QuerySet): The queryset of all documents + """ + docs = CompensationDocument.objects.filter( + instance=self + ) + return docs + + def add_state(self, form, is_before_state: bool) -> CompensationState: + """ Adds a new compensation state to the compensation + + Args: + form (NewStateModalForm): The form, holding all relevant data + is_before_state (bool): Whether this is a new before_state or after_state + + Returns: + + """ + form_data = form.cleaned_data + user = form.user + with transaction.atomic(): + user_action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.EDITED, + comment=_("Added state") + ) + self.log.add(user_action) + self.modified = user_action + self.save() + + state = CompensationState.objects.create( + biotope_type=form_data["biotope_type"], + surface=form_data["surface"], + ) + if is_before_state: + self.before_states.add(state) + else: + self.after_states.add(state) + return state + + +class CompensationDocument(AbstractDocument): + """ + Specializes document upload for revocations with certain path + """ + instance = models.ForeignKey( + Compensation, + on_delete=models.CASCADE, + related_name="documents", + ) + file = models.FileField( + upload_to=generate_document_file_upload_path, + max_length=1000, + ) + + def delete(self, *args, **kwargs): + """ + Custom delete functionality for CompensationDocuments. + Removes the folder from the file system if there are no further documents for this entry. + + Args: + *args (): + **kwargs (): + + Returns: + + """ + comp_docs = self.instance.get_documents() + + folder_path = None + if comp_docs.count() == 1: + # The only file left for this compensation is the one which is currently processed and will be deleted + # Make sure that the compensation folder itself is deleted as well, not only the file + # Therefore take the folder path from the file path + folder_path = self.file.path.split("/")[:-1] + folder_path = "/".join(folder_path) + + # Remove the file itself + super().delete(*args, **kwargs) + + # If a folder path has been set, we need to delete the whole folder! + if folder_path is not None: + try: + shutil.rmtree(folder_path) + except FileNotFoundError: + # Folder seems to be missing already... + pass diff --git a/compensation/models/eco_account.py b/compensation/models/eco_account.py new file mode 100644 index 00000000..2d3ae226 --- /dev/null +++ b/compensation/models/eco_account.py @@ -0,0 +1,248 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 16.11.21 + +""" +import shutil + +from django.core.exceptions import ValidationError +from django.core.validators import MinValueValidator +from django.db import models +from django.db.models import Sum, QuerySet +from django.utils.translation import gettext_lazy as _ + +from compensation.managers import EcoAccountManager, EcoAccountDeductionManager +from compensation.models.compensation import AbstractCompensation +from compensation.utils.quality import EcoAccountQualityChecker +from konova.models import ShareableObjectMixin, RecordableObjectMixin, AbstractDocument, BaseResource, \ + generate_document_file_upload_path +from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE +from intervention.models import Intervention, Legal + + +class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin): + """ + An eco account is a kind of 'prepaid' compensation. It can be compared to an account that already has been filled + with some kind of currency. From this account one is able to deduct currency for current projects. + """ + deductable_surface = models.FloatField( + blank=True, + null=True, + help_text="Amount of deductable surface - can be lower than the total surface due to deduction limitations", + default=0, + ) + + legal = models.OneToOneField( + Legal, + on_delete=models.SET_NULL, + null=True, + blank=True, + help_text="Holds data on legal dates or law" + ) + + objects = EcoAccountManager() + + def __str__(self): + return "{}".format(self.identifier) + + def clean(self): + # Deductable surface can not be larger than added states after surface + after_state_sum = self.get_state_after_surface_sum() + if self.deductable_surface > after_state_sum: + raise ValidationError(_("Deductable surface can not be larger than existing surfaces in after states")) + + # Deductable surface can not be lower than amount of already deducted surfaces + # User needs to contact deducting user in case of further problems + deducted_sum = self.get_deductions_surface() + if self.deductable_surface < deducted_sum: + raise ValidationError( + _("Deductable surface can not be smaller than the sum of already existing deductions. Please contact the responsible users for the deductions!") + ) + + def save(self, *args, **kwargs): + if self.identifier is None or len(self.identifier) == 0: + # Create new identifier if none was given + self.identifier = self.generate_new_identifier() + + # Before saving, make sure the given identifier is not used, yet + while EcoAccount.objects.filter(identifier=self.identifier).exclude(id=self.id).exists(): + self.identifier = self.generate_new_identifier() + super().save(*args, **kwargs) + + @property + def deductions_surface_sum(self) -> float: + """ Shortcut for get_deductions_surface. + + Can be used in templates + + Returns: + sum_surface (float) + """ + return self.get_deductions_surface() + + def get_deductions_surface(self) -> float: + """ Calculates the account's deductions surface sum + + Returns: + sum_surface (float) + """ + return self.deductions.all().aggregate(Sum("surface"))["surface__sum"] or 0 + + def get_state_after_surface_sum(self) -> float: + """ Calculates the account's after state surface sum + + Returns: + sum_surface (float) + """ + return self.after_states.all().aggregate(Sum("surface"))["surface__sum"] or 0 + + def get_available_rest(self) -> (float, float): + """ Calculates available rest surface of the eco account + + Args: + + Returns: + ret_val_total (float): Total amount + ret_val_relative (float): Amount as percentage (0-100) + """ + deductions = self.deductions.filter( + intervention__deleted=None, + ) + deductions_surfaces = deductions.aggregate(Sum("surface"))["surface__sum"] or 0 + available_surfaces = self.deductable_surface or deductions_surfaces ## no division by zero + ret_val_total = available_surfaces - deductions_surfaces + + if available_surfaces > 0: + ret_val_relative = int((ret_val_total / available_surfaces) * 100) + else: + ret_val_relative = 0 + + return ret_val_total, ret_val_relative + + def get_LANIS_link(self) -> str: + """ Generates a link for LANIS depending on the geometry + + Returns: + + """ + try: + geom = self.geometry.geom.transform(DEFAULT_SRID_RLP, clone=True) + x = geom.centroid.x + y = geom.centroid.y + zoom_lvl = 16 + except AttributeError: + # If no geometry has been added, yet. + x = 1 + y = 1 + zoom_lvl = 6 + return LANIS_LINK_TEMPLATE.format( + zoom_lvl, + x, + y, + ) + + def quality_check(self) -> EcoAccountQualityChecker: + """ Quality check + + Returns: + ret_msgs (EcoAccountQualityChecker): Holds validity and error messages + """ + checker = EcoAccountQualityChecker(self) + checker.run_check() + return checker + + def get_documents(self) -> QuerySet: + """ Getter for all documents of an EcoAccount + + Returns: + docs (QuerySet): The queryset of all documents + """ + docs = EcoAccountDocument.objects.filter( + instance=self + ) + return docs + + +class EcoAccountDocument(AbstractDocument): + """ + Specializes document upload for revocations with certain path + """ + instance = models.ForeignKey( + EcoAccount, + on_delete=models.CASCADE, + related_name="documents", + ) + file = models.FileField( + upload_to=generate_document_file_upload_path, + max_length=1000, + ) + + def delete(self, *args, **kwargs): + """ + Custom delete functionality for EcoAccountDocuments. + Removes the folder from the file system if there are no further documents for this entry. + + Args: + *args (): + **kwargs (): + + Returns: + + """ + acc_docs = self.instance.get_documents() + + folder_path = None + if acc_docs.count() == 1: + # The only file left for this eco account is the one which is currently processed and will be deleted + # Make sure that the compensation folder itself is deleted as well, not only the file + # Therefore take the folder path from the file path + folder_path = self.file.path.split("/")[:-1] + folder_path = "/".join(folder_path) + + # Remove the file itself + super().delete(*args, **kwargs) + + # If a folder path has been set, we need to delete the whole folder! + if folder_path is not None: + try: + shutil.rmtree(folder_path) + except FileNotFoundError: + # Folder seems to be missing already... + pass + + +class EcoAccountDeduction(BaseResource): + """ + A deduction object for eco accounts + """ + account = models.ForeignKey( + EcoAccount, + on_delete=models.SET_NULL, + null=True, + blank=True, + help_text="Deducted from", + related_name="deductions", + ) + surface = models.FloatField( + null=True, + blank=True, + help_text="Amount deducted (m²)", + validators=[ + MinValueValidator(limit_value=0.00), + ] + ) + intervention = models.ForeignKey( + Intervention, + on_delete=models.CASCADE, + null=True, + blank=True, + help_text="Deducted for", + related_name="deductions", + ) + + objects = EcoAccountDeductionManager() + + def __str__(self): + return "{} of {}".format(self.surface, self.account) diff --git a/compensation/models/payment.py b/compensation/models/payment.py new file mode 100644 index 00000000..a630e36d --- /dev/null +++ b/compensation/models/payment.py @@ -0,0 +1,38 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 16.11.21 + +""" +from django.core.validators import MinValueValidator +from django.db import models + +from intervention.models import Intervention +from konova.models import BaseResource + + +class Payment(BaseResource): + """ + Holds data on a payment for an intervention (alternative to a classic compensation) + """ + amount = models.FloatField(validators=[MinValueValidator(limit_value=0.00)]) + due_on = models.DateField(null=True, blank=True) + comment = models.TextField( + null=True, + blank=True, + help_text="Refers to german money transfer 'Verwendungszweck'", + ) + intervention = models.ForeignKey( + Intervention, + null=True, + blank=True, + on_delete=models.CASCADE, + related_name='payments' + ) + + class Meta: + ordering = [ + "-amount", + ] + diff --git a/compensation/models/state.py b/compensation/models/state.py new file mode 100644 index 00000000..01aad147 --- /dev/null +++ b/compensation/models/state.py @@ -0,0 +1,36 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 16.11.21 + +""" +from django.db import models + +from codelist.models import KonovaCode +from codelist.settings import CODELIST_BIOTOPES_ID +from compensation.managers import CompensationStateManager +from konova.models import UuidModel + + +class CompensationState(UuidModel): + """ + Compensations must define the state of an area before and after the compensation. + """ + biotope_type = models.ForeignKey( + KonovaCode, + on_delete=models.SET_NULL, + null=True, + blank=True, + limit_choices_to={ + "code_lists__in": [CODELIST_BIOTOPES_ID], + "is_selectable": True, + "is_archived": False, + } + ) + surface = models.FloatField() + + objects = CompensationStateManager() + + def __str__(self): + return "{} | {} m²".format(self.biotope_type, self.surface) diff --git a/ema/models/ema.py b/ema/models/ema.py index 5c7e9fce..bc7589d0 100644 --- a/ema/models/ema.py +++ b/ema/models/ema.py @@ -13,11 +13,11 @@ from django.db.models import QuerySet from compensation.models import AbstractCompensation from ema.managers import EmaManager from ema.utils.quality import EmaQualityChecker -from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableObject, ShareableObject +from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableObjectMixin, ShareableObjectMixin from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE -class Ema(AbstractCompensation, ShareableObject, RecordableObject): +class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin): """ EMA = Ersatzzahlungsmaßnahme (compensation actions from payments) diff --git a/intervention/models/intervention.py b/intervention/models/intervention.py index 9b69764e..0d7f1d8c 100644 --- a/intervention/models/intervention.py +++ b/intervention/models/intervention.py @@ -5,6 +5,8 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 15.11.21 """ +import shutil + from django.contrib.auth.models import User from django.db import models, transaction from django.db.models import QuerySet @@ -15,13 +17,13 @@ from intervention.models.legal import Legal from intervention.models.responsibility import Responsibility from intervention.models.revocation import RevocationDocument from intervention.utils.quality import InterventionQualityChecker -from konova.models import generate_document_file_upload_path, AbstractDocument, Geometry, BaseObject, ShareableObject, \ - RecordableObject, CheckableObject +from konova.models import generate_document_file_upload_path, AbstractDocument, Geometry, BaseObject, ShareableObjectMixin, \ + RecordableObjectMixin, CheckableObjectMixin from konova.settings import LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT, DEFAULT_SRID_RLP from user.models import UserAction, UserActionLogEntry -class Intervention(BaseObject, ShareableObject, RecordableObject, CheckableObject): +class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, CheckableObjectMixin): """ Interventions are e.g. construction sites where nature used to be. """ @@ -251,7 +253,6 @@ class InterventionDocument(AbstractDocument): if folder_path is not None: try: shutil.rmtree(folder_path) - pass except FileNotFoundError: # Folder seems to be missing already... pass diff --git a/konova/forms.py b/konova/forms.py index 3087c5fc..5a4e954c 100644 --- a/konova/forms.py +++ b/konova/forms.py @@ -22,7 +22,7 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ from konova.contexts import BaseContext -from konova.models import BaseObject, Geometry, RecordableObject +from konova.models import BaseObject, Geometry, RecordableObjectMixin from konova.settings import DEFAULT_SRID from konova.utils.message_templates import FORM_INVALID from user.models import UserActionLogEntry, UserAction @@ -290,7 +290,7 @@ class SimpleGeomForm(BaseForm): geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID)) geometry.modified = action geometry.save() - except (AttributeError) as e: + except AttributeError: # No geometry or linked instance holding a geometry exist --> create a new one! geometry = Geometry.objects.create( geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID)), @@ -446,7 +446,7 @@ class RecordModalForm(BaseModalForm): self.form_title = _("Unrecord data") self.form_caption = _("I, {} {}, confirm that this data must be unrecorded.").format(self.user.first_name, self.user.last_name) - if not isinstance(self.instance, RecordableObject): + if not isinstance(self.instance, RecordableObjectMixin): raise NotImplementedError def is_valid(self): diff --git a/konova/models/object.py b/konova/models/object.py index 7546e700..ef07b33e 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -71,7 +71,7 @@ class BaseResource(UuidModel): """ try: self.created.delete() - except (ObjectDoesNotExist, AttributeError) as e: + except (ObjectDoesNotExist, AttributeError): # Object does not exist anymore - we can skip this pass super().delete() @@ -146,7 +146,7 @@ class BaseObject(BaseResource): Returns: """ - if isinstance(self, ShareableObject): + if isinstance(self, ShareableObjectMixin): return self.users.filter(id=user.id) else: return User.objects.none() @@ -220,7 +220,7 @@ class BaseObject(BaseResource): return definitions[self.__class__]["template"].format(_str) -class RecordableObject(models.Model): +class RecordableObjectMixin(models.Model): """ Wraps record related fields and functionality """ @@ -289,7 +289,7 @@ class RecordableObject(models.Model): return ret_log_entry -class CheckableObject(models.Model): +class CheckableObjectMixin(models.Model): # Checks - Refers to "Genehmigen" but optional checked = models.OneToOneField( UserActionLogEntry, @@ -351,7 +351,7 @@ class CheckableObject(models.Model): return ret_log_entry -class ShareableObject(models.Model): +class ShareableObjectMixin(models.Model): # Users having access on this object users = models.ManyToManyField(User, help_text="Users having access (data shared with)") access_token = models.CharField( From d272d911ff1aef3e88c9c3854960f33383c7cf26 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 16 Nov 2021 08:58:15 +0100 Subject: [PATCH 05/13] Refactoring * create package from compensation/views --- compensation/account_urls.py | 2 +- compensation/comp_urls.py | 2 +- compensation/payment_urls.py | 2 +- compensation/views/__init__.py | 10 ++++++++++ 4 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 compensation/views/__init__.py diff --git a/compensation/account_urls.py b/compensation/account_urls.py index a2bfd6f8..e87fdf50 100644 --- a/compensation/account_urls.py +++ b/compensation/account_urls.py @@ -6,7 +6,7 @@ Created on: 24.08.21 """ from django.urls import path -from compensation.views.eco_account_views import * +from compensation.views import * urlpatterns = [ path("", index_view, name="acc-index"), diff --git a/compensation/comp_urls.py b/compensation/comp_urls.py index 8eb7c48d..4e603e47 100644 --- a/compensation/comp_urls.py +++ b/compensation/comp_urls.py @@ -6,7 +6,7 @@ Created on: 24.08.21 """ from django.urls import path -from compensation.views.compensation_views import * +from compensation.views import * urlpatterns = [ # Main compensation diff --git a/compensation/payment_urls.py b/compensation/payment_urls.py index d208b07d..5ec2b0a5 100644 --- a/compensation/payment_urls.py +++ b/compensation/payment_urls.py @@ -6,7 +6,7 @@ Created on: 24.08.21 """ from django.urls import path -from compensation.views.payment_views import * +from compensation.views import * urlpatterns = [ path('/new', new_payment_view, name='pay-new'), diff --git a/compensation/views/__init__.py b/compensation/views/__init__.py new file mode 100644 index 00000000..9b7c1c52 --- /dev/null +++ b/compensation/views/__init__.py @@ -0,0 +1,10 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 16.11.21 + +""" +from .compensation_views import * +from .eco_account_views import * +from .payment_views import * \ No newline at end of file From 0c6eba7c4b3ecbe5e0a0907907a8f0d5bdf96d81 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 16 Nov 2021 09:02:44 +0100 Subject: [PATCH 06/13] Refactoring * create package from compensation/urls * renames modules inside of compensation/urls and compensation/views --- compensation/urls/__init__.py | 8 ++++++++ compensation/{comp_urls.py => urls/compensation.py} | 2 +- compensation/{account_urls.py => urls/eco_account.py} | 2 +- compensation/{payment_urls.py => urls/payment.py} | 2 +- compensation/{ => urls}/urls.py | 6 +++--- compensation/views/__init__.py | 6 +++--- .../views/{compensation_views.py => compensation.py} | 2 +- .../views/{eco_account_views.py => eco_account.py} | 0 compensation/views/{payment_views.py => payment.py} | 0 9 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 compensation/urls/__init__.py rename compensation/{comp_urls.py => urls/compensation.py} (96%) rename compensation/{account_urls.py => urls/eco_account.py} (97%) rename compensation/{payment_urls.py => urls/payment.py} (89%) rename compensation/{ => urls}/urls.py (60%) rename compensation/views/{compensation_views.py => compensation.py} (99%) rename compensation/views/{eco_account_views.py => eco_account.py} (100%) rename compensation/views/{payment_views.py => payment.py} (100%) diff --git a/compensation/urls/__init__.py b/compensation/urls/__init__.py new file mode 100644 index 00000000..247de721 --- /dev/null +++ b/compensation/urls/__init__.py @@ -0,0 +1,8 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 16.11.21 + +""" +from .urls import * diff --git a/compensation/comp_urls.py b/compensation/urls/compensation.py similarity index 96% rename from compensation/comp_urls.py rename to compensation/urls/compensation.py index 4e603e47..f169bb9e 100644 --- a/compensation/comp_urls.py +++ b/compensation/urls/compensation.py @@ -6,7 +6,7 @@ Created on: 24.08.21 """ from django.urls import path -from compensation.views import * +from compensation.views.compensation import * urlpatterns = [ # Main compensation diff --git a/compensation/account_urls.py b/compensation/urls/eco_account.py similarity index 97% rename from compensation/account_urls.py rename to compensation/urls/eco_account.py index e87fdf50..9bdecf2c 100644 --- a/compensation/account_urls.py +++ b/compensation/urls/eco_account.py @@ -6,7 +6,7 @@ Created on: 24.08.21 """ from django.urls import path -from compensation.views import * +from compensation.views.eco_account import * urlpatterns = [ path("", index_view, name="acc-index"), diff --git a/compensation/payment_urls.py b/compensation/urls/payment.py similarity index 89% rename from compensation/payment_urls.py rename to compensation/urls/payment.py index 5ec2b0a5..0bb80938 100644 --- a/compensation/payment_urls.py +++ b/compensation/urls/payment.py @@ -6,7 +6,7 @@ Created on: 24.08.21 """ from django.urls import path -from compensation.views import * +from compensation.views.payment import * urlpatterns = [ path('/new', new_payment_view, name='pay-new'), diff --git a/compensation/urls.py b/compensation/urls/urls.py similarity index 60% rename from compensation/urls.py rename to compensation/urls/urls.py index 368a037b..58765bcb 100644 --- a/compensation/urls.py +++ b/compensation/urls/urls.py @@ -9,7 +9,7 @@ from django.urls import path, include app_name = "compensation" urlpatterns = [ - path("", include("compensation.comp_urls")), - path("acc/", include("compensation.account_urls")), - path("pay/", include("compensation.payment_urls")), + path("", include("compensation.urls.compensation")), + path("acc/", include("compensation.urls.eco_account")), + path("pay/", include("compensation.urls.payment")), ] \ No newline at end of file diff --git a/compensation/views/__init__.py b/compensation/views/__init__.py index 9b7c1c52..db03b5a1 100644 --- a/compensation/views/__init__.py +++ b/compensation/views/__init__.py @@ -5,6 +5,6 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 16.11.21 """ -from .compensation_views import * -from .eco_account_views import * -from .payment_views import * \ No newline at end of file +from .compensation import * +from .eco_account import * +from .payment import * diff --git a/compensation/views/compensation_views.py b/compensation/views/compensation.py similarity index 99% rename from compensation/views/compensation_views.py rename to compensation/views/compensation.py index a2a57b67..5bef5040 100644 --- a/compensation/views/compensation_views.py +++ b/compensation/views/compensation.py @@ -12,7 +12,7 @@ from compensation.tables import CompensationTable from intervention.models import Intervention from konova.contexts import BaseContext from konova.decorators import * -from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentForm +from konova.forms import RemoveModalForm, SimpleGeomForm from konova.utils.documents import get_document, remove_document from konova.utils.generators import generate_qr_code from konova.utils.message_templates import FORM_INVALID, IDENTIFIER_REPLACED, DATA_UNSHARED_EXPLANATION diff --git a/compensation/views/eco_account_views.py b/compensation/views/eco_account.py similarity index 100% rename from compensation/views/eco_account_views.py rename to compensation/views/eco_account.py diff --git a/compensation/views/payment_views.py b/compensation/views/payment.py similarity index 100% rename from compensation/views/payment_views.py rename to compensation/views/payment.py From e615a2fc011ba8a53489b4156a2c6c8a59c0793c Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 16 Nov 2021 11:57:06 +0100 Subject: [PATCH 07/13] Refactoring * drop SSO message sending for now from the logic --- intervention/forms/modalForms.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py index ef6fdf9e..b3fc9f3b 100644 --- a/intervention/forms/modalForms.py +++ b/intervention/forms/modalForms.py @@ -261,16 +261,6 @@ class CheckModalForm(BaseModalForm): with transaction.atomic(): self.instance.toggle_checked(self.user) - # Send message to the SSO server - messenger = Messenger( - self.instance.users.all(), - type="INFO", - ) - messenger.send_object_checked( - self.instance.identifier, - self.user, - ) - class NewDeductionModalForm(BaseModalForm): """ Form for creating new deduction From dafa53451fd2d1ed365949956b0a4c4c19a62aa2 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 16 Nov 2021 12:26:50 +0100 Subject: [PATCH 08/13] Refactoring * moves adding of revocation into Intervention model --- intervention/forms/modalForms.py | 28 +------------------ intervention/models/intervention.py | 42 ++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py index b3fc9f3b..480ac860 100644 --- a/intervention/forms/modalForms.py +++ b/intervention/forms/modalForms.py @@ -176,33 +176,7 @@ class NewRevocationModalForm(BaseModalForm): } def save(self): - with transaction.atomic(): - created_action = UserActionLogEntry.objects.create( - user=self.user, - action=UserAction.CREATED - ) - edited_action = UserActionLogEntry.objects.create( - user=self.user, - action=UserAction.EDITED - ) - revocation = Revocation.objects.create( - date=self.cleaned_data["date"], - legal=self.instance.legal, - comment=self.cleaned_data["comment"], - created=created_action, - ) - self.instance.modified = edited_action - self.instance.save() - self.instance.log.add(edited_action) - - if self.cleaned_data["file"]: - RevocationDocument.objects.create( - title="revocation_of_{}".format(self.instance.identifier), - date_of_creation=self.cleaned_data["date"], - comment=self.cleaned_data["comment"], - file=self.cleaned_data["file"], - instance=revocation - ) + revocation = self.instance.add_revocation(self) return revocation diff --git a/intervention/models/intervention.py b/intervention/models/intervention.py index 0d7f1d8c..4ae3d550 100644 --- a/intervention/models/intervention.py +++ b/intervention/models/intervention.py @@ -15,7 +15,7 @@ from django.utils.translation import gettext_lazy as _ from intervention.managers import InterventionManager from intervention.models.legal import Legal from intervention.models.responsibility import Responsibility -from intervention.models.revocation import RevocationDocument +from intervention.models.revocation import RevocationDocument, Revocation from intervention.utils.quality import InterventionQualityChecker from konova.models import generate_document_file_upload_path, AbstractDocument, Geometry, BaseObject, ShareableObjectMixin, \ RecordableObjectMixin, CheckableObjectMixin @@ -209,6 +209,46 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec self.save() return pay + def add_revocation(self, form): + """ Adds a new revocation to the intervention + + Args: + form (NewPaymentForm): The form holding the data + + Returns: + + """ + form_data = form.cleaned_data + user = form.user + with transaction.atomic(): + created_action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.CREATED + ) + edited_action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.EDITED + ) + revocation = Revocation.objects.create( + date=form_data["date"], + legal=self.legal, + comment=form_data["comment"], + created=created_action, + ) + self.modified = edited_action + self.save() + self.log.add(edited_action) + + if form_data["file"]: + RevocationDocument.objects.create( + title="revocation_of_{}".format(self.identifier), + date_of_creation=form_data["date"], + comment=form_data["comment"], + file=form_data["file"], + instance=revocation + ) + return revocation + class InterventionDocument(AbstractDocument): """ From 648bfba1bf6049897e3d39ebbd9eafa21c4bc1b5 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 16 Nov 2021 12:43:13 +0100 Subject: [PATCH 09/13] Refactoring * moves adding of deduction into Intervention and EcoAccount model * hardens against circular import issues --- compensation/models/compensation.py | 5 ++-- compensation/models/eco_account.py | 42 ++++++++++++++++++++++++++--- intervention/forms/modalForms.py | 39 +++------------------------ intervention/models/intervention.py | 37 ++++++++++++++++++++++++- 4 files changed, 79 insertions(+), 44 deletions(-) diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py index 7a33e1b3..499940c6 100644 --- a/compensation/models/compensation.py +++ b/compensation/models/compensation.py @@ -15,7 +15,6 @@ from django.utils.translation import gettext_lazy as _ from compensation.managers import CompensationManager from compensation.models import CompensationState, CompensationAction from compensation.utils.quality import CompensationQualityChecker -from intervention.models import Responsibility, Intervention from konova.models import BaseObject, AbstractDocument, Geometry, Deadline, generate_document_file_upload_path from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE from user.models import UserActionLogEntry, UserAction @@ -28,7 +27,7 @@ class AbstractCompensation(BaseObject): """ responsible = models.OneToOneField( - Responsibility, + "intervention.Responsibility", on_delete=models.SET_NULL, null=True, blank=True, @@ -186,7 +185,7 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin): Regular compensation, linked to an intervention """ intervention = models.ForeignKey( - Intervention, + "intervention.Intervention", on_delete=models.CASCADE, null=True, blank=True, diff --git a/compensation/models/eco_account.py b/compensation/models/eco_account.py index 2d3ae226..80cab9f9 100644 --- a/compensation/models/eco_account.py +++ b/compensation/models/eco_account.py @@ -9,7 +9,7 @@ import shutil from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator -from django.db import models +from django.db import models, transaction from django.db.models import Sum, QuerySet from django.utils.translation import gettext_lazy as _ @@ -19,7 +19,7 @@ from compensation.utils.quality import EcoAccountQualityChecker from konova.models import ShareableObjectMixin, RecordableObjectMixin, AbstractDocument, BaseResource, \ generate_document_file_upload_path from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE -from intervention.models import Intervention, Legal +from user.models import UserActionLogEntry, UserAction class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin): @@ -35,7 +35,7 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix ) legal = models.OneToOneField( - Legal, + "intervention.Legal", on_delete=models.SET_NULL, null=True, blank=True, @@ -164,6 +164,40 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix ) return docs + def add_deduction(self, form): + """ Adds a new deduction to the intervention + + Args: + form (NewDeductionModalForm): The form holding the data + + Returns: + + """ + form_data = form.cleaned_data + user = form.user + + with transaction.atomic(): + # Create log entry + user_action_edit = UserActionLogEntry.objects.create( + user=user, + action=UserAction.EDITED + ) + user_action_create = UserActionLogEntry.objects.create( + user=user, + action=UserAction.CREATED + ) + self.log.add(user_action_edit) + self.modified = user_action_edit + self.save() + + deduction = EcoAccountDeduction.objects.create( + intervention=form_data["intervention"], + account=self, + surface=form_data["surface"], + created=user_action_create, + ) + return deduction + class EcoAccountDocument(AbstractDocument): """ @@ -234,7 +268,7 @@ class EcoAccountDeduction(BaseResource): ] ) intervention = models.ForeignKey( - Intervention, + "intervention.Intervention", on_delete=models.CASCADE, null=True, blank=True, diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py index 480ac860..f246a598 100644 --- a/intervention/forms/modalForms.py +++ b/intervention/forms/modalForms.py @@ -290,14 +290,12 @@ class NewDeductionModalForm(BaseModalForm): super().__init__(*args, **kwargs) self.form_title = _("New Deduction") self.form_caption = _("Enter the information for a new deduction from a chosen eco-account") - self.is_intervention_initially = False # Check for Intervention or EcoAccount if isinstance(self.instance, Intervention): # Form has been called with a given intervention self.initialize_form_field("intervention", self.instance) self.disable_form_field("intervention") - self.is_intervention_initially = True elif isinstance(self.instance, EcoAccount): # Form has been called with a given account --> make it initial in the form and read-only self.initialize_form_field("account", self.instance) @@ -314,10 +312,7 @@ class NewDeductionModalForm(BaseModalForm): is_valid (bool) """ super_result = super().is_valid() - if self.is_intervention_initially: - acc = self.cleaned_data["account"] - else: - acc = self.instance + acc = self.cleaned_data["account"] if not acc.recorded: self.add_error( @@ -331,7 +326,7 @@ class NewDeductionModalForm(BaseModalForm): sum_surface_deductions = acc.get_deductions_surface() rest_surface = deductable_surface - sum_surface_deductions form_surface = float(self.cleaned_data["surface"]) - is_valid_surface = form_surface < rest_surface + is_valid_surface = form_surface <= rest_surface if not is_valid_surface: self.add_error( "surface", @@ -344,35 +339,7 @@ class NewDeductionModalForm(BaseModalForm): return is_valid_surface and super_result def save(self): - with transaction.atomic(): - # Create log entry - user_action_edit = UserActionLogEntry.objects.create( - user=self.user, - action=UserAction.EDITED - ) - user_action_create = UserActionLogEntry.objects.create( - user=self.user, - action=UserAction.CREATED - ) - self.instance.log.add(user_action_edit) - self.instance.modified = user_action_edit - self.instance.save() - - # Create deductions depending on Intervention or EcoAccount as the initial instance - if self.is_intervention_initially: - deduction = EcoAccountDeduction.objects.create( - intervention=self.instance, - account=self.cleaned_data["account"], - surface=self.cleaned_data["surface"], - created=user_action_create, - ) - else: - deduction = EcoAccountDeduction.objects.create( - intervention=self.cleaned_data["intervention"], - account=self.instance, - surface=self.cleaned_data["surface"], - created=user_action_create, - ) + deduction = self.instance.add_deduction(self) return deduction diff --git a/intervention/models/intervention.py b/intervention/models/intervention.py index 4ae3d550..64949559 100644 --- a/intervention/models/intervention.py +++ b/intervention/models/intervention.py @@ -12,6 +12,7 @@ from django.db import models, transaction from django.db.models import QuerySet from django.utils.translation import gettext_lazy as _ +from compensation.models import EcoAccountDeduction from intervention.managers import InterventionManager from intervention.models.legal import Legal from intervention.models.responsibility import Responsibility @@ -213,7 +214,7 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec """ Adds a new revocation to the intervention Args: - form (NewPaymentForm): The form holding the data + form (NewRevocationModalForm): The form holding the data Returns: @@ -249,6 +250,40 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec ) return revocation + def add_deduction(self, form): + """ Adds a new deduction to the intervention + + Args: + form (NewDeductionModalForm): The form holding the data + + Returns: + + """ + form_data = form.cleaned_data + user = form.user + + with transaction.atomic(): + # Create log entry + user_action_edit = UserActionLogEntry.objects.create( + user=user, + action=UserAction.EDITED + ) + user_action_create = UserActionLogEntry.objects.create( + user=user, + action=UserAction.CREATED + ) + self.log.add(user_action_edit) + self.modified = user_action_edit + self.save() + + deduction = EcoAccountDeduction.objects.create( + intervention=self, + account=form_data["account"], + surface=form_data["surface"], + created=user_action_create, + ) + return deduction + class InterventionDocument(AbstractDocument): """ From bd189c4a5446ed4ea3e9fa8cca448a86d05f7986 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 16 Nov 2021 12:51:10 +0100 Subject: [PATCH 10/13] Refactoring * moves updating of shared access users into Intervention --- intervention/forms/modalForms.py | 16 ++---- intervention/models/intervention.py | 19 +++++++ konova/models/object.py | 77 ++++++++++++++--------------- 3 files changed, 60 insertions(+), 52 deletions(-) diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py index f246a598..4d9abec4 100644 --- a/intervention/forms/modalForms.py +++ b/intervention/forms/modalForms.py @@ -10,16 +10,14 @@ from django.contrib.auth.models import User from django.db import transaction from django import forms from django.urls import reverse -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import gettext_lazy as _ -from compensation.models import EcoAccount, EcoAccountDeduction +from compensation.models import EcoAccount from intervention.inputs import TextToClipboardInput -from intervention.models import Revocation, RevocationDocument, Intervention, InterventionDocument +from intervention.models import Intervention, InterventionDocument from konova.forms import BaseModalForm, NewDocumentForm from konova.utils.general import format_german_float -from konova.utils.messenger import Messenger from konova.utils.user_checks import is_default_group_only -from user.models import UserActionLogEntry, UserAction class ShareInterventionModalForm(BaseModalForm): @@ -118,13 +116,7 @@ class ShareInterventionModalForm(BaseModalForm): ) def save(self): - still_accessing_users = self.cleaned_data["users"] - new_accessing_users = list(self.cleaned_data["user_select"].values_list("id", flat=True)) - accessing_users = still_accessing_users + new_accessing_users - users = User.objects.filter( - id__in=accessing_users - ) - self.instance.share_with_list(users) + self.instance.update_sharing_user(self) class NewRevocationModalForm(BaseModalForm): diff --git a/intervention/models/intervention.py b/intervention/models/intervention.py index 64949559..e75c215f 100644 --- a/intervention/models/intervention.py +++ b/intervention/models/intervention.py @@ -284,6 +284,25 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec ) return deduction + def update_sharing_user(self, form): + """ Adds a new user with shared access to the intervention + + Args: + form (ShareInterventionModalForm): The form holding the data + + Returns: + + """ + form_data = form.cleaned_data + + keep_accessing_users = form_data["users"] + new_accessing_users = list(form_data["user_select"].values_list("id", flat=True)) + accessing_users = keep_accessing_users + new_accessing_users + users = User.objects.filter( + id__in=accessing_users + ) + self.share_with_list(users) + class InterventionDocument(AbstractDocument): """ diff --git a/konova/models/object.py b/konova/models/object.py index ef07b33e..d62130cc 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -135,45 +135,6 @@ class BaseObject(BaseResource): ) self.log.add(user_action) - def is_shared_with(self, user: User): - """ Access check - - Checks whether a given user has access to this object - - Args: - user (): - - Returns: - - """ - if isinstance(self, ShareableObjectMixin): - return self.users.filter(id=user.id) - else: - return User.objects.none() - - def share_with(self, user: User): - """ Adds user to list of shared access users - - Args: - user (User): The user to be added to the object - - Returns: - - """ - if not self.is_shared_with(user): - self.users.add(user) - - def share_with_list(self, user_list: list): - """ Sets the list of shared access users - - Args: - user_list (list): The users to be added to the object - - Returns: - - """ - self.users.set(user_list) - def generate_new_identifier(self) -> str: """ Generates a new identifier for the intervention object @@ -398,4 +359,40 @@ class ShareableObjectMixin(models.Model): self.generate_access_token(make_unique, rec_depth) else: self.access_token = token - self.save() \ No newline at end of file + self.save() + + def is_shared_with(self, user: User): + """ Access check + + Checks whether a given user has access to this object + + Args: + user (): + + Returns: + + """ + return self.users.filter(id=user.id) + + def share_with(self, user: User): + """ Adds user to list of shared access users + + Args: + user (User): The user to be added to the object + + Returns: + + """ + if not self.is_shared_with(user): + self.users.add(user) + + def share_with_list(self, user_list: list): + """ Sets the list of shared access users + + Args: + user_list (list): The users to be added to the object + + Returns: + + """ + self.users.set(user_list) From 7f43f197d536472870b05d035127ad098d91ca55 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 16 Nov 2021 12:54:28 +0100 Subject: [PATCH 11/13] Refactoring * moves updating of shared access users into ShareableObjectMixin * renames ShareInterventionModalForm to ShareModalForm since it's used for EMA and eco accounts as well --- compensation/views/eco_account.py | 4 ++-- ema/views.py | 6 +++--- intervention/forms/modalForms.py | 2 +- intervention/models/intervention.py | 19 ------------------- intervention/views.py | 4 ++-- konova/models/object.py | 19 +++++++++++++++++++ 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/compensation/views/eco_account.py b/compensation/views/eco_account.py index 2c5340ab..ea058bf5 100644 --- a/compensation/views/eco_account.py +++ b/compensation/views/eco_account.py @@ -19,7 +19,7 @@ from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewEcoAccountDocumentForm from compensation.models import EcoAccount, EcoAccountDocument, CompensationState, CompensationAction from compensation.tables import EcoAccountTable -from intervention.forms.modalForms import NewDeductionModalForm, ShareInterventionModalForm +from intervention.forms.modalForms import NewDeductionModalForm, ShareModalForm from konova.contexts import BaseContext from konova.decorators import any_group_check, default_group_required, conservation_office_group_required, \ shared_access_required @@ -642,7 +642,7 @@ def create_share_view(request: HttpRequest, id: str): """ obj = get_object_or_404(EcoAccount, id=id) - form = ShareInterventionModalForm(request.POST or None, instance=obj, request=request) + form = ShareModalForm(request.POST or None, instance=obj, request=request) return form.process_request( request, msg_success=_("Share settings updated") diff --git a/ema/views.py b/ema/views.py index 783dc7c7..22f6e651 100644 --- a/ema/views.py +++ b/ema/views.py @@ -10,11 +10,11 @@ from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, from compensation.models import CompensationAction, CompensationState from ema.forms import NewEmaForm, EditEmaForm, NewEmaDocumentForm from ema.tables import EmaTable -from intervention.forms.modalForms import ShareInterventionModalForm +from intervention.forms.modalForms import ShareModalForm from konova.contexts import BaseContext from konova.decorators import conservation_office_group_required, shared_access_required from ema.models import Ema, EmaDocument -from konova.forms import RemoveModalForm, NewDocumentForm, SimpleGeomForm, RecordModalForm +from konova.forms import RemoveModalForm, SimpleGeomForm, RecordModalForm from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.utils.documents import get_document, remove_document from konova.utils.generators import generate_qr_code @@ -546,7 +546,7 @@ def create_share_view(request: HttpRequest, id: str): """ obj = get_object_or_404(Ema, id=id) - form = ShareInterventionModalForm(request.POST or None, instance=obj, request=request) + form = ShareModalForm(request.POST or None, instance=obj, request=request) return form.process_request( request, msg_success=_("Share settings updated") diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py index 4d9abec4..26aaa6b0 100644 --- a/intervention/forms/modalForms.py +++ b/intervention/forms/modalForms.py @@ -20,7 +20,7 @@ from konova.utils.general import format_german_float from konova.utils.user_checks import is_default_group_only -class ShareInterventionModalForm(BaseModalForm): +class ShareModalForm(BaseModalForm): url = forms.CharField( label=_("Share link"), label_suffix="", diff --git a/intervention/models/intervention.py b/intervention/models/intervention.py index e75c215f..64949559 100644 --- a/intervention/models/intervention.py +++ b/intervention/models/intervention.py @@ -284,25 +284,6 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec ) return deduction - def update_sharing_user(self, form): - """ Adds a new user with shared access to the intervention - - Args: - form (ShareInterventionModalForm): The form holding the data - - Returns: - - """ - form_data = form.cleaned_data - - keep_accessing_users = form_data["users"] - new_accessing_users = list(form_data["user_select"].values_list("id", flat=True)) - accessing_users = keep_accessing_users + new_accessing_users - users = User.objects.filter( - id__in=accessing_users - ) - self.share_with_list(users) - class InterventionDocument(AbstractDocument): """ diff --git a/intervention/views.py b/intervention/views.py index f79ae5f4..512285be 100644 --- a/intervention/views.py +++ b/intervention/views.py @@ -4,7 +4,7 @@ from django.http import HttpRequest, JsonResponse from django.shortcuts import render from intervention.forms.forms import NewInterventionForm, EditInterventionForm -from intervention.forms.modalForms import ShareInterventionModalForm, NewRevocationModalForm, \ +from intervention.forms.modalForms import ShareModalForm, NewRevocationModalForm, \ CheckModalForm, NewDeductionModalForm, NewInterventionDocumentForm from intervention.models import Intervention, Revocation, InterventionDocument, RevocationDocument from intervention.tables import InterventionTable @@ -402,7 +402,7 @@ def create_share_view(request: HttpRequest, id: str): """ intervention = get_object_or_404(Intervention, id=id) - form = ShareInterventionModalForm(request.POST or None, instance=intervention, request=request, user=request.user) + form = ShareModalForm(request.POST or None, instance=intervention, request=request, user=request.user) return form.process_request( request, msg_success=_("Share settings updated") diff --git a/konova/models/object.py b/konova/models/object.py index d62130cc..fce4d1bc 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -396,3 +396,22 @@ class ShareableObjectMixin(models.Model): """ self.users.set(user_list) + + def update_sharing_user(self, form): + """ Adds a new user with shared access to the object + + Args: + form (ShareModalForm): The form holding the data + + Returns: + + """ + form_data = form.cleaned_data + + keep_accessing_users = form_data["users"] + new_accessing_users = list(form_data["user_select"].values_list("id", flat=True)) + accessing_users = keep_accessing_users + new_accessing_users + users = User.objects.filter( + id__in=accessing_users + ) + self.share_with_list(users) From 96caebcae1f9cba6927401fdff52f48690678cc2 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 16 Nov 2021 13:15:15 +0100 Subject: [PATCH 12/13] Refactoring * adds simple getter methods for UserActionLogEntry * replaces manual creation of UserActionLogEntry with new methods --- compensation/forms/forms.py | 23 ++++-------- compensation/models/compensation.py | 33 +++++------------- compensation/models/eco_account.py | 12 ++----- ema/forms.py | 12 ++----- ema/tests/test_views.py | 7 ++-- intervention/forms/forms.py | 20 ++++------- intervention/models/intervention.py | 36 ++++++------------- intervention/tests/test_workflow.py | 5 +-- konova/forms.py | 20 +++-------- konova/models/object.py | 21 +++-------- konova/tests/test_views.py | 17 +++------ user/models/user_action.py | 54 +++++++++++++++++++++++++++++ 12 files changed, 108 insertions(+), 152 deletions(-) diff --git a/compensation/forms/forms.py b/compensation/forms/forms.py index 0616f4ff..85e8fd67 100644 --- a/compensation/forms/forms.py +++ b/compensation/forms/forms.py @@ -18,7 +18,7 @@ from compensation.models import Compensation, EcoAccount from intervention.inputs import GenerateInput from intervention.models import Intervention, Responsibility, Legal from konova.forms import BaseForm, SimpleGeomForm -from user.models import UserActionLogEntry, UserAction +from user.models import UserActionLogEntry class AbstractCompensationForm(BaseForm): @@ -210,10 +210,7 @@ class NewCompensationForm(AbstractCompensationForm, CEFCompensationFormMixin, Co comment = self.cleaned_data.get("comment", None) # Create log entry - action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.CREATED, - ) + action = UserActionLogEntry.get_created_action(user) # Process the geometry form geometry = geom_form.save(action) @@ -270,10 +267,7 @@ class EditCompensationForm(NewCompensationForm): comment = self.cleaned_data.get("comment", None) # Create log entry - action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.EDITED, - ) + action = UserActionLogEntry.get_edited_action(user) # Process the geometry form geometry = geom_form.save(action) @@ -364,10 +358,7 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix comment = self.cleaned_data.get("comment", None) # Create log entry - action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.CREATED, - ) + action = UserActionLogEntry.get_created_action(user) # Process the geometry form geometry = geom_form.save(action) @@ -444,10 +435,8 @@ class EditEcoAccountForm(NewEcoAccountForm): comment = self.cleaned_data.get("comment", None) # Create log entry - action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.EDITED, - ) + action = UserActionLogEntry.get_edited_action(user) + # Process the geometry form geometry = geom_form.save(action) diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py index 499940c6..3f00418a 100644 --- a/compensation/models/compensation.py +++ b/compensation/models/compensation.py @@ -17,7 +17,7 @@ from compensation.models import CompensationState, CompensationAction from compensation.utils.quality import CompensationQualityChecker from konova.models import BaseObject, AbstractDocument, Geometry, Deadline, generate_document_file_upload_path from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE -from user.models import UserActionLogEntry, UserAction +from user.models import UserActionLogEntry class AbstractCompensation(BaseObject): @@ -57,21 +57,16 @@ class AbstractCompensation(BaseObject): form_data = form.cleaned_data user = form.user with transaction.atomic(): - created_action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.CREATED - ) + created_action = UserActionLogEntry.get_created_action(user) + edited_action = UserActionLogEntry.get_edited_action(user, _("Added deadline")) + deadline = Deadline.objects.create( type=form_data["type"], date=form_data["date"], comment=form_data["comment"], created=created_action, ) - edited_action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.EDITED, - comment=_("Added deadline") - ) + self.modified = edited_action self.save() self.log.add(edited_action) @@ -90,10 +85,9 @@ class AbstractCompensation(BaseObject): form_data = form.cleaned_data user = form.user with transaction.atomic(): - user_action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.CREATED, - ) + user_action = UserActionLogEntry.get_created_action(user) + edited_action = UserActionLogEntry.get_edited_action(user, _("Added action")) + comp_action = CompensationAction.objects.create( action_type=form_data["action_type"], amount=form_data["amount"], @@ -101,11 +95,6 @@ class AbstractCompensation(BaseObject): comment=form_data["comment"], created=user_action, ) - edited_action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.EDITED, - comment=_("Added action"), - ) self.modified = edited_action self.save() self.log.add(edited_action) @@ -267,11 +256,7 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin): form_data = form.cleaned_data user = form.user with transaction.atomic(): - user_action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.EDITED, - comment=_("Added state") - ) + user_action = UserActionLogEntry.get_edited_action(user, _("Added state")) self.log.add(user_action) self.modified = user_action self.save() diff --git a/compensation/models/eco_account.py b/compensation/models/eco_account.py index 80cab9f9..ec612bc3 100644 --- a/compensation/models/eco_account.py +++ b/compensation/models/eco_account.py @@ -19,7 +19,7 @@ from compensation.utils.quality import EcoAccountQualityChecker from konova.models import ShareableObjectMixin, RecordableObjectMixin, AbstractDocument, BaseResource, \ generate_document_file_upload_path from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE -from user.models import UserActionLogEntry, UserAction +from user.models import UserActionLogEntry class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin): @@ -178,14 +178,8 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix with transaction.atomic(): # Create log entry - user_action_edit = UserActionLogEntry.objects.create( - user=user, - action=UserAction.EDITED - ) - user_action_create = UserActionLogEntry.objects.create( - user=user, - action=UserAction.CREATED - ) + user_action_create = UserActionLogEntry.get_created_action(user) + user_action_edit = UserActionLogEntry.get_edited_action(user) self.log.add(user_action_edit) self.modified = user_action_edit self.save() diff --git a/ema/forms.py b/ema/forms.py index f0f15163..cb7f0cc8 100644 --- a/ema/forms.py +++ b/ema/forms.py @@ -16,7 +16,7 @@ from compensation.forms.forms import AbstractCompensationForm, CompensationRespo from ema.models import Ema, EmaDocument from intervention.models import Responsibility from konova.forms import SimpleGeomForm, NewDocumentForm -from user.models import UserActionLogEntry, UserAction +from user.models import UserActionLogEntry class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin): @@ -59,10 +59,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin): comment = self.cleaned_data.get("comment", None) # Create log entry - action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.CREATED, - ) + action = UserActionLogEntry.get_created_action(user) # Process the geometry form geometry = geom_form.save(action) @@ -130,10 +127,7 @@ class EditEmaForm(NewEmaForm): comment = self.cleaned_data.get("comment", None) # Create log entry - action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.EDITED, - ) + action = UserActionLogEntry.get_edited_action(user) # Process the geometry form geometry = geom_form.save(action) diff --git a/ema/tests/test_views.py b/ema/tests/test_views.py index 45092883..3d853e7b 100644 --- a/ema/tests/test_views.py +++ b/ema/tests/test_views.py @@ -14,7 +14,7 @@ from ema.models import Ema from intervention.models import Responsibility from konova.models import Geometry from konova.settings import DEFAULT_GROUP, ETS_GROUP -from user.models import UserActionLogEntry, UserAction +from user.models import UserActionLogEntry class EmaViewTestCase(CompensationViewTestCase): @@ -61,10 +61,7 @@ class EmaViewTestCase(CompensationViewTestCase): def create_dummy_data(cls): # Create dummy data # Create log entry - action = UserActionLogEntry.objects.create( - user=cls.superuser, - action=UserAction.CREATED, - ) + action = UserActionLogEntry.get_created_action(cls.superuser) # Create responsible data object responsibility_data = Responsibility.objects.create() geometry = Geometry.objects.create() diff --git a/intervention/forms/forms.py b/intervention/forms/forms.py index d8f88979..fdf88403 100644 --- a/intervention/forms/forms.py +++ b/intervention/forms/forms.py @@ -10,7 +10,6 @@ from django import forms from django.contrib.auth.models import User from django.db import transaction from django.urls import reverse, reverse_lazy -from django.utils import timezone from django.utils.translation import gettext_lazy as _ from codelist.models import KonovaCode @@ -19,7 +18,7 @@ from codelist.settings import CODELIST_PROCESS_TYPE_ID, CODELIST_LAW_ID, \ from intervention.inputs import GenerateInput from intervention.models import Intervention, Legal, Responsibility from konova.forms import BaseForm, SimpleGeomForm -from user.models import UserActionLogEntry, UserAction +from user.models import UserActionLogEntry class NewInterventionForm(BaseForm): @@ -214,10 +213,7 @@ class NewInterventionForm(BaseForm): comment = self.cleaned_data.get("comment", None) # Create log entry - action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.CREATED, - ) + action = UserActionLogEntry.get_created_action(user) # Create legal data object (without M2M laws first) legal_data = Legal.objects.create( @@ -337,11 +333,7 @@ class EditInterventionForm(NewInterventionForm): self.instance.responsible.conservation_file_number = conservation_file_number self.instance.responsible.save() - user_action = UserActionLogEntry.objects.create( - user=user, - timestamp=timezone.now(), - action=UserAction.EDITED, - ) + user_action = UserActionLogEntry.get_edited_action(user) geometry = geom_form.save(user_action) self.instance.geometry = geometry @@ -356,8 +348,10 @@ class EditInterventionForm(NewInterventionForm): self.instance.save() # Uncheck and unrecord intervention due to changed data - self.instance.set_unchecked() - self.instance.set_unrecorded(user) + if self.instance.checked: + self.instance.set_unchecked() + if self.instance.recorded: + self.instance.set_unrecorded(user) return self.instance diff --git a/intervention/models/intervention.py b/intervention/models/intervention.py index 64949559..225c6bb7 100644 --- a/intervention/models/intervention.py +++ b/intervention/models/intervention.py @@ -21,7 +21,7 @@ from intervention.utils.quality import InterventionQualityChecker from konova.models import generate_document_file_upload_path, AbstractDocument, Geometry, BaseObject, ShareableObjectMixin, \ RecordableObjectMixin, CheckableObjectMixin from konova.settings import LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT, DEFAULT_SRID_RLP -from user.models import UserAction, UserActionLogEntry +from user.models import UserActionLogEntry class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, CheckableObjectMixin): @@ -189,15 +189,9 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec form_data = form.cleaned_data user = form.user with transaction.atomic(): - created_action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.CREATED, - ) - edited_action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.EDITED, - comment=_("Added payment"), - ) + created_action = UserActionLogEntry.get_created_action(user) + edited_action = UserActionLogEntry.get_edited_action(user, _("Added payment")) + pay = Payment.objects.create( created=created_action, amount=form_data.get("amount", -1), @@ -222,14 +216,9 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec form_data = form.cleaned_data user = form.user with transaction.atomic(): - created_action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.CREATED - ) - edited_action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.EDITED - ) + created_action = UserActionLogEntry.get_created_action(user) + edited_action = UserActionLogEntry.get_edited_action(user) + revocation = Revocation.objects.create( date=form_data["date"], legal=self.legal, @@ -264,14 +253,9 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec with transaction.atomic(): # Create log entry - user_action_edit = UserActionLogEntry.objects.create( - user=user, - action=UserAction.EDITED - ) - user_action_create = UserActionLogEntry.objects.create( - user=user, - action=UserAction.CREATED - ) + user_action_edit = UserActionLogEntry.get_edited_action(user) + user_action_create = UserActionLogEntry.get_created_action(user) + self.log.add(user_action_edit) self.modified = user_action_edit self.save() diff --git a/intervention/tests/test_workflow.py b/intervention/tests/test_workflow.py index f4484c6d..23a3c116 100644 --- a/intervention/tests/test_workflow.py +++ b/intervention/tests/test_workflow.py @@ -356,10 +356,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase): # Prepare the account for a working situation (enough deductable surface, recorded and shared) self.eco_account.deductable_surface = 10000.00 if self.eco_account.recorded is None: - rec_action = UserActionLogEntry.objects.create( - user=self.superuser, - action=UserAction.RECORDED - ) + rec_action = UserActionLogEntry.get_recorded_action(self.superuser) self.eco_account.recorded = rec_action self.eco_account.share_with_list([self.superuser]) self.eco_account.save() diff --git a/konova/forms.py b/konova/forms.py index 5a4e954c..5e644e5c 100644 --- a/konova/forms.py +++ b/konova/forms.py @@ -25,7 +25,7 @@ from konova.contexts import BaseContext from konova.models import BaseObject, Geometry, RecordableObjectMixin from konova.settings import DEFAULT_SRID from konova.utils.message_templates import FORM_INVALID -from user.models import UserActionLogEntry, UserAction +from user.models import UserActionLogEntry class BaseForm(forms.Form): @@ -168,11 +168,7 @@ class RemoveForm(BaseForm): if self.object_to_remove is not None and self.is_checked(): with transaction.atomic(): self.object_to_remove.is_active = False - action = UserActionLogEntry.objects.create( - user=user, - timestamp=timezone.now(), - action=UserAction.DELETED - ) + action = UserActionLogEntry.get_deleted_action(user) self.object_to_remove.deleted = action self.object_to_remove.save() return self.object_to_remove @@ -397,10 +393,9 @@ class NewDocumentForm(BaseModalForm): def save(self): with transaction.atomic(): - action = UserActionLogEntry.objects.create( - user=self.user, - action=UserAction.CREATED, - ) + action = UserActionLogEntry.get_created_action(self.user) + edited_action = UserActionLogEntry.get_edited_action(self.user, _("Added document")) + doc = self.document_model.objects.create( created=action, title=self.cleaned_data["title"], @@ -410,11 +405,6 @@ class NewDocumentForm(BaseModalForm): instance=self.instance, ) - edited_action = UserActionLogEntry.objects.create( - user=self.user, - action=UserAction.EDITED, - comment=_("Added document"), - ) self.instance.log.add(edited_action) self.instance.modified = edited_action self.instance.save() diff --git a/konova/models/object.py b/konova/models/object.py index fce4d1bc..9eeb3fbc 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -108,11 +108,7 @@ class BaseObject(BaseResource): return with transaction.atomic(): - action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.DELETED, - timestamp=timezone.now() - ) + action = UserActionLogEntry.get_deleted_action(user) self.deleted = action self.log.add(action) self.save() @@ -207,10 +203,7 @@ class RecordableObjectMixin(models.Model): Returns: """ - action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.UNRECORDED - ) + action = UserActionLogEntry.get_unrecorded_action(user) self.recorded = None self.save() self.log.add(action) @@ -225,10 +218,7 @@ class RecordableObjectMixin(models.Model): Returns: """ - action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.RECORDED - ) + action = UserActionLogEntry.get_recorded_action(user) self.recorded = action self.save() self.log.add(action) @@ -287,10 +277,7 @@ class CheckableObjectMixin(models.Model): Returns: """ - action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.CHECKED - ) + action = UserActionLogEntry.get_checked_action(user) self.checked = action self.save() self.log.add(action) diff --git a/konova/tests/test_views.py b/konova/tests/test_views.py index b3880623..ae5d4524 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -20,7 +20,7 @@ from konova.management.commands.setup_data import GROUPS_DATA from konova.models import Geometry from konova.settings import DEFAULT_GROUP from konova.utils.generators import generate_random_string -from user.models import UserActionLogEntry, UserAction +from user.models import UserActionLogEntry class BaseTestCase(TestCase): @@ -98,10 +98,7 @@ class BaseTestCase(TestCase): """ # Create dummy data # Create log entry - action = UserActionLogEntry.objects.create( - user=cls.superuser, - action=UserAction.CREATED, - ) + action = UserActionLogEntry.get_created_action(cls.superuser) # Create legal data object (without M2M laws first) legal_data = Legal.objects.create() # Create responsible data object @@ -131,10 +128,7 @@ class BaseTestCase(TestCase): cls.intervention = cls.create_dummy_intervention() # Create dummy data # Create log entry - action = UserActionLogEntry.objects.create( - user=cls.superuser, - action=UserAction.CREATED, - ) + action = UserActionLogEntry.get_created_action(cls.superuser) geometry = Geometry.objects.create() # Finally create main object, holding the other objects compensation = Compensation.objects.create( @@ -156,10 +150,7 @@ class BaseTestCase(TestCase): """ # Create dummy data # Create log entry - action = UserActionLogEntry.objects.create( - user=cls.superuser, - action=UserAction.CREATED, - ) + action = UserActionLogEntry.get_created_action(cls.superuser) geometry = Geometry.objects.create() # Create responsible data object lega_data = Legal.objects.create() diff --git a/user/models/user_action.py b/user/models/user_action.py index a37aab54..c1894f7d 100644 --- a/user/models/user_action.py +++ b/user/models/user_action.py @@ -67,3 +67,57 @@ class UserActionLogEntry(models.Model): if choice[0] == self.action: return choice[1] return None + + @classmethod + def get_created_action(cls, user: User, comment: str = None): + action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.CREATED, + comment=comment, + ) + return action + + @classmethod + def get_edited_action(cls, user: User, comment: str = None): + action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.EDITED, + comment=comment, + ) + return action + + @classmethod + def get_deleted_action(cls, user: User, comment: str = None): + action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.DELETED, + comment=comment, + ) + return action + + @classmethod + def get_checked_action(cls, user: User, comment: str = None): + action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.CHECKED, + comment=comment, + ) + return action + + @classmethod + def get_recorded_action(cls, user: User, comment: str = None): + action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.RECORDED, + comment=comment, + ) + return action + + @classmethod + def get_unrecorded_action(cls, user: User, comment: str = None): + action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.UNRECORDED, + comment=comment, + ) + return action From e501512a6356168e6914bdd4939cef9ecf30c280 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 16 Nov 2021 13:45:05 +0100 Subject: [PATCH 13/13] Refactoring * moves add_state from compensation to AbstractCompensation --- compensation/models/compensation.py | 57 ++++++++++++++--------------- compensation/models/payment.py | 1 - 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py index 3f00418a..7808187d 100644 --- a/compensation/models/compensation.py +++ b/compensation/models/compensation.py @@ -101,6 +101,34 @@ class AbstractCompensation(BaseObject): self.actions.add(comp_action) return comp_action + def add_state(self, form, is_before_state: bool) -> CompensationState: + """ Adds a new compensation state to the compensation + + Args: + form (NewStateModalForm): The form, holding all relevant data + is_before_state (bool): Whether this is a new before_state or after_state + + Returns: + + """ + form_data = form.cleaned_data + user = form.user + with transaction.atomic(): + user_action = UserActionLogEntry.get_edited_action(user, _("Added state")) + self.log.add(user_action) + self.modified = user_action + self.save() + + state = CompensationState.objects.create( + biotope_type=form_data["biotope_type"], + surface=form_data["surface"], + ) + if is_before_state: + self.before_states.add(state) + else: + self.after_states.add(state) + return state + def get_surface_after_states(self) -> float: """ Calculates the compensation's/account's surface @@ -243,35 +271,6 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin): ) return docs - def add_state(self, form, is_before_state: bool) -> CompensationState: - """ Adds a new compensation state to the compensation - - Args: - form (NewStateModalForm): The form, holding all relevant data - is_before_state (bool): Whether this is a new before_state or after_state - - Returns: - - """ - form_data = form.cleaned_data - user = form.user - with transaction.atomic(): - user_action = UserActionLogEntry.get_edited_action(user, _("Added state")) - self.log.add(user_action) - self.modified = user_action - self.save() - - state = CompensationState.objects.create( - biotope_type=form_data["biotope_type"], - surface=form_data["surface"], - ) - if is_before_state: - self.before_states.add(state) - else: - self.after_states.add(state) - return state - - class CompensationDocument(AbstractDocument): """ Specializes document upload for revocations with certain path diff --git a/compensation/models/payment.py b/compensation/models/payment.py index a630e36d..ec56910d 100644 --- a/compensation/models/payment.py +++ b/compensation/models/payment.py @@ -35,4 +35,3 @@ class Payment(BaseResource): ordering = [ "-amount", ] -