""" 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.db.models import QuerySet 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.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 konova.utils import generators 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) 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.legaldata.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 folder_path = self.file.path.split("/") 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 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, } ) laws = models.ManyToManyField( KonovaCode, blank=True, limit_choices_to={ "code_lists__in": [CODELIST_LAW_ID], "is_selectable": True, "is_archived": False, } ) revocation = models.OneToOneField(Revocation, null=True, blank=True, help_text="Refers to 'Widerspruch am'", on_delete=models.SET_NULL) class Intervention(BaseObject, ShareableObject, RecordableObject, CheckableObject): """ Interventions are e.g. construction sites where nature used to be. """ responsible = models.OneToOneField( ResponsibilityData, on_delete=models.SET_NULL, null=True, blank=True, help_text="Holds data on responsible organizations ('Zulassungsbehörde', 'Eintragungsstelle')" ) legal = models.OneToOneField( LegalData, on_delete=models.SET_NULL, null=True, blank=True, help_text="Holds data on legal dates or law" ) geometry = models.ForeignKey(Geometry, null=True, blank=True, on_delete=models.SET_NULL) objects = InterventionManager() def __str__(self): return "{} ({})".format(self.identifier, self.title) def save(self, *args, **kwargs): """ Custom save functionality Performs some pre-save checks: 1. Checking for existing identifiers Args: *args (): **kwargs (): Returns: """ if self.identifier is None or len(self.identifier) == 0: # No identifier given by the user self.identifier = self.generate_new_identifier() # Before saving, make sure the given identifier is not used in the meanwhile while Intervention.objects.filter(identifier=self.identifier).exclude(id=self.id).exists(): self.identifier = self.generate_new_identifier() super().save(*args, **kwargs) def delete(self, using=None, keep_parents=False): to_delete = [ self.legal, self.responsible, self.geometry, self.log.all() ] for entry in to_delete: try: entry.delete() except AttributeError: pass super().delete(using, keep_parents) def quality_check(self) -> InterventionQualityChecker: """ Quality check Returns: ret_msgs (list): Holds error messages """ checker = InterventionQualityChecker(obj=self) checker.run_check() return checker 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 area = int(geom.envelope.area) z_l = 16 for k_area, v_zoom in LANIS_ZOOM_LUT.items(): if k_area < area: z_l = v_zoom break zoom_lvl = z_l 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, QuerySet): """ Getter for all documents of an intervention Returns: revoc_docs (QuerySet): The queryset of a revocation document regular_docs (QuerySet): The queryset of regular other documents """ revoc_docs = RevocationDocument.objects.filter( instance=self.legal.revocation ) regular_docs = InterventionDocument.objects.filter( instance=self ) return revoc_docs, regular_docs class InterventionDocument(AbstractDocument): """ Specializes document upload for an intervention with certain path """ instance = models.ForeignKey( Intervention, 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 InterventionDocuments. 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.instance.get_documents() folder_path = None if revoc_docs.count() == 0 and other_intervention_docs.count() == 1: # The only file left for this intervention is the one which is currently processed and will be deleted # Make sure that the intervention 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