Compare commits

..

3 Commits

Author SHA1 Message Date
da23761f88 #36 Quality checks
* adds check on recording of intervention in case of invalid compensations
2021-10-26 07:47:53 +02:00
4a4c9ad049 #36 Quality checks
* adds unchecking/unrecording of interventions in case of post-check|post-record editing
2021-10-25 17:39:39 +02:00
1c3ab898cc #36 Quality checks
* refactors toggling of recorded status into RecordableMixin
2021-10-25 17:01:02 +02:00
10 changed files with 182 additions and 56 deletions

View File

@ -22,7 +22,7 @@ from compensation.managers import CompensationStateManager, EcoAccountDeductionM
from compensation.utils.quality import CompensationQualityChecker, EcoAccountQualityChecker from compensation.utils.quality import CompensationQualityChecker, EcoAccountQualityChecker
from intervention.models import Intervention, ResponsibilityData, LegalData from intervention.models import Intervention, ResponsibilityData, LegalData
from konova.models import BaseObject, BaseResource, Geometry, UuidModel, AbstractDocument, \ from konova.models import BaseObject, BaseResource, Geometry, UuidModel, AbstractDocument, \
generate_document_file_upload_path generate_document_file_upload_path, RecordableMixin
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@ -221,11 +221,12 @@ class Compensation(AbstractCompensation):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.identifier is None or len(self.identifier) == 0: if self.identifier is None or len(self.identifier) == 0:
# Create new identifier # Create new identifier is none was given
new_id = self.generate_new_identifier() self.identifier = self.generate_new_identifier()
while Compensation.objects.filter(identifier=new_id).exists():
new_id = self.generate_new_identifier() # Before saving, make sure a given identifier has not been taken already in the meanwhile
self.identifier = new_id while Compensation.objects.filter(identifier=self.identifier).exclude(id=self.id).exists():
self.identifier = self.generate_new_identifier()
super().save(*args, **kwargs) super().save(*args, **kwargs)
def get_LANIS_link(self) -> str: def get_LANIS_link(self) -> str:
@ -310,7 +311,7 @@ class CompensationDocument(AbstractDocument):
pass pass
class EcoAccount(AbstractCompensation): class EcoAccount(AbstractCompensation, RecordableMixin):
""" """
An eco account is a kind of 'prepaid' compensation. It can be compared to an account that already has been filled 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. with some kind of currency. From this account one is able to deduct currency for current projects.
@ -368,11 +369,12 @@ class EcoAccount(AbstractCompensation):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.identifier is None or len(self.identifier) == 0: if self.identifier is None or len(self.identifier) == 0:
# Create new identifier # Create new identifier if none was given
new_id = self.generate_new_identifier() self.identifier = self.generate_new_identifier()
while EcoAccount.objects.filter(identifier=new_id).exists():
new_id = self.generate_new_identifier() # Before saving, make sure the given identifier is not used, yet
self.identifier = new_id while EcoAccount.objects.filter(identifier=self.identifier).exclude(id=self.id).exists():
self.identifier = self.generate_new_identifier()
super().save(*args, **kwargs) super().save(*args, **kwargs)
@property @property

View File

@ -7,12 +7,12 @@ from django.db.models import QuerySet
from compensation.models import AbstractCompensation from compensation.models import AbstractCompensation
from ema.managers import EmaManager from ema.managers import EmaManager
from ema.utils.quality import EmaQualityChecker from ema.utils.quality import EmaQualityChecker
from konova.models import AbstractDocument, generate_document_file_upload_path from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableMixin
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
class Ema(AbstractCompensation): class Ema(AbstractCompensation, RecordableMixin):
""" """
EMA = Ersatzzahlungsmaßnahme EMA = Ersatzzahlungsmaßnahme
(compensation actions from payments) (compensation actions from payments)

View File

@ -355,5 +355,9 @@ class EditInterventionForm(NewInterventionForm):
self.instance.modified = user_action self.instance.modified = user_action
self.instance.save() self.instance.save()
# Uncheck and unrecord intervention due to changed data
self.instance.set_unchecked(user)
self.instance.set_unrecorded(user)
return self.instance return self.instance

View File

