From 2fa28760909a81b03033175a5503e2f246dd1495 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 24 Jan 2022 14:41:56 +0100 Subject: [PATCH] #31 API POST Compensation * adds support for POST of new compensations * adds shared_users property to BaseObject and Compensation to simplify fetching of shared users (Compensation inherits from intervention) * extends compensation admin index * modifies compensation manager which led to invisibility of deleted entries in the admin backend * fixes bug in sanitize_db.py where CREATED useractions would be removed if they are not found on any log but still are used on the .created attribute of the objects --- api/utils/serializer/serializer.py | 1 - api/utils/serializer/v1/compensation.py | 207 +++++++++++++++++++++- api/utils/serializer/v1/intervention.py | 5 +- compensation/admin.py | 1 + compensation/managers.py | 4 +- compensation/models/compensation.py | 9 + konova/management/commands/sanitize_db.py | 8 +- konova/models/object.py | 12 +- 8 files changed, 237 insertions(+), 10 deletions(-) diff --git a/api/utils/serializer/serializer.py b/api/utils/serializer/serializer.py index 5febafc..c8e9083 100644 --- a/api/utils/serializer/serializer.py +++ b/api/utils/serializer/serializer.py @@ -52,7 +52,6 @@ class AbstractModelAPISerializer: """ raise NotImplementedError("Must be implemented in subclasses") - @abstractmethod def prepare_lookup(self, _id, user): """ Updates lookup dict for db fetching diff --git a/api/utils/serializer/v1/compensation.py b/api/utils/serializer/v1/compensation.py index 44d61c7..d06807e 100644 --- a/api/utils/serializer/v1/compensation.py +++ b/api/utils/serializer/v1/compensation.py @@ -5,8 +5,15 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 24.01.22 """ +from django.db import transaction + from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1 -from compensation.models import Compensation +from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID +from compensation.models import Compensation, CompensationAction, CompensationState, UnitChoices +from intervention.models import Intervention +from konova.models import Geometry, Deadline +from konova.tasks import celery_update_parcels +from user.models import UserActionLogEntry class CompensationAPISerializerV1(AbstractModelAPISerializerV1): @@ -31,4 +38,200 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1): self.properties_data["before_states"] = self.compensation_state_to_json(entry.before_states.all()) self.properties_data["after_states"] = self.compensation_state_to_json(entry.after_states.all()) self.properties_data["actions"] = self.compensation_actions_to_json(entry.actions.all()) - self.properties_data["deadlines"] = self.deadlines_to_json(entry.deadlines.all()) \ No newline at end of file + self.properties_data["deadlines"] = self.deadlines_to_json(entry.deadlines.all()) + + def initialize_objects(self, json_model, user): + """ Initializes all needed objects from the json_model data + + Does not persist data to the DB! + + Args: + json_model (dict): The json data + user (User): The API user + + Returns: + obj (Compensation) + """ + create_action = UserActionLogEntry.get_created_action(user, comment="API Import") + # Create geometry + json_geom = self.create_geometry_from_json(json_model) + geometry = Geometry() + geometry.geom = json_geom + geometry.created = create_action + + # Create linked objects + obj = Compensation() + created = create_action + obj.created = created + obj.geometry = geometry + return obj + + def set_intervention(self, obj, intervention_id, user): + """ Sets the linked intervention according to the given id + + Fails if no such intervention found or user has no shared access + + Args: + obj (Compensation): The Compensation object + intervention_id (str): The intervention's id + user (User): The API user + + Returns: + obj (Compensation) + """ + intervention = Intervention.objects.get( + id=intervention_id, + users__in=[user], + ) + obj.intervention = intervention + return obj + + def set_deadlines(self, obj, deadline_data): + found_deadlines = [] + for entry in deadline_data: + deadline_type = entry["type"] + date = entry["date"] + comment = entry["comment"] + + pre_existing_deadlines = obj.deadlines.filter( + type=deadline_type, + date=date, + comment=comment, + ).exclude( + id__in=found_deadlines + ) + if pre_existing_deadlines.count() > 0: + found_deadlines += pre_existing_deadlines.values_list("id", flat=True) + else: + # Create! + new_deadline = Deadline.objects.create( + type=deadline_type, + date=date, + comment=comment, + ) + obj.deadlines.add(new_deadline) + return obj + + def set_compensation_states(self, obj, states_data, states_manager): + found_states = [] + for entry in states_data: + biotope_type = entry["biotope"] + surface = float(entry["surface"]) + if surface <= 0: + raise ValueError("State surfaces must be > 0") + pre_existing_states = states_manager.filter( + biotope_type__atom_id=biotope_type, + surface=surface, + ).exclude( + id__in=found_states + ) + if pre_existing_states.count() > 0: + found_states += pre_existing_states.values_list("id", flat=True) + else: + # Create! + new_state = CompensationState.objects.create( + biotope_type=self.konova_code_from_json(biotope_type, CODELIST_BIOTOPES_ID), + surface=surface + ) + states_manager.add(new_state) + return obj + + def set_compensation_actions(self, obj, actions_data): + found_actions = [] + for entry in actions_data: + action = entry["action"] + amount = float(entry["amount"]) + unit = entry["unit"] + comment = entry["comment"] + + if amount <= 0: + raise ValueError("Action amount must be > 0") + if unit not in UnitChoices: + raise ValueError(f"Invalid unit. Choices are {UnitChoices.values}") + pre_existing_actions = obj.actions.filter( + action_type__atom_id=action, + amount=amount, + unit=unit, + comment=comment, + ).exclude( + id__in=found_actions + ) + if pre_existing_actions.count() > 0: + found_actions += pre_existing_actions.values_list("id", flat=True) + else: + # Create! + new_action = CompensationAction.objects.create( + action_type=self.konova_code_from_json(action, CODELIST_COMPENSATION_ACTION_ID), + amount=amount, + unit=unit, + comment=comment, + ) + obj.actions.add(new_action) + return obj + + def create_model_from_json(self, json_model, user): + """ Creates a new entry for the model based on the contents of json_model + + Args: + json_model (dict): The json containing data + user (User): The API user + + Returns: + created_id (str): The id of the newly created Compensation entry + """ + with transaction.atomic(): + obj = self.initialize_objects(json_model, user) + + # Fill in data to objects + properties = json_model["properties"] + obj.identifier = obj.generate_new_identifier() + obj.title = properties["title"] + obj.is_cef = properties["is_cef"] + obj.is_coherence_keeping = properties["is_coherence_keeping"] + obj = self.set_intervention(obj, properties["intervention"], user) + + obj.geometry.save() + obj.save() + + obj = self.set_compensation_actions(obj, properties["actions"]) + obj = self.set_compensation_states(obj, properties["before_states"], obj.before_states) + obj = self.set_compensation_states(obj, properties["after_states"], obj.after_states) + obj = self.set_deadlines(obj, properties["deadlines"]) + + obj.log.add(obj.created) + + celery_update_parcels.delay(obj.geometry.id) + + return obj.id + + def update_model_from_json(self, id, json_model, user): + """ Updates an entry for the model based on the contents of json_model + + Args: + id (str): The object's id + json_model (dict): The json containing data + user (User): The API user + + Returns: + created_id (str): The id of the newly created Compensation entry + """ + with transaction.atomic(): + obj = self.get_obj_from_db(id, user) + + # Fill in data to objects + properties = json_model["properties"] + obj.title = properties["title"] + self.set_responsibility(obj, properties["responsible"]) + self.set_legal(obj, properties["legal"]) + obj.geometry.geom = self.create_geometry_from_json(json_model) + + obj.responsible.save() + obj.geometry.save() + obj.legal.save() + obj.save() + + obj.users.add(user) + + celery_update_parcels.delay(obj.geometry.id) + + return obj.id diff --git a/api/utils/serializer/v1/intervention.py b/api/utils/serializer/v1/intervention.py index 7e5028e..8264408 100644 --- a/api/utils/serializer/v1/intervention.py +++ b/api/utils/serializer/v1/intervention.py @@ -46,16 +46,18 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1): Returns: obj (Intervention) """ + create_action = UserActionLogEntry.get_created_action(user, comment="API Import") # Create geometry json_geom = self.create_geometry_from_json(json_model) geometry = Geometry() geometry.geom = json_geom + geometry.created = create_action # Create linked objects obj = Intervention() resp = Responsibility() legal = Legal() - created = UserActionLogEntry.get_created_action(user, comment="API Import") + created = create_action obj.legal = legal obj.created = created obj.geometry = geometry @@ -131,6 +133,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1): obj.save() obj.users.add(user) + obj.log.add(obj.created) celery_update_parcels.delay(obj.geometry.id) diff --git a/compensation/admin.py b/compensation/admin.py index ced9bfe..735ffb4 100644 --- a/compensation/admin.py +++ b/compensation/admin.py @@ -29,6 +29,7 @@ class CompensationAdmin(BaseObjectAdmin): "identifier", "title", "created", + "deleted", ] diff --git a/compensation/managers.py b/compensation/managers.py index 61933ee..c97cd51 100644 --- a/compensation/managers.py +++ b/compensation/managers.py @@ -35,9 +35,7 @@ class CompensationManager(models.Manager): """ def get_queryset(self): - return super().get_queryset().filter( - deleted__isnull=True, - ).select_related( + return super().get_queryset().select_related( "modified", "intervention", "intervention__recorded", diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py index 6ebed85..59a3fd9 100644 --- a/compensation/models/compensation.py +++ b/compensation/models/compensation.py @@ -245,6 +245,15 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin): # Compensations inherit their shared state from the interventions return self.intervention.is_shared_with(user) + @property + def shared_users(self) -> QuerySet: + """ Shortcut for fetching the users which have shared access on this object + + Returns: + users (QuerySet) + """ + return self.intervention.users.all() + def get_LANIS_link(self) -> str: """ Generates a link for LANIS depending on the geometry diff --git a/konova/management/commands/sanitize_db.py b/konova/management/commands/sanitize_db.py index 0eecdc0..b5be834 100644 --- a/konova/management/commands/sanitize_db.py +++ b/konova/management/commands/sanitize_db.py @@ -10,7 +10,7 @@ from ema.models import Ema from intervention.models import Intervention from konova.management.commands.setup import BaseKonovaCommand from konova.models import Deadline, Geometry, Parcel, District -from user.models import UserActionLogEntry +from user.models import UserActionLogEntry, UserAction class Command(BaseKonovaCommand): @@ -55,7 +55,11 @@ class Command(BaseKonovaCommand): """ self._write_warning("=== Sanitize log entries ===") - all_log_entries = UserActionLogEntry.objects.all() + # Exclude created log entries from being cleaned, since they can be part of objects which do not have logs + # Being in a log (or not) is essential for this cleanup + all_log_entries = UserActionLogEntry.objects.all().exclude( + action=UserAction.CREATED + ) intervention_log_entries_ids = self.get_all_log_entries_ids(Intervention) attached_log_entries_id = intervention_log_entries_ids.union( diff --git a/konova/models/object.py b/konova/models/object.py index 0a83a48..5a7d6f1 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -10,6 +10,7 @@ import uuid from abc import abstractmethod from django.contrib import messages +from django.db.models import QuerySet from konova.tasks import celery_send_mail_shared_access_removed, celery_send_mail_shared_access_given, \ celery_send_mail_shared_data_recorded, celery_send_mail_shared_data_unrecorded, \ @@ -124,7 +125,7 @@ class BaseObject(BaseResource): self.log.add(action) # Send mail - shared_users = self.users.all().values_list("id", flat=True) + shared_users = self.shared_users.values_list("id", flat=True) for user_id in shared_users: celery_send_mail_shared_data_deleted.delay(self.identifier, user_id) @@ -464,6 +465,15 @@ class ShareableObjectMixin(models.Model): # Set new shared users self.share_with_list(users) + @property + def shared_users(self) -> QuerySet: + """ Shortcut for fetching the users which have shared access on this object + + Returns: + users (QuerySet) + """ + return self.users.all() + class GeoReferencedMixin(models.Model): geometry = models.ForeignKey("konova.Geometry", null=True, blank=True, on_delete=models.SET_NULL)