diff --git a/compensation/filters.py b/compensation/filters.py index 75d6085a..ab7a5fe7 100644 --- a/compensation/filters.py +++ b/compensation/filters.py @@ -18,6 +18,24 @@ class CompensationTableFilter(InterventionTableFilter): """ + def _filter_show_all(self, queryset, name, value) -> QuerySet: + """ Filters queryset depending on value of 'show_all' setting + + Args: + queryset (): + name (): + value (): + + Returns: + + """ + if not value: + return queryset.filter( + intervention__users__in=[self.user], # requesting user has access + ) + else: + return queryset + def _filter_show_recorded(self, queryset, name, value) -> QuerySet: """ Filters queryset depending on value of 'show_recorded' setting @@ -31,7 +49,7 @@ class CompensationTableFilter(InterventionTableFilter): """ if not value: return queryset.filter( - intervention__recorded_on=None, + intervention__recorded=None, ) else: return queryset diff --git a/compensation/models.py b/compensation/models.py index d7625d1e..e7c439b5 100644 --- a/compensation/models.py +++ b/compensation/models.py @@ -12,8 +12,8 @@ from django.utils import timezone from django.utils.timezone import now from compensation.settings import COMPENSATION_IDENTIFIER_LENGTH, COMPENSATION_IDENTIFIER_TEMPLATE -from intervention.models import Intervention -from konova.models import BaseObject, BaseResource, Geometry +from intervention.models import Intervention, ResponsibilityData +from konova.models import BaseObject, BaseResource, Geometry, UuidModel from konova.utils.generators import generate_random_string from organisation.models import Organisation @@ -73,18 +73,19 @@ class Compensation(BaseObject): The compensation holds information about which actions have to be performed until which date, who is in charge of this, which legal authority is the point of contact, and so on. """ - registration_office = models.ForeignKey(Organisation, on_delete=models.SET_NULL, null=True, related_name="+") - conservation_office = models.ForeignKey(Organisation, on_delete=models.SET_NULL, null=True, related_name="+") + responsible = models.OneToOneField( + ResponsibilityData, + on_delete=models.SET_NULL, + null=True, + blank=True, + help_text="Holds data on responsible organizations ('Zulassungsbehörde', 'Eintragungsstelle') and handler", + ) - ground_definitions = models.CharField(max_length=500, null=True, blank=True) # ToDo: Need to be M2M to laws! - action_definitions = models.CharField(max_length=500, null=True, blank=True) # ToDo: Need to be M2M to laws! + 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, help_text="Refers to 'Maßnahmen'") - before_states = models.ManyToManyField(CompensationState, blank=True, related_name='+') - after_states = models.ManyToManyField(CompensationState, blank=True, related_name='+') - actions = models.ManyToManyField(CompensationAction) - - deadline_creation = models.ForeignKey("konova.Deadline", on_delete=models.SET_NULL, null=True, blank=True, related_name="+") - deadline_maintaining = models.ForeignKey("konova.Deadline", on_delete=models.SET_NULL, null=True, blank=True, related_name="+") + deadlines = models.ManyToManyField("konova.Deadline", null=True, blank=True, related_name="+") geometry = models.ForeignKey(Geometry, null=True, blank=True, on_delete=models.SET_NULL) documents = models.ManyToManyField("konova.Document", blank=True) @@ -100,9 +101,6 @@ class Compensation(BaseObject): related_name='compensations' ) - # Users having access on this object - users = models.ManyToManyField(User) - @staticmethod def _generate_new_identifier() -> str: """ Generates a new identifier for the intervention object @@ -152,4 +150,5 @@ class EcoAccount(Compensation): 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 'withdraw' currency for current projects. """ - handler = models.CharField(max_length=500, null=True, blank=True, help_text="Who is responsible for handling the actions") + # Users having access on this object + users = models.ManyToManyField(User) diff --git a/compensation/tables.py b/compensation/tables.py index d187338f..0277b52b 100644 --- a/compensation/tables.py +++ b/compensation/tables.py @@ -34,13 +34,13 @@ class CompensationTable(BaseTable): verbose_name=_("Checked"), orderable=True, empty_values=[], - accessor="intervention__checked_on", + accessor="intervention__checked", ) r = tables.Column( verbose_name=_("Recorded"), orderable=True, empty_values=[], - accessor="intervention__recorded_on", + accessor="intervention__recorded", ) e = tables.Column( verbose_name=_("Editable"), @@ -90,7 +90,7 @@ class CompensationTable(BaseTable): def render_c(self, value, record: Compensation): """ Renders the checked column for a compensation - checked_on is set by the main object Intervention + checked is set by the main object Intervention Args: value (str): The identifier value @@ -103,9 +103,10 @@ class CompensationTable(BaseTable): checked = value is not None tooltip = _("Not checked yet") if checked: + value = value.timestamp value = localtime(value) checked_on = value.strftime(DEFAULT_DATE_TIME_FORMAT) - tooltip = _("Checked on {} by {}").format(checked_on, record.intervention.checked_by) + tooltip = _("Checked on {} by {}").format(checked_on, record.intervention.checked.user) html += self.render_checked_star( tooltip=tooltip, icn_filled=checked, @@ -126,9 +127,10 @@ class CompensationTable(BaseTable): checked = value is not None tooltip = _("Not registered yet") if checked: + value = value.timestamp value = localtime(value) on = value.strftime(DEFAULT_DATE_TIME_FORMAT) - tooltip = _("Registered on {} by {}").format(on, record.intervention.recorded_by) + tooltip = _("Registered on {} by {}").format(on, record.intervention.recorded.user) html += self.render_bookmark( tooltip=tooltip, icn_filled=checked, diff --git a/intervention/admin.py b/intervention/admin.py index 6d0df042..4adf095e 100644 --- a/intervention/admin.py +++ b/intervention/admin.py @@ -1,17 +1,38 @@ from django.contrib import admin -from intervention.models import Intervention +from intervention.models import Intervention, ResponsibilityData, LegalData class InterventionAdmin(admin.ModelAdmin): list_display = [ "id", "title", - "process_type", - "handler", "created_on", "deleted_on", ] +class ResponsibilityAdmin(admin.ModelAdmin): + list_display = [ + "id", + "registration_office", + "registration_file_number", + "conservation_office", + "conservation_file_number", + "handler", + ] + + +class LegalAdmin(admin.ModelAdmin): + list_display = [ + "id", + "process_type", + "law", + "registration_date", + "binding_date", + ] + + admin.site.register(Intervention, InterventionAdmin) +admin.site.register(ResponsibilityData, ResponsibilityAdmin) +admin.site.register(LegalData, LegalAdmin) diff --git a/intervention/filters.py b/intervention/filters.py index 7eb9553f..319a0992 100644 --- a/intervention/filters.py +++ b/intervention/filters.py @@ -117,7 +117,7 @@ class InterventionTableFilter(django_filters.FilterSet): """ if not value: return queryset.filter( - recorded_on=None, + recorded=None, ) else: return queryset diff --git a/intervention/models.py b/intervention/models.py index 6d72078e..67fd83a4 100644 --- a/intervention/models.py +++ b/intervention/models.py @@ -12,47 +12,84 @@ from django.utils import timezone from django.utils.timezone import now from intervention.settings import INTERVENTION_IDENTIFIER_LENGTH, INTERVENTION_IDENTIFIER_TEMPLATE -from konova.models import BaseObject, Geometry +from konova.models import BaseObject, Geometry, UuidModel from konova.utils.generators import generate_random_string 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(Organisation, on_delete=models.SET_NULL, null=True, related_name="+") + registration_file_number = models.CharField(max_length=1000, blank=True, null=True) + conservation_office = models.ForeignKey(Organisation, on_delete=models.SET_NULL, null=True, related_name="+") + 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'") + + +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.CharField(max_length=500, null=True, blank=True) + law = models.CharField(max_length=500, null=True, blank=True) class Intervention(BaseObject): """ Interventions are e.g. construction sites where nature used to be. """ - registration_office = models.ForeignKey(Organisation, on_delete=models.SET_NULL, null=True, related_name="+") - registration_file_number = models.CharField(max_length=1000, blank=True, null=True) - conservation_office = models.ForeignKey(Organisation, on_delete=models.SET_NULL, null=True, related_name="+") - conservation_file_number = models.CharField(max_length=1000, blank=True, null=True) - - process_type = models.CharField(max_length=500, null=True, blank=True) - law = models.CharField(max_length=500, null=True, blank=True) - handler = models.CharField(max_length=500, null=True, blank=True, help_text="Who is responsible for this intervention?") + 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 - checked_on = models.DateTimeField(default=None, null=True, blank=True) - checked_by = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, related_name='+') - - # Refers to "zugelassen am" - registration_date = models.DateField(null=True, blank=True) - - # Refers to "Bestandskraft am" - binding_on = models.DateField(null=True, 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_on = models.DateTimeField(default=None, null=True, blank=True) - recorded_by = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, related_name='+') + 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="+" + ) # Holds which intervention is simply a newer version of this dataset next_version = models.ForeignKey("Intervention", null=True, blank=True, on_delete=models.DO_NOTHING) - # Compensation or payments, one-directional - #payments = models.ManyToManyField(Payment, related_name="+", blank=True) - #compensations = models.ManyToManyField(Compensation, related_name="+", blank=True) - # Users having access on this object users = models.ManyToManyField(User) diff --git a/intervention/tables.py b/intervention/tables.py index 69d93b85..570eed41 100644 --- a/intervention/tables.py +++ b/intervention/tables.py @@ -33,13 +33,13 @@ class InterventionTable(BaseTable): verbose_name=_("Checked"), orderable=True, empty_values=[], - accessor="checked_on", + accessor="checked", ) r = tables.Column( verbose_name=_("Recorded"), orderable=True, empty_values=[], - accessor="recorded_on", + accessor="recorded", ) e = tables.Column( verbose_name=_("Editable"), @@ -110,9 +110,10 @@ class InterventionTable(BaseTable): checked = value is not None tooltip = _("Not checked yet") if checked: + value = value.timestamp value = localtime(value) checked_on = value.strftime(DEFAULT_DATE_TIME_FORMAT) - tooltip = _("Checked on {} by {}").format(checked_on, record.checked_by) + tooltip = _("Checked on {} by {}").format(checked_on, record.checked.user) html += self.render_checked_star( tooltip=tooltip, icn_filled=checked, @@ -133,9 +134,10 @@ class InterventionTable(BaseTable): checked = value is not None tooltip = _("Not registered yet") if checked: + value = value.timestamp value = localtime(value) on = value.strftime(DEFAULT_DATE_TIME_FORMAT) - tooltip = _("Registered on {} by {}").format(on, record.recorded_by) + tooltip = _("Registered on {} by {}").format(on, record.recorded.user) html += self.render_bookmark( tooltip=tooltip, icn_filled=checked, diff --git a/intervention/templates/intervention/detail/view.html b/intervention/templates/intervention/detail/view.html index 9cddd4a5..7354bca1 100644 --- a/intervention/templates/intervention/detail/view.html +++ b/intervention/templates/intervention/detail/view.html @@ -64,41 +64,41 @@ {% trans 'Process type' %} - {{intervention.process_type|default_if_none:""}} + {{intervention.legal.process_type|default_if_none:""}} {% trans 'Law' %} - {{intervention.law|default_if_none:""}} + {{intervention.legal.law|default_if_none:""}} {% trans 'Registration office' %} - {{intervention.registration_office|default_if_none:""}} + {{intervention.responsible.registration_office|default_if_none:""}} {% trans 'Registration office file number' %} - {{intervention.registration_file_number|default_if_none:""}} + {{intervention.responsible.registration_file_number|default_if_none:""}} {% trans 'Conservation office' %} - {{intervention.conservation_office|default_if_none:""}} + {{intervention.responsible.conservation_office|default_if_none:""}} {% trans 'Conversation office file number' %} - {{intervention.conservation_file_number|default_if_none:""}} + {{intervention.responsible.conservation_file_number|default_if_none:""}} {% trans 'Intervention handler' %} - {{intervention.handler|default_if_none:""}} + {{intervention.responsible.handler|default_if_none:""}} {% trans 'Checked' %} - {% if intervention.checked_on is None %} + {% if intervention.checked is None %} {% fa5_icon 'star' 'far' %} {% else %} - + {% fa5_icon 'star' %} {% endif %} @@ -107,12 +107,12 @@ {% trans 'Recorded' %} - {% if intervention.recorded_on is None %} + {% if intervention.recorded is None %} {% fa5_icon 'bookmark' 'far' %} {% else %} - + {% fa5_icon 'bookmark' %} {% endif %} @@ -120,11 +120,11 @@ {% trans 'Registration date' %} - {{intervention.registration_date|default_if_none:""}} + {{intervention.legal.registration_date|default_if_none:""}} {% trans 'Binding on' %} - {{intervention.binding_on|default_if_none:""}} + {{intervention.legal.binding_on|default_if_none:""}} {% trans 'Last modified' %} diff --git a/konova/admin.py b/konova/admin.py index 6fc31b76..e53a7fa3 100644 --- a/konova/admin.py +++ b/konova/admin.py @@ -7,7 +7,7 @@ Created on: 22.07.21 """ from django.contrib import admin -from konova.models import Geometry, Document +from konova.models import Geometry, Document, Deadline class GeometryAdmin(admin.ModelAdmin): @@ -28,5 +28,15 @@ class DocumentAdmin(admin.ModelAdmin): ] +class DeadlineAdmin(admin.ModelAdmin): + list_display = [ + "id", + "type", + "date", + "comment", + ] + + admin.site.register(Geometry, GeometryAdmin) admin.site.register(Document, DocumentAdmin) +admin.site.register(Deadline, DeadlineAdmin) diff --git a/konova/enums.py b/konova/enums.py index cabb9788..f2209350 100644 --- a/konova/enums.py +++ b/konova/enums.py @@ -47,3 +47,17 @@ class ServerMessageImportance(BaseEnum): DEFAULT = "DEFAULT" INFO = "INFO" WARNING = "WARNING" + + +class UserActionLogEntryEnum(BaseEnum): + """ + Defines different possible user actions for UserActionLogEntry + """ + CHECKED = "Checked" + RECORDED = "Recorded" + + +class DeadlineTypeEnum(BaseEnum): + MAINTAIN = "Maintain" + CONTROL = "Control" + OTHER = "Other" \ No newline at end of file diff --git a/konova/models.py b/konova/models.py index 0d4d79a7..e47764f7 100644 --- a/konova/models.py +++ b/konova/models.py @@ -12,17 +12,27 @@ from django.contrib.auth.models import User from django.contrib.gis.db.models import MultiPolygonField from django.db import models +from konova.enums import DeadlineTypeEnum from konova.settings import DEFAULT_SRID -class BaseResource(models.Model): +class UuidModel(models.Model): """ - A basic resource model, which defines attributes for every derived model + Encapsules identifying via uuid """ id = models.UUIDField( primary_key=True, default=uuid.uuid4, ) + + class Meta: + abstract = True + + +class BaseResource(UuidModel): + """ + A basic resource model, which defines attributes for every derived model + """ created_on = models.DateTimeField(auto_now_add=True, null=True) created_by = models.ForeignKey(User, null=True, on_delete=models.SET_NULL, related_name="+") @@ -50,8 +60,9 @@ class Deadline(BaseResource): """ Defines a deadline, which can be used to define dates with a semantic meaning """ - type = models.CharField(max_length=500, null=True, blank=True) + type = models.CharField(max_length=255, null=True, blank=True, choices=DeadlineTypeEnum.as_choices(drop_empty_choice=True)) date = models.DateField(null=True, blank=True) + comment = models.CharField(max_length=1000, null=True, blank=True) def __str__(self): return self.type diff --git a/konova/views.py b/konova/views.py index df11a1e0..e857d092 100644 --- a/konova/views.py +++ b/konova/views.py @@ -78,7 +78,7 @@ def home_view(request: HttpRequest): next_version=None, ) user_comps = comps.filter( - users__in=[user] + intervention__users__in=[user] ) eco_accs = EcoAccount.objects.filter( deleted_on=None, diff --git a/user/admin.py b/user/admin.py index fb8a5d68..9a633e77 100644 --- a/user/admin.py +++ b/user/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from user.models import UserNotification, KonovaUserExtension +from user.models import UserNotification, KonovaUserExtension, UserActionLogEntry class UserNotificationAdmin(admin.ModelAdmin): @@ -17,5 +17,15 @@ class KonovaUserExtensionAdmin(admin.ModelAdmin): ] +class UserActionLogEntryAdmin(admin.ModelAdmin): + list_display = [ + "id", + "user", + "timestamp", + "action", + ] + + admin.site.register(UserNotification, UserNotificationAdmin) admin.site.register(KonovaUserExtension, KonovaUserExtensionAdmin) +admin.site.register(UserActionLogEntry, UserActionLogEntryAdmin) \ No newline at end of file diff --git a/user/models.py b/user/models.py index 428b4f5b..588bf2f1 100644 --- a/user/models.py +++ b/user/models.py @@ -1,6 +1,9 @@ +import uuid + from django.contrib.auth.models import User from django.db import models +from konova.enums import UserActionLogEntryEnum from user.enums import UserNotificationEnum @@ -36,3 +39,27 @@ class KonovaUserExtension(models.Model): """ user = models.OneToOneField(User, on_delete=models.CASCADE) notifications = models.ManyToManyField(UserNotification, related_name="+") + + +class UserActionLogEntry(models.Model): + """ Wraps a user action log entry + + Can be used for workflow related attributes like checking or recording. + + """ + id = models.UUIDField( + primary_key=True, + default=uuid.uuid4, + ) + user = models.ForeignKey(User, related_name='+', on_delete=models.CASCADE, help_text="Performing user") + timestamp = models.DateTimeField(auto_now_add=True, help_text="Timestamp of performed action") + action = models.CharField( + max_length=255, + null=True, + blank=True, + help_text="Short name for performed action - optional", + choices=UserActionLogEntryEnum.as_choices(drop_empty_choice=True), + ) + + def __str__(self): + return "{} | {} | {}".format(self.user.username, self.timestamp, self.action) \ No newline at end of file