@ -10,7 +10,6 @@ import shutil
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.gis.db import models from django.contrib.gis.db import models
from django.db.models import QuerySet from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_LAW_ID, \ from codelist.settings import CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_LAW_ID, \
@ -18,7 +17,7 @@ from codelist.settings import CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVA
from intervention.managers import InterventionManager from intervention.managers import InterventionManager
from intervention.utils.quality import InterventionQualityChecker from intervention.utils.quality import InterventionQualityChecker
from konova.models import BaseObject, Geometry, UuidModel, BaseResource, AbstractDocument, \ from konova.models import BaseObject, Geometry, UuidModel, BaseResource, AbstractDocument, \
generate_document_file_upload_path generate_document_file_upload_path, RecordableMixin, CheckableMixin
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT
from konova.utils import generators from konova.utils import generators
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@ -172,7 +171,7 @@ class LegalData(UuidModel):
revocation = models.OneToOneField(Revocation, null=True, blank=True, help_text="Refers to 'Widerspruch am'", on_delete=models.SET_NULL) revocation = models.OneToOneField(Revocation, null=True, blank=True, help_text="Refers to 'Widerspruch am'", on_delete=models.SET_NULL)
class Intervention(BaseObject): class Intervention(BaseObject, RecordableMixin, CheckableMixin):
""" """
Interventions are e.g. construction sites where nature used to be. Interventions are e.g. construction sites where nature used to be.
""" """
@ -274,11 +273,11 @@ class Intervention(BaseObject):
""" """
if self.identifier is None or len(self.identifier) == 0: if self.identifier is None or len(self.identifier) == 0:
# No identifier given # No identifier given by the user
self.identifier = self.generate_new_identifier() self.identifier = self.generate_new_identifier()
# Before saving, make sure the set identifier is not used, yet # Before saving, make sure the given identifier is not used in the meanwhile
while Intervention.objects.filter(identifier=self.identifier).exists(): while Intervention.objects.filter(identifier=self.identifier).exclude(id=self.id).exists():
self.identifier = self.generate_new_identifier() self.identifier = self.generate_new_identifier()
super().save(*args, **kwargs) super().save(*args, **kwargs)

View File

@ -15,7 +15,7 @@ from konova.sub_settings.django_settings import DEFAULT_DATE_FORMAT
from konova.utils.documents import remove_document, get_document from konova.utils.documents import remove_document, get_document
from konova.utils.generators import generate_qr_code from konova.utils.generators import generate_qr_code
from konova.utils.message_templates import INTERVENTION_INVALID, FORM_INVALID, IDENTIFIER_REPLACED, \ from konova.utils.message_templates import INTERVENTION_INVALID, FORM_INVALID, IDENTIFIER_REPLACED, \
DATA_UNSHARED_EXPLANATION DATA_UNSHARED_EXPLANATION, CHECKED_RECORDED_RESET
from konova.utils.user_checks import in_group from konova.utils.user_checks import in_group
@ -270,8 +270,13 @@ def edit_view(request: HttpRequest, id: str):
if request.method == "POST": if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid(): if data_form.is_valid() and geom_form.is_valid():
# The data form takes the geom form for processing, as well as the performing user # The data form takes the geom form for processing, as well as the performing user
# Save the current state of recorded|checked to inform the user in case of a status reset due to editing
i_rec = intervention.recorded is not None
i_check = intervention.checked is not None
intervention = data_form.save(request.user, geom_form) intervention = data_form.save(request.user, geom_form)
messages.success(request, _("Intervention {} edited").format(intervention.identifier)) messages.success(request, _("Intervention {} edited").format(intervention.identifier))
if i_check or i_rec:
messages.info(request, CHECKED_RECORDED_RESET)
return redirect("intervention:detail", id=intervention.id) return redirect("intervention:detail", id=intervention.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger",) messages.error(request, FORM_INVALID, extra_tags="danger",)

View File

@ -481,26 +481,29 @@ class RecordModalForm(BaseModalForm):
"confirm", "confirm",
msg msg
) )
# Special case: Intervention
# Add direct checks for related compensations
if isinstance(self.instance, Intervention):
self._are_compensations_valid()
return super_val and checker.valid return super_val and checker.valid
def _are_compensations_valid(self):
""" Runs a special case for intervention-compensations validity
Returns:
"""
comps = self.instance.compensations.all()
for comp in comps:
checker = comp.quality_check()
for msg in checker.messages:
self.add_error(
"confirm",
f"{comp.identifier}: {msg}"
)
def save(self): def save(self):
with transaction.atomic(): with transaction.atomic():
if self.cleaned_data["confirm"]: if self.cleaned_data["confirm"]:
if self.instance.recorded: self.instance.toggle_recorded(self.user)
# unrecord!
unrecord_action = UserActionLogEntry.objects.create(
user=self.user,
action=UserAction.UNRECORDED
)
# Do not delete the old .recorded attribute, since it shall stay in the .log list!
self.instance.recorded = None
self.instance.log.add(unrecord_action)
else:
record_action = UserActionLogEntry.objects.create(
user=self.user,
action=UserAction.RECORDED
)
self.instance.recorded = record_action
self.instance.log.add(record_action)
self.instance.save()
return self.instance return self.instance

