Merge pull request 'user_action_log_introduction' (#2) from user_action_log_introduction into master

Reviewed-on: SGD-Nord/konova#2
This commit is contained in:
Michel Peltriaux 2021-07-30 12:22:02 +02:00
commit b7ab9f6f55
14 changed files with 224 additions and 73 deletions

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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)

View File

@ -117,7 +117,7 @@ class InterventionTableFilter(django_filters.FilterSet):
"""
if not value:
return queryset.filter(
recorded_on=None,
recorded=None,
)
else:
return queryset

View File

@ -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)

View File

@ -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,

View File

@ -64,41 +64,41 @@
</tr>
<tr>
<th scope="row">{% trans 'Process type' %}</th>
<td class="align-middle">{{intervention.process_type|default_if_none:""}}</td>
<td class="align-middle">{{intervention.legal.process_type|default_if_none:""}}</td>
</tr>
<tr>
<th scope="row">{% trans 'Law' %}</th>
<td class="align-middle">{{intervention.law|default_if_none:""}}</td>
<td class="align-middle">{{intervention.legal.law|default_if_none:""}}</td>
</tr>
<tr>
<th scope="row">{% trans 'Registration office' %}</th>
<td class="align-middle">{{intervention.registration_office|default_if_none:""}}</td>
<td class="align-middle">{{intervention.responsible.registration_office|default_if_none:""}}</td>
</tr>
<tr>
<th scope="row">{% trans 'Registration office file number' %}</th>
<td class="align-middle">{{intervention.registration_file_number|default_if_none:""}}</td>
<td class="align-middle">{{intervention.responsible.registration_file_number|default_if_none:""}}</td>
</tr>
<tr>
<th scope="row">{% trans 'Conservation office' %}</th>
<td class="align-middle">{{intervention.conservation_office|default_if_none:""}}</td>
<td class="align-middle">{{intervention.responsible.conservation_office|default_if_none:""}}</td>
</tr>
<tr>
<th scope="row">{% trans 'Conversation office file number' %}</th>
<td class="align-middle">{{intervention.conservation_file_number|default_if_none:""}}</td>
<td class="align-middle">{{intervention.responsible.conservation_file_number|default_if_none:""}}</td>
</tr>
<tr>
<th scope="row">{% trans 'Intervention handler' %}</th>
<td class="align-middle">{{intervention.handler|default_if_none:""}}</td>
<td class="align-middle">{{intervention.responsible.handler|default_if_none:""}}</td>
</tr>
<tr>
<th scope="row">{% trans 'Checked' %}</th>
<td class="align-middle">
{% if intervention.checked_on is None %}
{% if intervention.checked is None %}
<span>
{% fa5_icon 'star' 'far' %}
</span>
{% else %}
<span class="check-star">
<span class="check-star" title="{% trans 'Checked on '%} {{intervention.checked.timestamp}} {% trans 'by' %} {{intervention.checked.user}}">
{% fa5_icon 'star' %}
</span>
{% endif %}
@ -107,12 +107,12 @@
<tr>
<th scope="row">{% trans 'Recorded' %}</th>
<td class="align-middle">
{% if intervention.recorded_on is None %}
{% if intervention.recorded is None %}
<span>
{% fa5_icon 'bookmark' 'far' %}
</span>
{% else %}
<span class="registered-bookmark">
<span class="registered-bookmark" title="{% trans 'Recorded on '%} {{intervention.recorded.timestamp}} {% trans 'by' %} {{intervention.recorded.user}}">
{% fa5_icon 'bookmark' %}
</span>
{% endif %}
@ -120,11 +120,11 @@
</tr>
<tr>
<th scope="row">{% trans 'Registration date' %}</th>
<td class="align-middle">{{intervention.registration_date|default_if_none:""}}</td>
<td class="align-middle">{{intervention.legal.registration_date|default_if_none:""}}</td>
</tr>
<tr>
<th scope="row">{% trans 'Binding on' %}</th>
<td class="align-middle">{{intervention.binding_on|default_if_none:""}}</td>
<td class="align-middle">{{intervention.legal.binding_on|default_if_none:""}}</td>
</tr>
<tr>
<th scope="row">{% trans 'Last modified' %}</th>

View File

@ -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)

View File

@ -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"

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -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)