""" Author: Michel Peltriaux Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany Contact: michel.peltriaux@sgdnord.rlp.de Created on: 17.11.20 """ from django.contrib.auth.models import User from django.contrib.gis.db import models from django.utils.timezone import localtime 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 konova.models import BaseObject, Geometry, UuidModel, BaseResource from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE from konova.sub_settings.django_settings import DEFAULT_DATE_TIME_FORMAT from konova.utils import generators from organisation.models import Organisation from user.models import UserActionLogEntry class ResponsibilityData(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") comment = models.TextField(null=True, blank=True) document = models.ForeignKey("konova.Document", blank=True, null=True, on_delete=models.SET_NULL) def delete(self): # Make sure related objects are being removed as well if self.document: self.document.delete() super().delete() class LegalData(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, } ) law = models.ForeignKey( KonovaCode, on_delete=models.SET_NULL, null=True, related_name="+", blank=True, limit_choices_to={ "code_lists__in": [CODELIST_LAW_ID], "is_selectable": True, "is_archived": False, } ) revocation = models.ForeignKey(Revocation, null=True, blank=True, help_text="Refers to 'Widerspruch am'", on_delete=models.SET_NULL) def __str__(self): return "{} | {} | {}".format( self.process_type, self.law, self.id ) class Intervention(BaseObject): """ Interventions are e.g. construction sites where nature used to be. """ responsible = models.OneToOneField( ResponsibilityData, on_delete=models.SET_NULL, null=True, blank=True, related_name='+', help_text="Holds data on responsible organizations ('Zulassungsbehörde', 'Eintragungsstelle')" ) legal = models.OneToOneField( LegalData, on_delete=models.SET_NULL, null=True, blank=True, related_name='+', help_text="Holds data on legal dates or law" ) geometry = models.ForeignKey(Geometry, null=True, blank=True, on_delete=models.SET_NULL) documents = models.ManyToManyField("konova.Document", blank=True) # Checks - Refers to "Genehmigen" but optional checked = models.OneToOneField( UserActionLogEntry, on_delete=models.SET_NULL, null=True, blank=True, help_text="Holds data on user and timestamp of this action", related_name="+" ) # Refers to "verzeichnen" recorded = models.OneToOneField( UserActionLogEntry, on_delete=models.SET_NULL, null=True, blank=True, help_text="Holds data on user and timestamp of this action", related_name="+" ) # Users having access on this object users = models.ManyToManyField(User, help_text="Users having access (data shared with)") access_token = models.CharField( max_length=255, null=True, blank=True, help_text="Used for sharing access", ) def __str__(self): return "{} ({})".format(self.identifier, self.title) def generate_access_token(self, make_unique: bool = False, rec_depth: int = 5): """ Creates a new access token for the intervention Tokens are not used for identification of a table row. The share logic checks the intervention id as well as the given token. Therefore two different interventions can hold the same access_token without problems. For (possible) future changes to the share logic, the make_unique parameter may be used for checking whether the access_token is already used in any intervention. If so, tokens will be generated as long as a free token can be found. Args: make_unique (bool): Perform check on uniqueness over all intervention entries rec_depth (int): How many tries for generating a free random token (only if make_unique) Returns: """ # Make sure we won't end up in an infinite loop of trying to generate access_tokens rec_depth = rec_depth - 1 if rec_depth < 0 and make_unique: raise RuntimeError( "Access token generating for {} does not seem to find a free random token! Aborted!".format(self.id) ) # Create random token token = generators.generate_random_string(15) token_used_in = Intervention.objects.filter(access_token=token) # Make sure the token is not used anywhere as access_token, yet. # Make use of QuerySet lazy method for checking if it exists or not. if token_used_in and make_unique: self.generate_access_token(make_unique, rec_depth) else: self.access_token = token self.save() def save(self, *args, **kwargs): if self.identifier is None or len(self.identifier) == 0: # Create new identifier new_id = self._generate_new_identifier() while Intervention.objects.filter(identifier=new_id).exists(): new_id = self._generate_new_identifier() self.identifier = new_id super().save(*args, **kwargs) def quality_check(self) -> list: """ Quality check Returns: ret_msgs (list): Holds error messages """ ret_msgs = [] self._check_quality_responsible_data(ret_msgs) self._check_quality_legal_data(ret_msgs) # ToDo: Extend for more! return ret_msgs def _check_quality_responsible_data(self, ret_msgs: list): """ Checks data quality of related ResponsibilityData Args: ret_msgs (dict): Holds error messages Returns: """ try: # Check for file numbers if not self.responsible.registration_file_number or len(self.responsible.registration_file_number) == 0: ret_msgs.append(_("Registration office file number missing")) if not self.responsible.conservation_file_number or len(self.responsible.conservation_file_number) == 0: ret_msgs.append(_("Conversation office file number missing")) except AttributeError: # responsible data not found ret_msgs.append(_("Responsible data missing")) def _check_quality_legal_data(self, ret_msgs: list): """ Checks data quality of related LegalData Args: ret_msgs (dict): Holds error messages Returns: """ try: # Check for a revocation if self.legal.revocation: ret_msgs.append(_("Revocation exists")) if self.legal.registration_date is None: ret_msgs.append(_("Registration date missing")) if self.legal.binding_date is None: ret_msgs.append(_("Binding on missing")) except AttributeError: ret_msgs.append(_("Legal data missing")) 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, ) @property def recorded_tooltip(self): tooltip = _("Not recorded yet") if self.recorded: value = self.recorded.timestamp value = localtime(value) on = value.strftime(DEFAULT_DATE_TIME_FORMAT) tooltip = _("Recorded on {} by {}").format(on, self.recorded.user) return tooltip