View File

@ -313,3 +313,107 @@ class Geometry(BaseResource):
""" """
from konova.settings import DEFAULT_SRID from konova.settings import DEFAULT_SRID
geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID) geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID)
class RecordableMixin:
""" Mixin to be combined with BaseObject class
Provides functionality related to un/recording of data
"""
def set_unrecorded(self, user: User):
""" Perform unrecording
Args:
user (User): Performing user
Returns:
"""
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.UNRECORDED
)
self.recorded = None
self.save()
self.log.add(action)
def set_recorded(self, user: User):
""" Perform recording
Args:
user (User): Performing user
Returns:
"""
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.RECORDED
)
self.recorded = action
self.save()
self.log.add(action)
def toggle_recorded(self, user: User):
""" Un/Record intervention
Args:
user (User): Performing user
Returns:
"""
if not self.recorded:
self.set_recorded(user)
else:
self.set_unrecorded(user)
class CheckableMixin:
""" Mixin to be combined with BaseObject class
Provides functionality related to un/checking of data
"""
def set_unchecked(self, user: User):
""" Perform unrecording
Args:
Returns:
"""
self.checked = None
self.save()
def set_checked(self, user: User):
""" Perform checking
Args:
user (User): Performing user
Returns:
"""
action = UserActionLogEntry.objects.create(
user=user,
action=UserAction.CHECKED
)
self.checked = action
self.save()
self.log.add(action)
def toggle_checked(self, user: User):
""" Un/Record intervention
Args:
user (User): Performing user
Returns:
"""
if not self.checked:
self.set_checked(user)
else:
self.set_unchecked(user)

View File

@ -15,3 +15,5 @@ IDENTIFIER_REPLACED = _("The identifier '{}' had to be changed to '{}' since ano
DATA_UNSHARED = _("This data is not shared with you") DATA_UNSHARED = _("This data is not shared with you")
DATA_UNSHARED_EXPLANATION = _("Remember: This data has not been shared with you, yet. This means you can only read but can not edit or perform any actions like running a check or recording.") DATA_UNSHARED_EXPLANATION = _("Remember: This data has not been shared with you, yet. This means you can only read but can not edit or perform any actions like running a check or recording.")
MISSING_GROUP_PERMISSION = _("You need to be part of another user group.") MISSING_GROUP_PERMISSION = _("You need to be part of another user group.")
CHECKED_RECORDED_RESET = _("Status of Checked and Recorded reseted")

Binary file not shown.

View File

@ -19,7 +19,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-25 14:13+0200\n" "POT-Creation-Date: 2021-10-25 17:10+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -41,7 +41,8 @@ msgstr "Bis"
#: compensation/templates/compensation/detail/eco_account/view.html:58 #: compensation/templates/compensation/detail/eco_account/view.html:58
#: compensation/templates/compensation/report/eco_account/report.html:16 #: compensation/templates/compensation/report/eco_account/report.html:16
#: compensation/utils/quality.py:100 ema/templates/ema/detail/view.html:42 #: compensation/utils/quality.py:100 ema/templates/ema/detail/view.html:42
#: ema/templates/ema/report/report.html:16 intervention/forms/forms.py:101 #: ema/templates/ema/report/report.html:16 ema/utils/quality.py:26
#: intervention/forms/forms.py:101
#: intervention/templates/intervention/detail/view.html:56 #: intervention/templates/intervention/detail/view.html:56
#: intervention/templates/intervention/report/report.html:37 #: intervention/templates/intervention/report/report.html:37
#: intervention/utils/quality.py:49 #: intervention/utils/quality.py:49
@ -373,7 +374,8 @@ msgstr "Zusätzlicher Kommentar"
#: compensation/templates/compensation/detail/eco_account/view.html:62 #: compensation/templates/compensation/detail/eco_account/view.html:62
#: compensation/templates/compensation/report/eco_account/report.html:20 #: compensation/templates/compensation/report/eco_account/report.html:20
#: compensation/utils/quality.py:102 ema/templates/ema/detail/view.html:46 #: compensation/utils/quality.py:102 ema/templates/ema/detail/view.html:46
#: ema/templates/ema/report/report.html:20 intervention/forms/forms.py:129 #: ema/templates/ema/report/report.html:20 ema/utils/quality.py:28
#: intervention/forms/forms.py:129
#: intervention/templates/intervention/detail/view.html:60 #: intervention/templates/intervention/detail/view.html:60
#: intervention/templates/intervention/report/report.html:41 #: intervention/templates/intervention/report/report.html:41
#: intervention/utils/quality.py:42 #: intervention/utils/quality.py:42
@ -1030,7 +1032,8 @@ msgstr ""
"Die abbuchbare Fläche darf die Gesamtfläche der Zielzustände nicht " "Die abbuchbare Fläche darf die Gesamtfläche der Zielzustände nicht "
"überschreiten" "überschreiten"
#: compensation/utils/quality.py:104 intervention/utils/quality.py:55 #: compensation/utils/quality.py:104 ema/utils/quality.py:30
#: intervention/utils/quality.py:55
msgid "Responsible data" msgid "Responsible data"
msgstr "Daten zu den verantwortlichen Stellen" msgstr "Daten zu den verantwortlichen Stellen"
@ -1044,7 +1047,7 @@ msgstr "Kompensation {} bearbeitet"
#: compensation/views/compensation_views.py:216 #: compensation/views/compensation_views.py:216
#: compensation/views/eco_account_views.py:290 ema/views.py:178 #: compensation/views/eco_account_views.py:290 ema/views.py:178
#: intervention/views.py:447 #: intervention/views.py:448
msgid "Log" msgid "Log"
msgstr "Log" msgstr "Log"
@ -1098,16 +1101,16 @@ msgid "Deduction removed"
msgstr "Abbuchung entfernt" msgstr "Abbuchung entfernt"
#: compensation/views/eco_account_views.py:310 ema/views.py:252 #: compensation/views/eco_account_views.py:310 ema/views.py:252
#: intervention/views.py:487 #: intervention/views.py:488
msgid "{} unrecorded" msgid "{} unrecorded"
msgstr "{} entzeichnet" msgstr "{} entzeichnet"
#: compensation/views/eco_account_views.py:310 ema/views.py:252 #: compensation/views/eco_account_views.py:310 ema/views.py:252
#: intervention/views.py:487 #: intervention/views.py:488
msgid "{} recorded" msgid "{} recorded"
msgstr "{} verzeichnet" msgstr "{} verzeichnet"
#: compensation/views/eco_account_views.py:455 intervention/views.py:469 #: compensation/views/eco_account_views.py:455 intervention/views.py:470
msgid "Deduction added" msgid "Deduction added"
msgstr "Abbuchung hinzugefügt" msgstr "Abbuchung hinzugefügt"
@ -1436,39 +1439,43 @@ msgstr "Es existiert ein Widerspruch vom {}"
msgid "Intervention {} edited" msgid "Intervention {} edited"
msgstr "Eingriff {} bearbeitet" msgstr "Eingriff {} bearbeitet"
#: intervention/views.py:306 #: intervention/views.py:275
msgid "Status of Checked and Recorded reseted"
msgstr "'Geprüft' und 'Verzeichnet' sind zurückgesetzt worden"
#: intervention/views.py:307
msgid "{} removed" msgid "{} removed"
msgstr "{} entfernt" msgstr "{} entfernt"
#: intervention/views.py:327 #: intervention/views.py:328
msgid "Revocation removed" msgid "Revocation removed"
msgstr "Widerspruch entfernt" msgstr "Widerspruch entfernt"
#: intervention/views.py:353 #: intervention/views.py:354
msgid "{} has already been shared with you" msgid "{} has already been shared with you"
msgstr "{} wurde bereits für Sie freigegeben" msgstr "{} wurde bereits für Sie freigegeben"
#: intervention/views.py:358 #: intervention/views.py:359
msgid "{} has been shared with you" msgid "{} has been shared with you"
msgstr "{} ist nun für Sie freigegeben" msgstr "{} ist nun für Sie freigegeben"
#: intervention/views.py:365 #: intervention/views.py:366
msgid "Share link invalid" msgid "Share link invalid"
msgstr "Freigabelink ungültig" msgstr "Freigabelink ungültig"
#: intervention/views.py:386 #: intervention/views.py:387
msgid "Share settings updated" msgid "Share settings updated"
msgstr "Freigabe Einstellungen aktualisiert" msgstr "Freigabe Einstellungen aktualisiert"
#: intervention/views.py:405 #: intervention/views.py:406
msgid "Check performed" msgid "Check performed"
msgstr "Prüfung durchgeführt" msgstr "Prüfung durchgeführt"
#: intervention/views.py:425 #: intervention/views.py:426
msgid "Revocation added" msgid "Revocation added"
msgstr "Widerspruch hinzugefügt" msgstr "Widerspruch hinzugefügt"
#: intervention/views.py:492 #: intervention/views.py:493
msgid "There are errors on this intervention:" msgid "There are errors on this intervention:"
msgstr "Es liegen Fehler in diesem Eingriff vor:" msgstr "Es liegen Fehler in diesem Eingriff vor:"