This commit is contained in:
mipel 2021-07-01 13:36:07 +02:00
commit c14e9466fb
91 changed files with 22395 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Project exclude paths
/venv/
/.idea/
*/migrations/

0
compensation/__init__.py Normal file
View File

46
compensation/admin.py Normal file
View File

@ -0,0 +1,46 @@
from django.contrib import admin
from compensation.models import Compensation, CompensationAction, CompensationState, CompensationControl
class CompensationControlAdmin(admin.ModelAdmin):
list_display = [
"id",
"type",
"deadline",
"expected_result",
"by_authority",
]
class CompensationStateAdmin(admin.ModelAdmin):
list_display = [
"id",
"biotope_type",
"amount",
"unit",
]
class CompensationActionAdmin(admin.ModelAdmin):
list_display = [
"id",
"action_type",
"amount",
"unit",
"control",
]
class CompensationAdmin(admin.ModelAdmin):
list_display = [
"id",
"type",
"created_on",
]
admin.site.register(Compensation, CompensationAdmin)
admin.site.register(CompensationAction, CompensationActionAdmin)
admin.site.register(CompensationState, CompensationStateAdmin)
admin.site.register(CompensationControl, CompensationControlAdmin)

5
compensation/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class CompensationConfig(AppConfig):
name = 'compensation'

14
compensation/forms.py Normal file
View File

@ -0,0 +1,14 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.12.20
"""
from konova.forms import BaseForm
class NewCompensationForm(BaseForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

145
compensation/models.py Normal file
View File

@ -0,0 +1,145 @@
"""
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 now
from compensation.settings import COMPENSATION_IDENTIFIER_LENGTH, COMPENSATION_IDENTIFIER_TEMPLATE
from konova.models import BaseObject, BaseResource
from konova.utils.generators import generate_random_string
from process.models import Process
class CompensationControl(BaseResource):
"""
Holds data on how a compensation shall be controlled
"""
deadline = models.ForeignKey("konova.Deadline", on_delete=models.SET_NULL, null=True, blank=True)
type = models.CharField(max_length=500, null=True, blank=True)
expected_result = models.CharField(max_length=500, null=True, blank=True, help_text="The expected outcome, that needs to be controlled")
by_authority = models.CharField(max_length=500, null=True, blank=True)
comment = models.TextField()
class CompensationState(models.Model):
"""
Compensations must define the state of an area before and after the compensation.
"""
biotope_type = models.CharField(max_length=500, null=True, blank=True)
amount = models.FloatField()
unit = models.CharField(max_length=100, null=True, blank=True)
class CompensationAction(BaseResource):
"""
Compensations include actions like planting trees, refreshing rivers and so on.
"""
action_type = models.CharField(max_length=500, null=True, blank=True)
amount = models.FloatField()
unit = models.CharField(max_length=100, null=True, blank=True)
control = models.ForeignKey(CompensationControl, on_delete=models.SET_NULL, null=True, blank=True)
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.
"""
is_old_law = models.BooleanField(default=False)
type = models.CharField(max_length=500, null=True, blank=True)
registration_office = models.CharField(max_length=500, null=True, blank=True) # ToDo: Really needed?
process = models.ForeignKey("process.Process", related_name="compensations", on_delete=models.CASCADE)
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!
actions = models.ManyToManyField(CompensationAction)
deadline_creation = models.ForeignKey("konova.Deadline", on_delete=models.SET_NULL, null=True, blank=True, related_name="deadline_creation")
deadline_maintaining = models.ForeignKey("konova.Deadline", on_delete=models.SET_NULL, null=True, blank=True, related_name="deadline_maintaining")
initial_states = models.ManyToManyField(CompensationState, blank=True, related_name='+')
final_states = models.ManyToManyField(CompensationState, blank=True, related_name='+')
geometry = models.MultiPolygonField(null=True, blank=True)
documents = models.ManyToManyField("konova.Document", blank=True)
def __str__(self):
return "{} of {}".format(self.type, self.process)
@staticmethod
def __generate_new_identifier() -> str:
""" Generates a new identifier for the intervention object
Returns:
str
"""
curr_month = str(now().month)
curr_year = str(now().year)
rand_str = generate_random_string(
length=COMPENSATION_IDENTIFIER_LENGTH,
only_numbers=True,
)
_str = "{}{}{}".format(curr_month, curr_year, rand_str)
return COMPENSATION_IDENTIFIER_TEMPLATE.format(_str)
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 Compensation.objects.filter(identifier=new_id).exists():
new_id = self.__generate_new_identifier()
self.identifier = new_id
super().save(*args, **kwargs)
@staticmethod
def get_role_objects(user: User, order_by: str = "-created_on"):
""" Returns objects depending on the currently selected role of the user
* REGISTRATIONOFFICE
* User can see the processes where registration_office is set to the organisation of the currently selected role
* User can see self-created processes
* LICENSINGOFFICE
* same
* DATAPROVIDER
* User can see only self-created processes
Args:
user (User): The performing user
order_by (str): Order by which Process attribute
Returns:
"""
role = user.current_role
if role is None:
return Compensation.objects.none()
processes = Process.get_role_objects(user, order_by)
processes.prefetch_related("compensations")
compensations = []
[compensations.extend(process.compensations.all()) for process in processes]
return compensations
class EcoAccount(BaseResource):
"""
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.
'Withdrawing' can only be applied by shrinking the size of the available geometry and declaring the withdrawed
geometry as a compensation for a process.
"""
is_old_law = models.BooleanField(default=False)
type = models.CharField(max_length=500, null=True, blank=True)
licensing_authority_document_identifier = models.CharField(max_length=500, null=True, blank=True)
registration_office = models.CharField(max_length=500, null=True, blank=True)
handler = models.CharField(max_length=500, null=True, blank=True)
handler_comments = models.TextField()
geometry = models.GeometryCollectionField()
documents = models.ManyToManyField("konova.Document")
initial_states = models.ManyToManyField(CompensationState, blank=True, related_name='+')
final_states = models.ManyToManyField(CompensationState, blank=True, related_name='+')
actions = models.ManyToManyField(CompensationAction)
deadline_maintaining = models.ForeignKey("konova.Deadline", on_delete=models.SET_NULL, null=True, blank=True)
deadline_other = models.ManyToManyField("konova.Deadline", blank=True, related_name='+')
comments = models.TextField()

9
compensation/settings.py Normal file
View File

@ -0,0 +1,9 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 18.12.20
"""
COMPENSATION_IDENTIFIER_LENGTH = 10
COMPENSATION_IDENTIFIER_TEMPLATE = "KOM-{}"

118
compensation/tables.py Normal file
View File

@ -0,0 +1,118 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 01.12.20
"""
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from konova.utils.tables import BaseTable
import django_tables2 as tables
class CompensationTable(BaseTable):
id = tables.Column(
verbose_name=_("Identifier"),
orderable=True,
accessor="identifier",
)
t = tables.Column(
verbose_name=_("Title"),
orderable=True,
accessor="title",
)
p = tables.Column(
verbose_name=_("Process"),
orderable=True,
accessor="process",
)
d = tables.Column(
verbose_name=_("Created on"),
orderable=True,
accessor="created_on",
)
ac = tables.Column(
verbose_name=_("Actions"),
orderable=False,
empty_values=[],
attrs={"td": {"class": "action-col"}}
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title = _("Compensations")
self.add_new_url = reverse("compensation:new")
def render_ac(self, value, record):
"""
Renders possible actions for this record, such as delete.
"""
intervention = _("Compensation")
html = ""
html += self.render_open_btn(
_("Open {}").format(intervention),
reverse("compensation:open", args=(record.id,)),
new_tab=True
)
html += self.render_edit_btn(
_("Edit {}").format(intervention),
reverse("compensation:edit", args=(record.id,)),
)
html += self.render_delete_btn(
_("Delete {}").format(intervention),
reverse("compensation:remove", args=(record.id,)),
)
return format_html(html)
class EcoAccountTable(BaseTable):
id = tables.Column(
verbose_name=_("Identifier"),
orderable=True,
accessor="identifier",
)
t = tables.Column(
verbose_name=_("Title"),
orderable=True,
accessor="title",
)
d = tables.Column(
verbose_name=_("Created on"),
orderable=True,
accessor="created_on",
)
ac = tables.Column(
verbose_name=_("Actions"),
orderable=False,
empty_values=[],
attrs={"td": {"class": "action-col"}}
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title = _("Eco Accounts")
self.add_new_url = reverse("compensation:account-new")
def render_ac(self, value, record):
"""
Renders possible actions for this record, such as delete.
"""
intervention = _("Compensation")
html = ""
html += self.render_open_btn(
_("Open {}").format(intervention),
reverse("compensation:open", args=(record.id,)),
new_tab=True
)
html += self.render_edit_btn(
_("Edit {}").format(intervention),
reverse("compensation:edit", args=(record.id,)),
)
html += self.render_delete_btn(
_("Delete {}").format(intervention),
reverse("compensation:remove", args=(record.id,)),
)
return format_html(html)

3
compensation/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

27
compensation/urls.py Normal file
View File

@ -0,0 +1,27 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 30.11.20
"""
from django.urls import path
from compensation.views import *
app_name = "compensation"
urlpatterns = [
# Main compensation
path("", index_view, name="index"),
path('new/', new_view, name='new'),
path('open/<id>', open_view, name='open'),
path('edit/<id>', edit_view, name='edit'),
path('remove/<id>', remove_view, name='remove'),
# Eco-account
path("account/", account_index_view, name="account-index"),
path('account/new/', account_new_view, name='account-new'),
path('account/open/<id>', account_open_view, name='account-open'),
path('account/edit/<id>', account_edit_view, name='account-edit'),
path('account/remove/<id>', account_remove_view, name='account-remove'),
]

110
compensation/views.py Normal file
View File

@ -0,0 +1,110 @@
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest
from django.shortcuts import render
from compensation.models import Compensation, EcoAccount
from compensation.tables import CompensationTable, EcoAccountTable
from konova.contexts import BaseContext
from konova.decorators import *
@login_required
@resolve_user_role
def index_view(request: HttpRequest):
"""
Renders the index view for compensation
Args:
request (HttpRequest): The incoming request
Returns:
A rendered view
"""
template = "generic_index.html"
user = request.user
compensations = Compensation.get_role_objects(user)
table = CompensationTable(
request=request,
queryset=compensations
)
context = {
"table": table,
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
def new_view(request: HttpRequest):
# ToDo
pass
@login_required
def edit_view(request: HttpRequest, id: str):
# ToDo
pass
@login_required
def open_view(request: HttpRequest, id: str):
# ToDo
pass
@login_required
def remove_view(request: HttpRequest, id: str):
# ToDo
pass
@login_required
def account_index_view(request: HttpRequest):
"""
Renders the index view for eco accounts
Args:
request (HttpRequest): The incoming request
Returns:
A rendered view
"""
template = "generic_index.html"
user = request.user
eco_accounts = EcoAccount.objects.filter(
created_by=user,
is_deleted=False,
)
table = EcoAccountTable(
request=request,
queryset=eco_accounts
)
context = {
"table": table,
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
def account_new_view(request: HttpRequest):
# ToDo
pass
@login_required
def account_edit_view(request: HttpRequest, id: str):
# ToDo
pass
@login_required
def account_open_view(request: HttpRequest, id: str):
# ToDo
pass
@login_required
def account_remove_view(request: HttpRequest, id: str):
# ToDo
pass

0
intervention/__init__.py Normal file
View File

18
intervention/admin.py Normal file
View File

@ -0,0 +1,18 @@
from django.contrib import admin
from intervention.models import Intervention
class InterventionAdmin(admin.ModelAdmin):
list_display = [
"id",
"title",
"type",
"handler",
"created_on",
"is_active",
"is_deleted",
]
admin.site.register(Intervention, InterventionAdmin)

5
intervention/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class InterventionConfig(AppConfig):
name = 'intervention'

220
intervention/forms.py Normal file
View File

@ -0,0 +1,220 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 02.12.20
"""
from dal import autocomplete
from django import forms
from django.contrib.auth.models import User
from django.contrib.gis import forms as gis_forms
from django.contrib.gis.geos import Polygon
from django.db import transaction
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from intervention.models import Intervention
from konova.forms import BaseForm
from konova.models import Document
from konova.settings import DEFAULT_LAT, DEFAULT_LON, DEFAULT_ZOOM
from organisation.models import Organisation
class NewInterventionForm(BaseForm):
identifier = forms.CharField(
label=_("Identifier"),
label_suffix="",
max_length=255,
help_text=_("Generated automatically if none was given"),
required=False,
)
title = forms.CharField(
label=_("Title"),
label_suffix="",
max_length=255,
)
type = forms.CharField(
label=_("Type"),
label_suffix="",
max_length=255,
help_text=_("Which intervention type is this"),
)
law = forms.CharField(
label=_("Law"),
label_suffix="",
max_length=255,
help_text=_("Based on which law"),
)
handler = forms.CharField(
label=_("Intervention handler"),
label_suffix="",
max_length=255,
help_text=_("Who performs the intervention"),
)
data_provider = forms.ModelChoiceField(
label=_("Data provider"),
label_suffix="",
help_text=_("Who provides the data for the intervention"),
queryset=Organisation.objects.all(),
widget=autocomplete.ModelSelect2(
url="other-orgs-autocomplete",
attrs={
"data-placeholder": _("Organization"),
"data-minimum-input-length": 3,
}
),
)
data_provider_detail = forms.CharField(
label=_("Data provider details"),
label_suffix="",
max_length=255,
help_text=_("Further details"),
required=False,
)
geometry = gis_forms.MultiPolygonField(
widget=gis_forms.OSMWidget(
attrs={
"default_lat": DEFAULT_LAT,
"default_lon": DEFAULT_LON,
"default_zoom": DEFAULT_ZOOM,
'map_width': 800,
'map_height': 500
},
),
label=_("Map"),
label_suffix="",
help_text=_("Where does the intervention take place")
)
documents = forms.FileField(
widget=forms.ClearableFileInput(
attrs={
"multiple": True,
}
),
label=_("Files"),
label_suffix="",
required=False,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New intervention")
self.action_url = reverse("intervention:new")
self.cancel_redirect = reverse("intervention:index")
def save(self, user: User):
with transaction.atomic():
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
_type = self.cleaned_data.get("type", None)
law = self.cleaned_data.get("law", None)
handler = self.cleaned_data.get("handler", None)
data_provider = self.cleaned_data.get("data_provider", None)
data_provider_detail = self.cleaned_data.get("data_provider_detail", None)
geometry = self.cleaned_data.get("geometry", Polygon())
documents = self.cleaned_data.get("documents", []) or []
intervention = Intervention(
identifier=identifier,
title=title,
type=_type,
law=law,
handler=handler,
data_provider=data_provider,
data_provider_detail=data_provider_detail,
geometry=geometry,
created_by=user,
)
intervention.save()
for doc in documents:
doc_obj = Document()
doc_obj.document = doc
# ToDo Add functionality for other attributes
doc_obj.save()
intervention.documents.add(doc_obj)
return intervention
class EditInterventionForm(NewInterventionForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance is not None:
self.action_url = reverse("intervention:edit", args=(self.instance.id,))
self.cancel_redirect = reverse("intervention:index")
self.form_title = _("Edit intervention")
self.form_caption = ""
# Initialize form data
form_data = {
"identifier": self.instance.identifier,
"title": self.instance.title,
"type": self.instance.type,
"law": self.instance.law,
"handler": self.instance.handler,
"data_provider": self.instance.data_provider,
"data_provider_detail": self.instance.data_provider_detail,
"geometry": self.instance.geometry,
"documents": self.instance.documents.all(),
}
disabled_fields = [
"identifier",
]
self.load_initial_data(
form_data,
disabled_fields,
)
def save(self, user: User):
""" Overwrite instance with new form data
Args:
user ():
Returns:
"""
with transaction.atomic():
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
_type = self.cleaned_data.get("type", None)
law = self.cleaned_data.get("law", None)
handler = self.cleaned_data.get("handler", None)
data_provider = self.cleaned_data.get("data_provider", None)
data_provider_detail = self.cleaned_data.get("data_provider_detail", None)
geometry = self.cleaned_data.get("geometry", Polygon())
documents = self.cleaned_data.get("documents", []) or []
self.instance.identifier = identifier
self.instance.title = title
self.instance.type = _type
self.instance.law = law
self.instance.handler = handler
self.instance.data_provider = data_provider
self.instance.data_provider_detail = data_provider_detail
self.instance.geometry = geometry
self.instance.save()
for doc in documents:
doc_obj = Document()
doc_obj.document = doc
# ToDo Add functionality for other attributes
doc_obj.save()
self.instance.documents.add(doc_obj)
return self.instance
class OpenInterventionForm(EditInterventionForm):
"""
This form is not intended to be used as data-input form. It's used to simplify the rendering of intervention:open
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Resize map
self.fields["geometry"].widget.attrs["map_width"] = 500
self.fields["geometry"].widget.attrs["map_height"] = 300
# Disable all form fields
for field in self.fields:
self.disable_form_field(field)

91
intervention/models.py Normal file
View File

@ -0,0 +1,91 @@
"""
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 now
from intervention.settings import INTERVENTION_IDENTIFIER_LENGTH, INTERVENTION_IDENTIFIER_TEMPLATE
from konova.models import BaseObject
from konova.utils.generators import generate_random_string
from organisation.enums import RoleTypeEnum
from process.models import Process
class Intervention(BaseObject):
"""
Interventions are e.g. construction sites where nature used to be.
A process consists of exactly one intervention and one or more compensation
"""
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)
data_provider = models.ForeignKey("organisation.Organisation", on_delete=models.SET_NULL, null=True, blank=True)
data_provider_detail = models.CharField(max_length=500, null=True, blank=True)
geometry = models.MultiPolygonField(null=True, blank=True)
process = models.OneToOneField("process.Process", on_delete=models.CASCADE, null=True, blank=True, related_name="intervention")
documents = models.ManyToManyField("konova.Document", blank=True)
def __str__(self):
return "{} by {}".format(self.type, self.handler)
def delete(self, *args, **kwargs):
if self.process is not None:
self.process.delete()
super().delete(*args, **kwargs)
@staticmethod
def __generate_new_identifier() -> str:
""" Generates a new identifier for the intervention object
Returns:
str
"""
curr_month = str(now().month)
curr_year = str(now().year)
rand_str = generate_random_string(
length=INTERVENTION_IDENTIFIER_LENGTH,
only_numbers=True,
)
_str = "{}{}{}".format(curr_month, curr_year, rand_str)
return INTERVENTION_IDENTIFIER_TEMPLATE.format(_str)
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)
@staticmethod
def get_role_objects(user: User, order_by: str = "-created_on") -> list:
""" Returns objects depending on the currently selected role of the user
* REGISTRATIONOFFICE
* User can see the processes where registration_office is set to the organisation of the currently selected role
* User can see self-created processes
* LICENSINGOFFICE
* same
* DATAPROVIDER
* User can see only self-created processes
Args:
user (User): The performing user
order_by (str): Order by which Process attribute
Returns:
"""
role = user.current_role
if role is None:
return Intervention.objects.none()
processes = Process.get_role_objects(user, order_by)
interventions = [process.intervention for process in processes]
return interventions

9
intervention/settings.py Normal file
View File

@ -0,0 +1,9 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 30.11.20
"""
INTERVENTION_IDENTIFIER_LENGTH = 10
INTERVENTION_IDENTIFIER_TEMPLATE = "EIV-{}"

87
intervention/tables.py Normal file
View File

@ -0,0 +1,87 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 01.12.20
"""
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from intervention.models import Intervention
from konova.utils.tables import BaseTable
import django_tables2 as tables
class InterventionTable(BaseTable):
id = tables.Column(
verbose_name=_("Identifier"),
orderable=True,
accessor="identifier",
)
t = tables.Column(
verbose_name=_("Title"),
orderable=True,
accessor="title",
)
p = tables.Column(
verbose_name=_("Process"),
orderable=True,
accessor="process",
)
d = tables.Column(
verbose_name=_("Created on"),
orderable=True,
accessor="created_on",
)
ac = tables.Column(
verbose_name=_("Actions"),
orderable=False,
empty_values=[],
attrs={"td": {"class": "action-col"}}
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title = _("Interventions")
self.add_new_url = reverse("intervention:new")
def render_id(self, value, record: Intervention):
""" Renders the id column for an intervention
Args:
value (str): The identifier value
record (Intervention): The intervention record
Returns:
"""
html = ""
html += self.render_link(
tooltip=_("Open {}").format(_("Intervention")),
href=reverse("intervention:open", args=(record.id,)),
txt=value,
new_tab=False,
)
return format_html(html)
def render_ac(self, value, record):
"""
Renders possible actions for this record, such as delete.
"""
intervention = _("Intervention")
html = ""
html += self.render_open_btn(
_("Open {}").format(intervention),
reverse("intervention:open", args=(record.id,))
)
html += self.render_edit_btn(
_("Edit {}").format(intervention),
reverse("intervention:edit", args=(record.id,)),
)
html += self.render_delete_btn(
_("Delete {}").format(intervention),
reverse("intervention:remove", args=(record.id,)),
)
return format_html(html)

View File

@ -0,0 +1,47 @@
{% extends 'base.html' %}
{% load i18n static %}
{% block body %}
<div class="rows">
<div class="columns">
<div class="large-10 column">
<h3>{% trans 'Intervention' %}: {{ intervention.title }}</h3>
</div>
<div class="large-2 column">
<a href="{% url 'intervention:edit' intervention.id %}">
<button class="button small" role="button" value="{% trans 'Edit' %}"><i class='fas fa-edit'></i> {% trans 'Edit' %}</button>
</a>
</div>
</div>
<hr>
{% comment %}
form.media needs to be loaded to ensure the openlayers client will be loaded properly
{% endcomment %}
{{ form.media }}
<div class="columns">
<div class="small-6 columns">
<table>
<tbody>
{% for field in form %}
{% if field != form.geometry %}
<tr title="{{ field.help_text }}" class="{% if field.errors %}error{% endif %}">
<th scope="row" class="small-3">
<div>{{ field.label }}</div>
<small>{{ field.help_text }}</small>
</th>
<td class="small-12">
{{ field }}
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
<div class="small-6 columns">
{{ form.geometry }}
</div>
</div>
</div>
{% endblock %}

3
intervention/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

19
intervention/urls.py Normal file
View File

@ -0,0 +1,19 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 30.11.20
"""
from django.urls import path
from intervention.views import index_view, new_view, open_view, edit_view, remove_view
app_name = "intervention"
urlpatterns = [
path("", index_view, name="index"),
path('new/', new_view, name='new'),
path('open/<id>', open_view, name='open'),
path('edit/<id>', edit_view, name='edit'),
path('remove/<id>', remove_view, name='remove'),
]

171
intervention/views.py Normal file
View File

@ -0,0 +1,171 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.utils.translation import gettext_lazy as _
from django.http import HttpRequest
from django.shortcuts import render, get_object_or_404, redirect
from django.urls import reverse
from intervention.forms import NewInterventionForm, EditInterventionForm, OpenInterventionForm
from intervention.models import Intervention
from intervention.tables import InterventionTable
from konova.contexts import BaseContext
from konova.decorators import *
from konova.forms import RemoveForm
from process.models import Process
@login_required
@resolve_user_role
def index_view(request: HttpRequest):
"""
Renders the index view for process
Args:
request (HttpRequest): The incoming request
Returns:
A rendered view
"""
template = "generic_index.html"
user = request.user
interventions = Intervention.get_role_objects(user)
table = InterventionTable(
request=request,
queryset=interventions
)
context = {
"table": table,
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
def new_view(request: HttpRequest):
"""
Renders a view for a new intervention creation
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "konova/form.html"
form = NewInterventionForm(request.POST or None)
if request.method == "POST":
if form.is_valid():
intervention = form.save(request.user)
if intervention.process is None:
# An intervention can not be created without a process -> automatically create a new process
process = Process.create_from_intervention(intervention)
messages.info(request, _("Interventions must be part of a process. Please fill in the missing data for the process"))
return redirect("process:edit", id=process.id)
else:
messages.success(request, _("Intervention {} added").format(intervention.title))
return redirect("intervention:index")
else:
messages.error(request, _("Invalid input"))
else:
# For clarification: nothing in this case
pass
context = {
"form": form,
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
def open_view(request: HttpRequest, id: str):
""" Renders a view for viewing an intervention's data
Args:
request (HttpRequest): The incoming request
id (str): The intervention's id
Returns:
"""
template = "intervention/open.html"
intervention = get_object_or_404(Intervention, id=id)
form = OpenInterventionForm(instance=intervention)
context = {
"intervention": intervention,
"form": form,
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
def edit_view(request: HttpRequest, id: str):
"""
Renders a view for editing interventions
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "konova/form.html"
intervention = get_object_or_404(Intervention, id=id)
if request.method == "POST":
form = EditInterventionForm(request.POST or None, instance=intervention)
if form.is_valid():
intervention = form.save(request.user)
messages.success(request, _("{} edited").format(intervention))
return redirect("intervention:index")
else:
messages.error(request, _("Invalid input"))
form = EditInterventionForm(instance=intervention)
context = {
"form": form,
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
def remove_view(request: HttpRequest, id: str):
""" Renders a remove view for this process
Args:
request (HttpRequest): The incoming request
id (str): The uuid id as string
Returns:
"""
template = "konova/form.html"
# Since an intervention is always organized inside a process, we will call the process removing routine, which
# disables all related elements by default
obj = get_object_or_404(Intervention, id=id)
process = obj.process
if request.method == "POST":
form = RemoveForm(
request.POST or None,
object_to_remove=obj,
remove_post_url=reverse("process:remove", args=(process.id,)),
cancel_url=reverse("intervention:index"),
)
if form.is_valid():
confirmed = form.is_checked()
if confirmed:
process.deactivate()
messages.success(request, _("Intervention {} removed").format(obj))
return redirect("intervention:index")
else:
messages.error(request, _("Invalid input"))
form = RemoveForm(
object_to_remove=obj,
remove_post_url=reverse("process:remove", args=(process.id,)),
cancel_url=reverse("intervention:index"),
)
context = {
"form": form,
}
context = BaseContext(request, context).context
return render(request, template, context)

0
kspneo/__init__.py Normal file
View File

32
kspneo/admin.py Normal file
View File

@ -0,0 +1,32 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 07.12.20
"""
from django.contrib import admin
from konova.models import RoleType, RoleGroup
class RoleTypeAdmin(admin.ModelAdmin):
list_display = [
"type",
"created_on",
"created_by",
]
class RoleGroupAdmin(admin.ModelAdmin):
list_display = [
"name",
"organisation",
"role",
]
admin.site.register(RoleType, RoleTypeAdmin)
admin.site.register(RoleGroup, RoleGroupAdmin)

16
kspneo/asgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
ASGI config for konova project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'konova.settings')
application = get_asgi_application()

42
kspneo/autocompletes.py Normal file
View File

@ -0,0 +1,42 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 07.12.20
"""
from dal_select2.views import Select2QuerySetView
from organisation.enums import OrganisationTypeEnum
from organisation.models import Organisation
class OrganisationAutocomplete(Select2QuerySetView):
def get_queryset(self):
if self.request.user.is_anonymous:
return Organisation.objects.none()
qs = Organisation.objects.all()
if self.q:
qs = qs.filter(name__icontains=self.q)
qs = qs.order_by(
"name"
)
return qs
class NonOfficialOrganisationAutocomplete(Select2QuerySetView):
def get_queryset(self):
if self.request.user.is_anonymous:
return Organisation.objects.none()
qs = Organisation.objects.all()
if self.q:
qs = qs.filter(
name__icontains=self.q,
)
qs = qs.exclude(
type=OrganisationTypeEnum.OFFICIAL.value
)
qs = qs.order_by(
"name"
)
return qs

53
kspneo/contexts.py Normal file
View File

@ -0,0 +1,53 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.20
"""
from django.http import HttpRequest
from konova.models import RoleGroup
from konova.sub_settings.context_settings import BASE_TITLE, WIKI_URL, BASE_FRONTEND_TITLE, RENDER_HEADER
from konova.utils.session import set_session_user_role
class BaseContext:
"""
Holds all base data which is needed for every context rendering
"""
context = {
"base_title": BASE_TITLE,
"base_frontend_title": BASE_FRONTEND_TITLE,
"language": "en",
"wiki_url": WIKI_URL,
"user": None,
"render_header": RENDER_HEADER,
"current_role": None,
}
def __init__(self, request: HttpRequest, additional_context: dict = {}):
self.context["language"] = request.LANGUAGE_CODE
self.context["user"] = request.user
self.__handle_current_role(request)
# Add additional context, derived from given parameters
self.context.update(additional_context)
def __handle_current_role(self, request: HttpRequest):
""" Reads/Writes current role from/to session
Args:
request (HttpRequest): The incoming request
Returns:
"""
# Store current role in session object to reduce amount of db access
current_role = request.session.get("current_role", {})
if len(current_role) == 0 and request.user.is_authenticated:
role_group = RoleGroup.get_users_role_groups(request.user).first()
current_role = set_session_user_role(request, role_group)
self.context["current_role"] = current_role

94
kspneo/decorators.py Normal file
View File

@ -0,0 +1,94 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.20
"""
from functools import wraps
from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from konova.models import RoleGroup
from konova.utils.session import get_session_user_role
from organisation.enums import RoleTypeEnum
from process.enums import PROCESS_EDITABLE_STATE
from process.models import Process
def staff_required(function):
"""
A decorator for functions which shall only be usable for staff members of the system
"""
@wraps(function)
def wrap(request, *args, **kwargs):
user = request.user
if user.is_staff:
return function(request, *args, **kwargs)
else:
messages.info(request, _("You need to be staff to perform this action!"))
return redirect(request.META.get("HTTP_REFERER", reverse("home")))
return wrap
def superuser_required(function):
"""
A decorator for functions which shall only be usable for superusers of the system
"""
@wraps(function)
def wrap(request, *args, **kwargs):
user = request.user
if user.is_superuser:
return function(request, *args, **kwargs)
else:
messages.info(request, _("You need to be administrator to perform this action!"))
return redirect(request.META.get("HTTP_REFERER", reverse("home")))
return wrap
def resolve_user_role(function):
"""
A decorator for functions to resolve the current user role and store it in the user object
"""
@wraps(function)
def wrap(request, *args, **kwargs):
user = request.user
role = get_session_user_role(request)
try:
role = RoleGroup.objects.get(id=role.get("id", -1))
user.current_role = role
except ObjectDoesNotExist:
user.current_role = None
return function(request, *args, **kwargs)
return wrap
def valid_process_role_required(function):
"""
A decorator for functions to check whether the user has a valid role selected
"""
@wraps(function)
def wrap(request, *args, **kwargs):
user = request.user
if user.current_role is None:
role = get_session_user_role(request)
else:
role = user.current_role
try:
process = Process.objects.get(id=kwargs.get("id"))
editable = PROCESS_EDITABLE_STATE.get(process.state)
role_enum = RoleTypeEnum[role.role.type]
if role_enum in editable:
return function(request, *args, **kwargs)
else:
messages.error(request, _("Your current role is not allowed to do this"))
return redirect(request.META.get("HTTP_REFERER", "home"))
except ObjectDoesNotExist:
process = None
return function(request, *args, **kwargs)
return wrap

40
kspneo/enums.py Normal file
View File

@ -0,0 +1,40 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 17.11.20
"""
from enum import Enum
class BaseEnum(Enum):
""" Provides basic functionality for Enums
"""
@classmethod
def as_choices(cls, drop_empty_choice: bool = False):
empty_choice = [] if drop_empty_choice else [(None, "---")]
choices = empty_choice + [(enum.value, enum.name) for enum in cls]
return choices
class UnitEnum(BaseEnum):
"""
Predefines units for selection
"""
mm = "mm"
dm = "dm"
cm = "cm"
m = "m"
km = "km"
qmm = "qmm"
qdm = "qdm"
qcm = "qcm"
qm = "qm"
qkm = "qkm"
ha = "ha"
st = "St." # pieces

141
kspneo/forms.py Normal file
View File

@ -0,0 +1,141 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.20
"""
from abc import abstractmethod
from django import forms
from django.http import HttpRequest
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from konova.models import RoleGroup
from konova.utils.session import set_session_user_role
from organisation.settings import ROLE_TYPE_STRINGS
class BaseForm(forms.Form):
"""
Basic form for that holds attributes needed in all other forms
"""
action_url = None
form_title = None
cancel_redirect = None
form_caption = None
instance = None # The data holding model object
def __init__(self, *args, **kwargs):
self.instance = kwargs.pop("instance", None)
super().__init__(*args, **kwargs)
@abstractmethod
def save(self):
# To be implemented in subclasses!
pass
def disable_form_field(self, field: str):
"""
Disables a form field for user editing
"""
self.fields[field].widget.attrs["readonly"] = True
self.fields[field].disabled = True
self.fields[field].widget.attrs["title"] = _("Not editable")
def initialize_form_field(self, field: str, val):
"""
Initializes a form field with a value
"""
self.fields[field].initial = val
def load_initial_data(self, form_data: dict, disabled_fields: list):
""" Initializes form data from instance
Inserts instance data into form and disables form fields
Returns:
"""
if self.instance is None:
return
for k, v in form_data.items():
self.initialize_form_field(k, v)
for field in disabled_fields:
self.disable_form_field(field)
class RemoveForm(BaseForm):
check = forms.BooleanField(
label=_("Confirm"),
label_suffix=_(""),
required=True,
)
def __init__(self, *args, **kwargs):
self.object_to_remove = kwargs.pop("object_to_remove", None)
self.remove_post_url = kwargs.pop("remove_post_url", "")
self.cancel_url = kwargs.pop("cancel_url", "")
super().__init__(*args, **kwargs)
self.form_title = _("Remove")
if self.object_to_remove is not None:
self.form_caption = _("You are about to remove {} {}").format(self.object_to_remove.__class__.__name__, self.object_to_remove)
self.action_url = self.remove_post_url
self.cancel_redirect = self.cancel_url
def is_checked(self) -> bool:
return self.cleaned_data.get("check", False)
def save(self):
if self.object_to_remove is not None and self.is_checked():
self.object_to_remove.is_active = False
self.object_to_remove.is_deleted = True
self.object_to_remove.save()
return self.object_to_remove
class ChangeUserRoleForm(BaseForm):
"""
Form for a user to change the current role
"""
role = forms.ChoiceField(
label=_("You are working as"),
label_suffix="",
choices=[],
widget=forms.Select(
attrs={
"onchange": "submit();",
}
)
)
def __init__(self, *args, **kwargs):
user = kwargs.pop("user", None)
super().__init__(*args, **kwargs)
self.action_url = reverse("home")
self.cancel_redirect = reverse("home")
role_groups = RoleGroup.get_users_role_groups(user)
choices = []
for group in role_groups:
choices.append(
(group.id, "{} ({})".format(ROLE_TYPE_STRINGS.get(group.role.type, None), group.organisation))
)
self.fields["role"].choices = choices
def save(self, request: HttpRequest) -> RoleGroup:
""" Custom save method for storing the newly selected role
Args:
request (HttpRequest):
Returns:
"""
role_group = RoleGroup.get_users_role_groups(request.user).get(id=self.cleaned_data.get("role", -1))
set_session_user_role(request, role_group)
return role_group

View File

@ -0,0 +1,153 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.12.20
"""
from getpass import getpass
from django.contrib.auth.models import User
from django.core.management import BaseCommand
from django.db import transaction
from konova.management.commands.setup_test_data import TEST_ORGANISATION_DATA, TEST_ROLE_GROUPS_DATA
from konova.models import RoleType, RoleGroup
from organisation.enums import RoleTypeEnum
from organisation.models import Organisation
CREATED_TEMPLATE = "{} created"
class Command(BaseCommand):
help = "Initializes database with basic data"
def handle(self, *args, **options):
try:
with transaction.atomic():
self.__init_superuser()
self.__init_test_organisation()
self.__init_role_types()
self.__init_role_groups()
except KeyboardInterrupt:
self.__break_line()
exit(-1)
def __init_superuser(self):
""" Create a superuser by user prompt input
Returns:
"""
self.stdout.write(
self.style.WARNING(
"--- Superuser ---",
)
)
username = input("Superuser name: ")
if User.objects.filter(username=username).exists():
self.stdout.write(
self.style.ERROR(
"Name already taken!"
)
)
exit(-1)
pw = getpass("Password: ")
pw_confirm = getpass("Confirm password : ")
if pw != pw_confirm:
self.stdout.write(
self.style.ERROR(
"Passwords did not match!"
)
)
exit(-1)
# Create superuser
superuser = User()
superuser.username = username
superuser.is_superuser = True
superuser.is_staff = True
superuser.set_password(pw)
superuser.save()
self.stdout.write(
self.style.SUCCESS(
"Superuser {} created".format(username)
)
)
self.__break_line()
def __init_role_types(self):
""" Initializes available role types according to RoleTypeEnum
Returns:
"""
self.stdout.write(
self.style.WARNING(
"--- Role types ---"
)
)
for role_type_enum in RoleTypeEnum:
role_type = RoleType.objects.get_or_create(
type=role_type_enum.value
)[0]
self.stdout.write(
self.style.SUCCESS(
CREATED_TEMPLATE.format(role_type.type)
)
)
self.__break_line()
def __init_test_organisation(self):
""" Creates test organisations from predefined data
Returns:
"""
self.stdout.write(
self.style.WARNING(
"--- Organisations ---"
)
)
for org in TEST_ORGANISATION_DATA:
db_org = Organisation.objects.get_or_create(
**org
)[0]
self.stdout.write(
self.style.SUCCESS(
CREATED_TEMPLATE.format(db_org.name)
)
)
self.__break_line()
def __init_role_groups(self):
""" Creates test role groups from predefined data
Returns:
"""
self.stdout.write(
self.style.WARNING(
"--- Role Groups ---"
)
)
for group_data in TEST_ROLE_GROUPS_DATA:
group_data["organisation"] = Organisation.objects.get(name=group_data["organisation"])
group_data["role"] = RoleType.objects.get(type=group_data["role"])
group = RoleGroup.objects.get_or_create(
**group_data
)[0]
self.stdout.write(
self.style.SUCCESS(
CREATED_TEMPLATE.format(group.name)
)
)
self.__break_line()
def __break_line(self):
""" Simply prints a line break
Returns:
"""
self.stdout.write("\n")

View File

@ -0,0 +1,58 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.12.20
"""
from organisation.enums import OrganisationTypeEnum, RoleTypeEnum
TEST_ORGANISATION_DATA = [
{
"name": "Test_Official_1",
"is_active": True,
"is_deleted": False,
"type": OrganisationTypeEnum.OFFICIAL.value,
},
{
"name": "Test_Official_2",
"is_active": True,
"is_deleted": False,
"type": OrganisationTypeEnum.OFFICIAL.value,
},
{
"name": "Test_NGO_1",
"is_active": True,
"is_deleted": False,
"type": OrganisationTypeEnum.NGO.value,
},
{
"name": "Test_Company_1",
"is_active": True,
"is_deleted": False,
"type": OrganisationTypeEnum.COMPANY.value,
},
]
TEST_ROLE_GROUPS_DATA = [
{
"name": "Registration office Test_Official_1",
"organisation": "Test_Official_1",
"role": RoleTypeEnum.REGISTRATIONOFFICE.value,
},
{
"name": "Licensing authority Test_Official_1",
"organisation": "Test_Official_1",
"role": RoleTypeEnum.LICENSINGAUTHORITY.value,
},
{
"name": "Dataprovider Test_Official_2",
"organisation": "Test_Official_2",
"role": RoleTypeEnum.LICENSINGAUTHORITY.value,
},
{
"name": "Dataprovider Test_Company_1",
"organisation": "Test_Company_1",
"role": RoleTypeEnum.DATAPROVIDER.value,
},
]

104
kspneo/models.py Normal file
View File

@ -0,0 +1,104 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 17.11.20
"""
import uuid
from django.contrib.auth.models import User, Group
from django.db import models
from django.db.models import QuerySet
from organisation.enums import RoleTypeEnum
class BaseResource(models.Model):
"""
A basic resource model, which defines attributes for every derived model
"""
id = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
)
is_active = models.BooleanField(default=True)
is_deleted = models.BooleanField(default=False)
created_on = models.DateTimeField(auto_now_add=True, null=True)
created_by = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
class Meta:
abstract = True
class BaseObject(BaseResource):
"""
A basic object model, which specifies BaseResource.
Mainly used for intervention, compensation, ecoaccount
"""
identifier = models.CharField(max_length=1000, null=True, blank=True)
title = models.CharField(max_length=1000, null=True, blank=True)
class Meta:
abstract = True
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)
date = models.DateField(null=True, blank=True)
def __str__(self):
return self.type
class Document(BaseResource):
"""
Documents can be attached to process, compensation or intervention for uploading legal documents or pictures.
"""
date_of_creation = models.DateField()
document = models.FileField()
comment = models.TextField()
class RoleType(BaseResource):
"""
Defines different role types
"""
type = models.CharField(max_length=255, choices=RoleTypeEnum.as_choices(drop_empty_choice=True), unique=True)
def __str__(self):
return self.type
class RoleGroup(Group):
"""
Role groups are specialized groups which hold information on which users are related to a certain organisation and
a role
"""
organisation = models.ForeignKey("organisation.Organisation", on_delete=models.CASCADE)
role = models.ForeignKey(RoleType, on_delete=models.CASCADE)
class Meta:
unique_together = [
["organisation", "role", ]
]
@staticmethod
def get_users_role_groups(user: User) -> QuerySet:
""" Get all role groups of a given user
Args:
user (User): The user
Returns:
qs (QuerySet)
"""
if user.is_anonymous:
return RoleGroup.objects.none()
return RoleGroup.objects.filter(
user=user
)

50
kspneo/settings.py Normal file
View File

@ -0,0 +1,50 @@
"""
Django settings for konova project.
Generated by 'django-admin startproject' using Django 3.1.2.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
# Load other settings
from konova.sub_settings.django_settings import *
# Num of days if user enables Remember-me on login
KEEP_LOGGED_DURATION = 30
# German DateTime string format
STRF_DATE_TIME = "%d.%m.%Y %H:%M:%S"
# Tables
RESULTS_PER_PAGE_PARAM = "rpp"
PAGE_PARAM = "page"
PAGE_SIZE_OPTIONS = [5, 10, 15, 20, 25, 30, 50, 75, 100]
PAGE_SIZE_OPTIONS_TUPLES = [
(5, 5),
(10, 10),
(15, 15),
(20, 20),
(25, 25),
(30, 30),
(50, 50),
(75, 75),
(100, 100),
]
PAGE_SIZE_DEFAULT = 5
PAGE_SIZE_MAX = 100
PAGE_DEFAULT = 1
# SSO settings
SSO_SERVER_BASE = "http://127.0.0.1:8000/"
SSO_SERVER = "{}sso/".format(SSO_SERVER_BASE)
SSO_PRIVATE_KEY = "CHANGE_ME"
SSO_PUBLIC_KEY = "CHANGE_ME"
# MAPS
DEFAULT_LAT = 50.00
DEFAULT_LON = 7.00
DEFAULT_ZOOM = 8.0

View File

@ -0,0 +1,57 @@
.body-content{
min-height: 75vh;
}
.note{
font-size: 0.75rem;
color: lightgray;
}
.label-required{
color: red;
}
table{
width: 100%;
}
.footer{
width: 100%;
position: absolute;
}
.error{
color: #D8000C !important;
background-color: #FFBABA !important;
}
i.true{
color: green;
}
i.false{
color: red;
}
.button{
margin: 0.5625rem;
}
.action-col{
max-width: 8rem;
}
.action-col .button{
margin: 0.1rem;
}
.user-role{
background: url('../images/menu-bg.png') repeat #871d33;
color: white;
padding: 0.5rem 0;
border-bottom: 4px solid #8e8e8e;
text-align: center;
}
.user-role > a {
color: white;
}

View File

@ -0,0 +1,43 @@
.info, .success, .warning, .error, .validation {
border: 1px solid;
margin: 10px 0px;
padding:15px 10px;
}
.info:hover,
.success:hover,
.warning:hover,
.error:hover,
.validation:hover {
cursor: default;
filter: brightness(1.2);
}
.info {
color: #00529B;
background-color: #BDE5F8;
/*
background-image: url('../images/knobs/info.png');
*/
}
.success {
color: #4F8A10;
background-color: #DFF2BF;
/*
background-image:url('../images/knobs/success.png');
*/
}
.warning {
color: #9F6000;
background-color: #FEEFB3;
/*
background-image: url('../images/knobs/warning.png');
*/
}
.error {
color: #D8000C;
background-color: #FFBABA;
/*
background-image: url('../images/knobs/error.png');
*/
}

9877
kspneo/static/css/mulewf.css Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

2
kspneo/static/js/jquery-3.5.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

13
kspneo/static/js/jquery-ui.min.js vendored Normal file

File diff suppressed because one or more lines are too long

6445
kspneo/static/js/mulewf.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,12 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.20
"""
BASE_TITLE = "konova"
BASE_FRONTEND_TITLE = "Kompensationsverzeichnis Service Portal"
WIKI_URL = "https://dienste.naturschutz.rlp.de/doku/doku.php?id=ksp:start"
RENDER_HEADER = False

View File

@ -0,0 +1,230 @@
"""
Django settings for konova project.
Generated by 'django-admin startproject' using Django 3.1.3.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = os.path.dirname(
os.path.dirname(
os.path.dirname(
os.path.abspath(
__file__
)
)
)
)
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '5=9-)2)h$u9=!zrhia9=lj-2#cpcb8=#$7y+)l$5tto$3q(n_+'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Authentication settings
LOGIN_URL = "/login/"
# Session settings
SESSION_COOKIE_AGE = 30 * 60 # 30 minutes
SESSION_SAVE_EVERY_REQUEST = True
# Application definition
INSTALLED_APPS = [
'dal',
'dal_select2',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.gis',
'simple_sso.sso_server',
'django_tables2',
'fontawesome_5',
'konova',
'compensation',
'intervention',
'process',
'organisation',
]
if DEBUG:
INSTALLED_APPS += [
'debug_toolbar',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
"django.middleware.locale.LocaleMiddleware",
]
if DEBUG:
MIDDLEWARE += [
"debug_toolbar.middleware.DebugToolbarMiddleware",
]
ROOT_URLCONF = 'konova.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, "templates"),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'konova.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': 'konova',
'USER': 'postgres',
'HOST': '127.0.0.1',
'PORT': '5432',
}
}
# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
DEFAULT_DATE_TIME_FORMAT = 'YYYY-MM-DD hh:mm:ss'
TIME_ZONE = 'Europe/Berlin'
USE_I18N = True
USE_L10N = True
USE_TZ = True
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale'),
)
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, "static")
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'konova/static'),
]
# DJANGO DEBUG TOOLBAR
INTERNAL_IPS = [
"127.0.0.1"
]
DEBUG_TOOLBAR_CONFIG = {
"DISABLE_PANELS": {
'debug_toolbar.panels.versions.VersionsPanel',
'debug_toolbar.panels.timer.TimerPanel',
'debug_toolbar.panels.settings.SettingsPanel',
'debug_toolbar.panels.headers.HeadersPanel',
'debug_toolbar.panels.request.RequestPanel',
'debug_toolbar.panels.sql.SQLPanel',
'debug_toolbar.panels.staticfiles.StaticFilesPanel',
'debug_toolbar.panels.templates.TemplatesPanel',
'debug_toolbar.panels.cache.CachePanel',
'debug_toolbar.panels.signals.SignalsPanel',
'debug_toolbar.panels.logging.LoggingPanel',
'debug_toolbar.panels.redirects.RedirectsPanel',
'debug_toolbar.panels.profiling.ProfilingPanel',
}
}
# EMAIL (see https://docs.djangoproject.com/en/dev/topics/email/)
DEFAULT_FROM_EMAIL = "bot@arneo.de" # The default email address for the 'from' element
EMAIL_HOST = "localhost"
EMAIL_PORT = "1025"
#EMAIL_HOST_USER = ""
#EMAIL_HOST_PASSWORD = ""
EMAIL_USE_TLS = False
EMAIL_USE_SSL = False
# LOGGING
BASIC_LOGGER = "logger"
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module}: {message}',
'style': '{',
},
'simple': {
'format': '{levelname} {message}',
'style': '{',
},
},
'handlers': {
'log_to_file': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'filename': '{}/logs/error.log'.format(BASE_DIR),
'maxBytes': 1024*1024*5, # 5 MB
'backupCount': 5,
'formatter': 'verbose',
},
},
'loggers': {
BASIC_LOGGER: {
'handlers': ['log_to_file'],
'level': 'INFO',
'propagate': True,
},
},
}

View File

@ -0,0 +1,5 @@
<form action="{{ form.action_url }}" method="post">
{% csrf_token %}
{{ form.as_p }}
</form>

View File

@ -0,0 +1,8 @@
{% extends 'base.html' %}
{% load i18n %}
{% block body %}
<div class="column">
{% include 'generic_table_form.html' %}
</div>
{% endblock %}

View File

@ -0,0 +1,44 @@
{% extends 'base.html' %}
{% load i18n %}
{% block body_middle %}
<h1>Kompensationsverzeichnis</h1>
<h2>Service Portal</h2>
<hr>
{% if user.is_anonymous %}
<a href="{% url 'simple-sso-login' %}">
<button class="button middle">
{% trans 'Proceed with login' %}
</button>
</a>
{% else %}
<article>
{% trans 'Logged in as' %} <strong>{{ user.username }}</strong>
<br>
{% trans 'Last login on' %} {{ user.last_login }}
</article>
<form action="{{form.action_url}}" method="post">
{% csrf_token %}
<table>
{% comment %}
This is an alternative to using the <article></article>
<tr>
<td>{% trans 'Logged in as' %}</td>
<td><strong>{{ user.username }}</strong></td>
</tr>
<tr>
<td>{% trans 'Last login on' %}</td>
<td><strong>{{ user.last_login }}</strong></td>
</tr>
{% endcomment %}
{% for field in form %}
<tr>
<td>{{ field.label }}</td>
<td>{{ field }}</td>
</tr>
{% endfor %}
</table>
</form>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.12.20
"""

View File

@ -0,0 +1,17 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.12.20
"""
from django import template
from process.settings import PROCESS_STATE_STRINGS
register = template.Library()
@register.filter
def resolve_process_state(value):
return PROCESS_STATE_STRINGS.get(value, None)

47
kspneo/urls.py Normal file
View File

@ -0,0 +1,47 @@
"""konova URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
import debug_toolbar
from django.contrib import admin
from django.urls import path, include
from simple_sso.sso_client.client import Client
from konova.autocompletes import OrganisationAutocomplete, NonOfficialOrganisationAutocomplete
from konova.settings import SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY, DEBUG
from konova.views import logout_view, home_view
sso_client = Client(SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY)
urlpatterns = [
path('admin/', admin.site.urls),
path('login/', include(sso_client.get_urls())),
path('logout/', logout_view, name="logout"),
path('', home_view, name="home"),
path('process/', include("process.urls")),
path('intervention/', include("intervention.urls")),
path('compensation/', include("compensation.urls")),
path('eco-account/', include("process.urls")),
path('ema/', include("process.urls")),
path('organisation/', include("organisation.urls")),
path('user/', include("process.urls")),
# Autocomplete paths
path("atcmplt/orgs", OrganisationAutocomplete.as_view(), name="orgs-autocomplete"),
path("atcmplt/orgs/other", NonOfficialOrganisationAutocomplete.as_view(), name="other-orgs-autocomplete"),
]
if DEBUG:
urlpatterns += [
path('__debug__/', include(debug_toolbar.urls)),
]

View File

@ -0,0 +1,22 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 09.11.20
"""
import random
import string
def generate_random_string(length: int, only_numbers: bool = False) -> str:
"""
Generates a random string of variable length
"""
if only_numbers:
elements = string.digits
else:
elements = string.ascii_letters
ret_val = "".join(random.choice(elements) for i in range(length))
return ret_val

52
kspneo/utils/mailer.py Normal file
View File

@ -0,0 +1,52 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 09.11.20
"""
import logging
from django.core.mail import send_mail
from konova.sub_settings.django_settings import DEFAULT_FROM_EMAIL
logger = logging.getLogger(__name__)
class Mailer:
"""
A wrapper for the django internal mailing functionality
"""
from_mail = None
to_mail = []
fail_silently = False
# Optional. Can be changed using the constructor to authenticate on the smtp server using other credentials
auth_user = None
auth_password = None
def __init__(self, to_mail: list, from_mail: str = DEFAULT_FROM_EMAIL, auth_user: str = None, auth_password: str = None, fail_silently: bool = False):
# Make sure given to_mail parameter is a list
if isinstance(to_mail, str):
to_mail = [to_mail]
self.from_mail = from_mail
self.to_mail = to_mail
self.fail_silently = fail_silently
self.auth_user = auth_user
self.auth_password = auth_password
def send(self, subject: str, msg: str):
"""
Sends a mail with subject and message
"""
return send_mail(
subject=subject,
message=msg,
from_email=self.from_mail,
recipient_list=self.to_mail,
fail_silently=self.fail_silently,
auth_user=self.auth_user,
auth_password=self.auth_password
)

45
kspneo/utils/session.py Normal file
View File

@ -0,0 +1,45 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 09.12.20
"""
from django.http import HttpRequest
from idna import unicode
from konova.models import RoleGroup
from organisation.settings import ROLE_TYPE_STRINGS
CURRENT_ROLE_ID = "current_role"
def set_session_user_role(request: HttpRequest, role_group: RoleGroup) -> dict:
""" Set the user session to an active role
Args:
request (HttpRequest): The user request
role_group (RoleGroup): The selected role group
Returns:
"""
current_role = {}
if role_group is not None:
current_role["type"] = unicode(ROLE_TYPE_STRINGS.get(role_group.role.type))
current_role["org"] = role_group.organisation.__str__()
current_role["id"] = role_group.id
request.session[CURRENT_ROLE_ID] = current_role
return current_role
def get_session_user_role(request: HttpRequest) -> dict:
""" Returns the current role chosen by a user for this session
Args:
request (HttpRequest): The used request
Returns:
"""
return request.session.get(CURRENT_ROLE_ID, {})

121
kspneo/utils/tables.py Normal file
View File

@ -0,0 +1,121 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 25.11.20
"""
import uuid
from django import forms
from django.core.paginator import PageNotAnInteger, EmptyPage
from django.http import HttpRequest
from django.utils.html import format_html
import django_tables2 as tables
from konova.forms import BaseForm
from konova.settings import PAGE_SIZE_DEFAULT, PAGE_PARAM, RESULTS_PER_PAGE_PARAM, PAGE_SIZE_OPTIONS
class BaseTable(tables.tables.Table):
results_per_page_choices = PAGE_SIZE_OPTIONS
results_per_page_chosen = None
results_per_page_parameter = RESULTS_PER_PAGE_PARAM
add_new_entries = True
add_new_url = None
title = None
def __init__(self, request: HttpRequest = None, filter_set=None, queryset=None, *args, **kwargs):
self.user = request.user or None
if filter_set is not None:
queryset = filter_set.qs
kwargs["data"] = queryset
kwargs["request"] = request
super().__init__(*args, **kwargs)
self.results_per_page_chosen = int(request.GET.get(RESULTS_PER_PAGE_PARAM, PAGE_SIZE_DEFAULT))
try:
self.paginate(
page=request.GET.get(PAGE_PARAM, 1),
per_page=self.results_per_page_chosen,
)
except (PageNotAnInteger, EmptyPage) as e:
self.paginate(
page=1,
per_page=self.results_per_page_chosen,
)
def render_link(self, tooltip: str, href: str, txt: str, new_tab: bool = False):
"""
Returns an <a> html element using given parameters
"""
new_tab = "_blank" if new_tab else "_self"
return format_html(
"<a href={} target='{}' title='{}'>{}</a>",
href,
new_tab,
tooltip,
txt,
)
def render_delete_btn(self, tooltip: str = None, href: str = None):
"""
Returns a remover icon with <a> support as html element using given parameters
"""
return format_html(
"<a href={} title='{}'><button class='button small'><em class='fas fa-trash-alt'></em></button></a>",
href,
tooltip,
)
def render_edit_btn(self, tooltip: str = None, href: str = None):
"""
Returns a remover icon with <a> support as html element using given parameters
"""
return format_html(
"<a href={} title='{}'><button class='button small'><em class='fas fa-edit'></em></button></a>",
href,
tooltip,
)
def render_open_btn(self, tooltip: str = None, href: str = None, new_tab: bool = False):
"""
Returns a remover icon with <a> support as html element using given parameters
"""
return format_html(
"<a href={} title='{}' target='{}'><button class='button small'><em class='fas fa-sign-in-alt'></em></button></a>",
href,
tooltip,
"_blank" if new_tab else ""
)
def render_boolean(self, tooltip: str = None, val: bool = False):
"""
Returns a remover icon with <a> support as html element using given parameters
"""
icon = "fas fa-check-circle true" if val else "fas fa-times-circle false"
return format_html(
"<em title='{}' class='{}'></em>",
tooltip,
icon
)
class ChoicesColumnForm(BaseForm):
select = forms.ChoiceField(
choices=[],
label="",
label_suffix="",
widget=forms.Select(
attrs={
"onchange": "submit();",
}
)
)
def __init__(self, *args, **kwargs):
self.action_url = kwargs.pop("action_url", None)
self.choices = kwargs.pop("choices", [])
super().__init__(*args, **kwargs)
self.auto_id += "_" + str(uuid.uuid4())
if len(self.choices) > 0:
self.fields["select"].choices = self.choices

67
kspneo/views.py Normal file
View File

@ -0,0 +1,67 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.20
"""
from django.utils.translation import gettext_lazy as _
from django.contrib import messages
from django.contrib.auth import logout
from django.http import HttpRequest
from django.shortcuts import redirect, render
from konova.contexts import BaseContext
from konova.forms import ChangeUserRoleForm
from konova.settings import SSO_SERVER_BASE
from konova.utils.session import get_session_user_role
def logout_view(request: HttpRequest):
"""
Logout route for ending the session manually.
Args:
request (HttpRequest): The used request object
Returns:
A redirect
"""
logout(request)
return redirect(SSO_SERVER_BASE)
def home_view(request: HttpRequest):
"""
Renders the landing page
Args:
request (HttpRequest): The used request object
Returns:
A redirect
"""
template = "konova/home.html"
if request.method == "POST":
form = ChangeUserRoleForm(
request.POST or None,
user=request.user,
)
if form.is_valid():
role = form.save(request)
messages.success(request, _("Role changed"))
else:
messages.error(request, _("Invalid role"))
return redirect("home")
else:
# GET
form = ChangeUserRoleForm(
user=request.user,
initial={"role": int(get_session_user_role(request).get("id", -1))},
)
additional_context = {
"form": form,
}
context = BaseContext(request, additional_context).context
return render(request, template, context)

16
kspneo/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for konova project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'konova.settings')
application = get_wsgi_application()

Binary file not shown.

File diff suppressed because it is too large Load Diff

0
logs/error.log Normal file
View File

22
manage.py Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'konova.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

0
organisation/__init__.py Normal file
View File

15
organisation/admin.py Normal file
View File

@ -0,0 +1,15 @@
from django.contrib import admin
from organisation.models import Organisation
class OrganisationAdmin(admin.ModelAdmin):
list_display = [
"name",
"type",
"created_on",
"created_by",
]
admin.site.register(Organisation, OrganisationAdmin)

5
organisation/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class OrganisationConfig(AppConfig):
name = 'organisation'

26
organisation/enums.py Normal file
View File

@ -0,0 +1,26 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 07.12.20
"""
from konova.enums import BaseEnum
class RoleTypeEnum(BaseEnum):
"""
Defines the possible role types for organisation and users
"""
DATAPROVIDER = "DATAPROVIDER"
LICENSINGAUTHORITY = "LICENSINGAUTHORITY"
REGISTRATIONOFFICE = "REGISTRATIONOFFICE"
class OrganisationTypeEnum(BaseEnum):
"""
Defines the possible role types for organisation and users
"""
OFFICIAL = "OFFICIAL"
COMPANY = "COMPANY"
NGO = "NGO"

18
organisation/models.py Normal file
View File

@ -0,0 +1,18 @@
from django.db import models
from konova.models import BaseResource
from organisation.enums import OrganisationTypeEnum
class Organisation(BaseResource):
name = models.CharField(max_length=500, unique=True)
address = models.CharField(max_length=500, null=True, blank=True)
city = models.CharField(max_length=500, null=True, blank=True)
postal_code = models.CharField(max_length=100, null=True, blank=True)
phone = models.CharField(max_length=500, null=True, blank=True)
email = models.EmailField(max_length=500, null=True, blank=True)
facsimile = models.CharField(max_length=500, null=True, blank=True)
type = models.CharField(max_length=255, choices=OrganisationTypeEnum.as_choices(drop_empty_choice=True), null=True, blank=True)
def __str__(self):
return self.name

22
organisation/settings.py Normal file
View File

@ -0,0 +1,22 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 07.12.20
"""
from django.utils.translation import gettext_lazy as _
from organisation.enums import OrganisationTypeEnum as ote
from organisation.enums import RoleTypeEnum as rte
ORGANISATION_ROLE_STRINGS = {
ote.OFFICIAL.value: _("Official"),
ote.COMPANY.value: _("Company"),
ote.NGO.value: _("NGO"),
}
ROLE_TYPE_STRINGS = {
rte.DATAPROVIDER.value: _("Data provider"),
rte.LICENSINGAUTHORITY.value: _("Licencing Authority"),
rte.REGISTRATIONOFFICE.value: _("Registration office"),
}

3
organisation/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

12
organisation/urls.py Normal file
View File

@ -0,0 +1,12 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 07.12.20
"""
from django.urls import path
app_name = "organisation"
urlpatterns = [
]

3
organisation/views.py Normal file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

0
process/__init__.py Normal file
View File

38
process/admin.py Normal file
View File

@ -0,0 +1,38 @@
from django.contrib import admin
from process.models import Process
def activate_process(modeladmin, request, queryset):
for process in queryset:
process.activate()
def deactivate_process(modeladmin, request, queryset):
for process in queryset:
process.deactivate()
activate_process.short_description = "Activate selected process"
deactivate_process.short_description = "Deactivate selected process"
class ProcessAdmin(admin.ModelAdmin):
list_display = [
"id",
"licensing_authority",
"licensing_authority_document_identifier",
"registration_office",
"registration_office_document_identifier",
"is_active",
"is_deleted",
]
actions = [
activate_process,
deactivate_process,
]
admin.site.register(Process, ProcessAdmin)

5
process/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class ProcessConfig(AppConfig):
name = 'process'

202
process/enums.py Normal file
View File

@ -0,0 +1,202 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 25.11.20
"""
from django.contrib.auth.models import User
from konova.enums import BaseEnum
from organisation.enums import RoleTypeEnum
class ProcessStateEnum(BaseEnum):
"""
ProcessStates define in which state a process can be:
private
Only the user can see and edit this process. Not viewable by any others
accessible
The user and all participated users can see this process. Only the responsible next user can edit
the process
licensed
The user and all participated users can see this process. Only the responsible next user can edit
the process
official
The user and all participated users can see this process. Only the registration office user can now
change details. If a process is changed in this state, the state will be set back to accessible, so all
participated users need to take a look on the changed data again.
recorded [Will be set automatically after certain time]
The user and all participated users can see this process. No one can edit data. To change any details,
the process has to be set to accessible manually again.
"""
PRIVATE = 0
ACCESSIBLE = 1
LICENSED = 2
OFFICIAL = 3
RECORDED = 4
@classmethod
def as_choices(cls, drop_empty_choice: bool = False) -> list:
""" Extends as_choices, so choices will be translated
Args:
drop_empty_choice (bool): Whether the empty choice shall be dropped or not
Returns:
trans_choices (list): Translated choices
"""
choices = super().as_choices(drop_empty_choice)
return ProcessStateEnum.__translate_choices(choices)
@staticmethod
def __translate_choices(choices: list) -> list:
""" Translates a list of prepared but untranslated choices
Args:
choices (list): A list of tuple chocies
Returns:
choices (list): The same list but translated
"""
from process.settings import PROCESS_STATE_STRINGS
trans_choices = []
# Translate
for choice in choices:
if choice[0] is not None:
choice = list(choice)
trans = PROCESS_STATE_STRINGS.get(choice[0])
choice[1] = trans
choice = tuple(choice)
trans_choices.append(choice)
return trans_choices
@classmethod
def is_state(cls, state: int) -> bool:
""" Checks whether the given state is a valid Enum
Args:
state (int): The state to be checked
Returns:
is_valid (bool)
"""
valid_vals = {enum.value for enum in cls}
return state in valid_vals
@classmethod
def as_role_choices(cls, user: User) -> list:
""" Checks whether the given state is a valid Enum
Args:
user (User): The performing user
Returns:
is_valid (bool)
"""
role = user.current_role
if role is None:
return []
role_type = role.role.type
role_type_enum = RoleTypeEnum[role_type]
choices = PROCESS_ROLE_STATES.get(role_type_enum)
choices = [(enum.value, enum.name) for enum in choices]
choices = ProcessStateEnum.__translate_choices(choices)
return choices
@classmethod
def as_next_role_choices(cls, user: User, current_state: int, is_owner: bool = False) -> list:
""" Returns a list of valid choices depending on the current role of the user and the current state
Args:
user (User): The performing user
current_state (int): The current state of the process
Returns:
choices (list): A list of valid choices
"""
role = user.current_role
if role is None:
return []
role_type = role.role.type
role_type_enum = RoleTypeEnum[role_type]
# Merge the possible choices depending on the current user role
# with the possible choices depending on the process state
role_choices = PROCESS_ROLE_STATES.get(role_type_enum)
status_choices = PROCESS_STATE_NEXT_CHOICES.get(current_state)
current_choice = {ProcessStateEnum(current_state)}
choices = (status_choices & role_choices) | current_choice
# If user is owner of this process, we shall add the private choice if not existing, yet
if is_owner:
choices = {ProcessStateEnum.PRIVATE} | choices
# Make sure enums are ordered by numerical value
choices = sorted(choices, key=lambda _enum: _enum.value)
# Create selectable and translated choices from enum list
choices = [(enum.value, enum.name) for enum in choices]
choices = ProcessStateEnum.__translate_choices(choices)
return choices
# DEFINES THE AVAILABLE STATES FOR EACH ROLE
PROCESS_ROLE_STATES = {
RoleTypeEnum.DATAPROVIDER: {
ProcessStateEnum.PRIVATE,
ProcessStateEnum.ACCESSIBLE,
},
RoleTypeEnum.LICENSINGAUTHORITY: {
#ProcessStateEnum.PRIVATE,
ProcessStateEnum.ACCESSIBLE,
ProcessStateEnum.LICENSED,
},
RoleTypeEnum.REGISTRATIONOFFICE: {
#ProcessStateEnum.PRIVATE,
ProcessStateEnum.ACCESSIBLE,
ProcessStateEnum.OFFICIAL,
},
}
# DEFINES POSSIBLE NEXT STATES FOR EACH PROCESS STATE
PROCESS_STATE_NEXT_CHOICES = {
ProcessStateEnum.PRIVATE.value: {
ProcessStateEnum.PRIVATE,
ProcessStateEnum.ACCESSIBLE,
},
ProcessStateEnum.ACCESSIBLE.value: {
ProcessStateEnum.PRIVATE,
ProcessStateEnum.ACCESSIBLE,
ProcessStateEnum.LICENSED,
},
ProcessStateEnum.LICENSED.value: {
ProcessStateEnum.PRIVATE,
ProcessStateEnum.ACCESSIBLE,
ProcessStateEnum.LICENSED,
ProcessStateEnum.OFFICIAL,
},
ProcessStateEnum.OFFICIAL.value: {
ProcessStateEnum.ACCESSIBLE,
ProcessStateEnum.OFFICIAL,
ProcessStateEnum.RECORDED,
},
}
# DEFINES FOR EACH STATE WHICH ROLE CAN EDIT THE PROCESS
PROCESS_EDITABLE_STATE = {
ProcessStateEnum.PRIVATE.value: {
RoleTypeEnum.DATAPROVIDER,
RoleTypeEnum.LICENSINGAUTHORITY,
},
ProcessStateEnum.ACCESSIBLE.value: {
RoleTypeEnum.LICENSINGAUTHORITY,
},
ProcessStateEnum.LICENSED.value: {
RoleTypeEnum.REGISTRATIONOFFICE,
},
ProcessStateEnum.OFFICIAL.value: {
RoleTypeEnum.REGISTRATIONOFFICE,
}
}

218
process/forms.py Normal file
View File

@ -0,0 +1,218 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 30.11.20
"""
from dal import autocomplete
from django import forms
from django.contrib.auth.models import User
from django.db import transaction
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from intervention.models import Intervention
from konova.forms import BaseForm
from organisation.models import Organisation
from process.enums import ProcessStateEnum
from process.models import Process
class NewProcessForm(BaseForm):
"""
A form for new process
"""
identifier = forms.CharField(
max_length=255,
label=_("Identifier"),
label_suffix=_(""),
help_text=_("Generated automatically if none was given"),
required=False,
)
title = forms.CharField(
max_length=255,
label=_("Title"),
label_suffix=_(""),
help_text=_("Proper title of the process"),
required=True,
)
type = forms.CharField(
max_length=255,
label=_("Type"),
label_suffix=_(""),
help_text=_("Which process type is this"),
required=True,
)
licensing_authority = forms.ModelChoiceField(
label=_("Licencing Authority"),
label_suffix=_(""),
required=True,
queryset=Organisation.objects.all(),
widget=autocomplete.ModelSelect2(
url="orgs-autocomplete",
attrs={
"data-placeholder": _("Organization"),
"data-minimum-input-length": 3,
}
),
)
licensing_authority_document_identifier = forms.CharField(
max_length=255,
label=_("Licencing document identifier"),
label_suffix=_(""),
required=True,
)
comment_licensing_authority = forms.CharField(
widget=forms.Textarea,
label=_("Comment licensing authority"),
label_suffix=_(""),
required=False,
)
registration_office = forms.ModelChoiceField(
label=_("Registration office"),
label_suffix=_(""),
required=True,
queryset=Organisation.objects.all(),
widget=autocomplete.ModelSelect2(
url="orgs-autocomplete",
attrs={
"data-placeholder": _("Organization"),
"data-minimum-input-length": 3,
}
),
)
registration_office_document_identifier = forms.CharField(
max_length=255,
label=_("Registration document identifier"),
label_suffix=_(""),
required=True,
)
comment_registration_office = forms.CharField(
widget=forms.Textarea,
label=_("Comment registration office"),
label_suffix=_(""),
required=False,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.action_url = reverse("process:new")
self.cancel_redirect = reverse("process:index")
self.form_title = _("Add new process")
self.form_caption = _("Enter these basic information for the new process.")
def save(self, user: User):
"""
Persists process objects into database
"""
with transaction.atomic():
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
_type = self.cleaned_data.get("type", None)
licencing_auth = self.cleaned_data.get("licensing_authority", None)
licencing_auth_doc_id = self.cleaned_data.get("licensing_authority_document_identifier", None)
reg_off = self.cleaned_data.get("registration_office", None)
reg_off_doc_id = self.cleaned_data.get("registration_office_document_identifier", None)
comment_license = self.cleaned_data.get("comment_licensing_authority", None)
comment_registration = self.cleaned_data.get("comment_registration_office", None)
process = Process()
process.licensing_authority = licencing_auth
process.licensing_authority_document_identifier = licencing_auth_doc_id
process.registration_office = reg_off
process.registration_office_document_identifier = reg_off_doc_id
process.state = ProcessStateEnum.PRIVATE.value
process.created_by = user
process.licensing_authority_comment = comment_license
process.registration_office_comment = comment_registration
process.save()
intervention = Intervention()
intervention.title = title
intervention.type = _type
intervention.created_by = user
intervention.process = process
intervention.identifier = identifier
intervention.save()
return process
class EditProcessForm(NewProcessForm):
status = forms.ChoiceField(
label=_("Status"),
label_suffix="",
choices=[],
)
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user", None)
super().__init__(*args, **kwargs)
if self.instance is not None:
self.action_url = reverse("process:edit", args=(self.instance.id,))
self.cancel_redirect = reverse("process:index")
self.form_title = _("Edit process")
self.form_caption = ""
self.fields["status"].choices = ProcessStateEnum.as_next_role_choices(self.user, self.instance.state)
# Initialize form data
form_data = {
"identifier": self.instance.intervention.identifier,
"title": self.instance.intervention.title,
"type": self.instance.intervention.type,
"licensing_authority": self.instance.licensing_authority,
"licensing_authority_document_identifier": self.instance.licensing_authority_document_identifier,
"registration_office": self.instance.registration_office,
"registration_office_document_identifier": self.instance.registration_office_document_identifier,
"comment_licensing_authority": self.instance.licensing_authority_comment,
"comment_registration_office": self.instance.registration_office_comment,
"status": self.instance.state,
}
disabled_fields = [
"identifier",
]
self.load_initial_data(
form_data,
disabled_fields,
)
def save(self, user: User):
""" Persists changes from form to instance
Args:
user (User): The performing user
Returns:
process (Process): The edited process instance
"""
with transaction.atomic():
title = self.cleaned_data.get("title", None)
_type = self.cleaned_data.get("type", None)
licencing_auth = self.cleaned_data.get("licensing_authority", None)
licencing_auth_doc_id = self.cleaned_data.get("licensing_authority_document_identifier", None)
reg_off = self.cleaned_data.get("registration_office", None)
reg_off_doc_id = self.cleaned_data.get("registration_office_document_identifier", None)
comment_license = self.cleaned_data.get("comment_licensing_authority", None)
comment_registration = self.cleaned_data.get("comment_registration_office", None)
new_state = self.cleaned_data.get("status", None)
self.instance.licensing_authority = licencing_auth
self.instance.licensing_authority_document_identifier = licencing_auth_doc_id
self.instance.registration_office = reg_off
self.instance.registration_office_document_identifier = reg_off_doc_id
self.instance.state = ProcessStateEnum.PRIVATE.value
self.instance.created_by = user
self.instance.licensing_authority_comment = comment_license
self.instance.registration_office_comment = comment_registration
self.instance.state = new_state
self.instance.save()
self.instance.intervention.title = title
self.instance.intervention.type = _type
self.instance.intervention.created_by = user
self.instance.intervention.save()
return self.instance

158
process/models.py Normal file
View File

@ -0,0 +1,158 @@
"""
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.db import models, transaction
from konova.models import BaseResource
from organisation.enums import RoleTypeEnum
from organisation.models import Organisation
from process.enums import ProcessStateEnum
from process.settings import PROCESS_STATE_STRINGS
class Process(BaseResource):
"""
Process links compensation, intervention and eco accounts. These links are realized as ForeignKeys in their models.
This process model therefore just holds further information on participated legal organizations.
Attribute 'state' holds information on the current state of the process, which are defined in ProcessStateEnum
"""
licensing_authority = models.ForeignKey(Organisation, on_delete=models.SET_NULL, null=True, blank=True, related_name="+")
licensing_authority_document_identifier = models.CharField(max_length=500, null=True, blank=True)
licensing_authority_comment = models.CharField(max_length=500, null=True, blank=True)
registration_office_document_identifier = models.CharField(max_length=500, null=True, blank=True)
registration_office = models.ForeignKey(Organisation, on_delete=models.SET_NULL, null=True, blank=True, related_name="+")
registration_office_comment = models.CharField(max_length=500, null=True, blank=True)
state = models.PositiveIntegerField(choices=ProcessStateEnum.as_choices(drop_empty_choice=True), default=0)
def __str__(self) -> str:
try:
intervention = self.intervention
title = intervention.title
except AttributeError:
title = "NO TITLE"
return title
def get_state_str(self):
"""
Translates the numeric state into a string
Returns:
"""
return PROCESS_STATE_STRINGS.get(self.state, None)
def deactivate(self):
""" Deactivates a process and it's related elements
Returns:
"""
if self.is_active and not self.is_deleted:
self.toggle_deletion()
def activate(self):
""" Activates a process and it's related elements
Returns:
"""
if not self.is_active and self.is_deleted:
self.toggle_deletion()
def toggle_deletion(self):
""" Enables or disables a process
Processes are not truly removed from the database, just toggled in their flags 'is_active' and 'is_deleted'
Returns:
"""
with transaction.atomic():
self.is_active = not self.is_active
self.is_deleted = not self.is_deleted
self.save()
# toggle related elements
comps = self.compensation.all()
elements = list(comps)
if self.intervention is not None:
elements.append(self.intervention)
for elem in elements:
elem.is_active = self.is_active
elem.is_deleted = self.is_deleted
elem.save()
@staticmethod
def get_role_objects(user: User, order_by: str = "-created_on"):
""" Returns processes depending on the currently selected role of the user
* REGISTRATIONOFFICE
* User can see the processes where registration_office is set to the organisation of the currently selected role
* User can see self-created processes
* LICENSINGOFFICE
* same
* DATAPROVIDER
* User can see only self-created processes
Args:
user (User): The performing user
order_by (str): Order by which Process attribute
Returns:
"""
role = user.current_role
if role is None:
return Process.objects.none()
_filter = {
"is_deleted": False,
}
if role.role.type == RoleTypeEnum.REGISTRATIONOFFICE.value:
_filter["registration_office"] = role.organisation
elif role.role.type == RoleTypeEnum.LICENSINGAUTHORITY.value:
_filter["licensing_authority"] = role.organisation
elif role.role.type == RoleTypeEnum.DATAPROVIDER.value:
# Nothing special
_filter["created_by"] = user
else:
# None of the above
pass
other_processes = Process.objects.filter(
**_filter
)
user_created_processes = Process.objects.filter(
created_by=user,
is_deleted=False,
)
qs = (other_processes | user_created_processes).distinct()
qs = qs.order_by(order_by)
return qs
@staticmethod
def create_from_intervention(intervention):
""" Creates a process for an intervention, in case an intervention has been created without a process
Args:
intervention (Intervention): The intervention
Returns:
process (Process)
"""
process = Process()
process.identifier = intervention.identifier
process.title = intervention.title
process.created_by = intervention.created_by
process.save()
intervention.process = process
intervention.save()
return process

19
process/settings.py Normal file
View File

@ -0,0 +1,19 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 25.11.20
"""
from django.utils.translation import gettext_lazy as _
from process.enums import ProcessStateEnum
# DEFINES TRANSLATIONS FOR EACH PROCESSSTATEENUM
PROCESS_STATE_STRINGS = {
ProcessStateEnum.PRIVATE.value: _("private"),
ProcessStateEnum.ACCESSIBLE.value: _("accessible"),
ProcessStateEnum.LICENSED.value: _("licensed"),
ProcessStateEnum.OFFICIAL.value: _("official"),
ProcessStateEnum.RECORDED.value: _("recorded"),
}

130
process/tables.py Normal file
View File

@ -0,0 +1,130 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 25.11.20
"""
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.html import format_html
from konova.utils.tables import BaseTable, ChoicesColumnForm
import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from process.enums import ProcessStateEnum
from process.models import Process
from process.settings import PROCESS_STATE_STRINGS
class ProcessTable(BaseTable):
id = tables.Column(
verbose_name=_("Intervention identifier"),
orderable=True,
accessor="intervention.identifier",
)
t = tables.Column(
verbose_name=_("Title"),
orderable=True,
accessor="intervention.title",
)
"""
THESE COLUMNS MIGHT NOT BE OF INTEREST. TO REDUCE TABLE WIDTH THEY CAN BE REMOVED
dila = tables.Column(
verbose_name=_("Licensing authority document identifier"),
orderable=True,
accessor="licensing_authority_document_identifier",
)
diro = tables.Column(
verbose_name=_("Registration office document identifier"),
orderable=True,
accessor="registration_office_document_identifier",
)
"""
s = tables.Column(
verbose_name=_("Status"),
orderable=True,
accessor="state",
)
d = tables.Column(
verbose_name=_("Created on"),
orderable=True,
accessor="created_on",
)
ac = tables.Column(
verbose_name=_("Actions"),
orderable=False,
empty_values=[],
attrs={"td": {"class": "action-col"}}
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title = _("Processes")
self.add_new_url = reverse("process:new")
def render_id(self, value, record: Process):
""" Renders the id column for an intervention
Args:
value (str): The identifier value
record (Process): The process record
Returns:
"""
html = ""
html += self.render_link(
tooltip=_("Open {}").format(_("Process")),
href=reverse("process:open", args=(record.id,)),
txt=value,
new_tab=False,
)
return format_html(html)
def render_s(self, value, record, column) -> str:
""" Translates the record state to a desired language
Args:
value (str): The value of state inside record
record (Process): The whole record itself
Returns:
str
"""
state = record.state
is_owner = record.created_by == self.request.user
choices = ProcessStateEnum.as_next_role_choices(self.request.user, state, is_owner)
valid_choices = [choice[0] for choice in choices]
if state in valid_choices:
form = ChoicesColumnForm(
action_url=reverse("process:edit-status", args=(record.id,)),
choices=choices,
initial={"select": state},
)
rendered = render_to_string("konova/choiceColumnForm.html", context={"form": form}, request=self.request)
else:
rendered = PROCESS_STATE_STRINGS.get(state)
return rendered
def render_ac(self, value, record):
"""
Renders possible actions for this record, such as delete.
"""
process = _("Process")
html = ""
html += self.render_open_btn(
_("Open {}").format(process),
reverse("process:open", args=(record.id,)),
)
html += self.render_edit_btn(
_("Edit {}").format(process),
reverse("process:edit", args=(record.id,)),
)
html += self.render_delete_btn(
_("Delete {}").format(process),
reverse("process:remove", args=(record.id,)),
)
return format_html(html)

View File

@ -0,0 +1,95 @@
{% extends 'base.html' %}
{% load i18n static custom_tags %}
{% block body %}
<div class="rows">
<div class="columns">
<div class="large-10 column">
<h3>{% trans 'Process' %}: {{process.intervention.identifier}}</h3>
<h4>{{process.intervention.title}}</h4>
</div>
<div class="large-2 column">
<a href="{% url 'process:edit' process.id %}">
<button class="button small" role="button" value="{% trans 'Edit' %}"><em class='fas fa-edit'></em> {% trans 'Edit' %}</button>
</a>
</div>
</div>
</div>
<div class="rows">
<table>
<tbody>
<tr>
<th scope="row">{% trans 'Licencing Authority' %}</th>
<td>{{ process.licensing_authority }}</td>
</tr>
<tr>
<th scope="row">{% trans 'Licencing document identifier' %}</th>
<td>{{ process.licensing_authority_document_identifier }}</td>
</tr>
<tr>
<th scope="row">{% trans 'Registration office' %}</th>
<td>{{ process.registration_office }}</td>
</tr>
<tr>
<th scope="row">{% trans 'Registration document identifier' %}</th>
<td>{{ process.registration_office_document_identifier }}</td>
</tr>
<tr>
<th scope="row">{% trans 'Status' %}</th>
<td>
<span class="small info" title="{% trans 'Status' %}">
<em class="fas fa-exclamation-triangle"></em>
{{process.state|resolve_process_state}}
</span>
</td>
</tr>
<tr>
<th scope="row">{% trans 'Intervention' %}</th>
<td>
<a href="{% url 'intervention:open' process.intervention.id %}" target="_blank">
<button class="button small" role="button" title="{{process.intervention.title}} - {{process.intervention.type}}">{{process.intervention.identifier}}</button>
</a>
</td>
</tr>
<tr>
<th scope="row" style="vertical-align: baseline">
{% trans 'Compensations' %}
<br>
<a href="{% url 'process:add-compensation' process.id %}" target="_blank">
<button class="button small" role="button" title="{% trans 'Add a new compensation' %}">
<em class="fas fa-plus-circle"></em>
{% trans 'New' %}
</button>
</a>
</th>
<td>
<table>
<tbody>
{% for compensation in process.compensations.all %}
<tr>
<td>
<a href="{% url 'compensation:open' compensation.id %}" target="_blank">
<button class="button small" role="button" title="{{compensation.title}} - {{compensation.type}}">{{compensation.identifier}}</button>
</a>
</td>
<td class="action-col">
<a href="{% url 'compensation:edit' compensation.id %}" title="{% trans 'Edit' %}"><button class="button small" title="{% trans 'Edit' %}"><em class='fas fa-edit'></em></button></a>
<a href="{% url 'compensation:remove' compensation.id %}" title="{% trans 'Remove' %}"><button class="button small" title="{% trans 'Remove' %}"><em class='fas fa-trash-alt'></em></button></a>
</td>
</tr>
{% empty %}
<tr>
<td>
{% trans 'No compensation' %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
{% endblock %}

3
process/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

22
process/urls.py Normal file
View File

@ -0,0 +1,22 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 25.11.20
"""
from django.urls import path
from process.views import index_view, new_view, open_view, edit_view, remove_view, edit_status_view, \
add_compensation_view
app_name = "process"
urlpatterns = [
path('', index_view, name='index'),
path('new/', new_view, name='new'),
path('open/<id>', open_view, name='open'),
path('edit/<id>', edit_view, name='edit'),
path('edit/<id>/status', edit_status_view, name='edit-status'),
path('remove/<id>', remove_view, name='remove'),
path('ac/<id>', add_compensation_view, name='add-compensation'),
]

217
process/views.py Normal file
View File

@ -0,0 +1,217 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest
from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from compensation.models import Compensation
from konova.contexts import BaseContext
from konova.decorators import resolve_user_role, valid_process_role_required
from konova.forms import RemoveForm
from konova.utils.tables import ChoicesColumnForm
from process.enums import ProcessStateEnum
from process.forms import NewProcessForm, EditProcessForm
from process.models import Process
from process.settings import PROCESS_STATE_STRINGS
from process.tables import ProcessTable
# URL definitions
PROCESS_INDEX_URL = "process:index"
@login_required
@resolve_user_role
def index_view(request: HttpRequest):
"""
Renders the index view for process
Args:
request (HttpRequest): The incoming request
Returns:
A rendered view
"""
template = "generic_index.html"
user = request.user
processes = Process.get_role_objects(user)
table = ProcessTable(
request=request,
queryset=processes
)
context = {
"table": table,
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
def new_view(request: HttpRequest):
"""
Renders a view for a new process creation
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "konova/form.html"
form = NewProcessForm(request.POST or None)
if request.method == "POST":
if form.is_valid():
process = form.save(request.user)
intervention = process.intervention
messages.info(request, _("A process is based on an intervention. Please fill in the missing data for this intervention"))
return redirect("intervention:edit", id=intervention.id)
else:
messages.error(request, _("Invalid input"))
else:
# For clarification: Nothing to do here
pass
context = {
"form": form,
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
def open_view(request: HttpRequest, id: str):
""" Renders a detail view for this process
Args:
request (HttpRequest): The incoming request
id (str): The uuid id as string
Returns:
"""
template = "process/open.html"
process = get_object_or_404(Process, id=id)
context = {
"process": process,
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@resolve_user_role
def edit_view(request: HttpRequest, id: str):
""" Renders an edit view for this process
Args:
request (HttpRequest): The incoming request
id (str): The uuid id as string
Returns:
"""
template = "konova/form.html"
process = get_object_or_404(Process, id=id)
if request.method == "POST":
form = EditProcessForm(request.POST or None, instance=process, user=request.user)
if form.is_valid():
process = form.save(request.user)
messages.success(request, _("{} edited").format(process))
return redirect("process:index")
else:
messages.error(request, _("Invalid input"))
form = EditProcessForm(instance=process, user=request.user)
context = {
"form": form,
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
def remove_view(request: HttpRequest, id: str):
""" Renders a remove view for this process
Args:
request (HttpRequest): The incoming request
id (str): The uuid id as string
Returns:
"""
template = "konova/form.html"
process = get_object_or_404(Process, id=id)
if request.method == "POST":
form = RemoveForm(
request.POST or None,
object_to_remove=process,
remove_post_url=reverse("process:remove", args=(id,)),
cancel_url=reverse("process:index"),
)
if form.is_valid():
confirmed = form.is_checked()
if confirmed:
process.deactivate()
messages.success(request, _("Process {} removed").format(process))
return redirect("process:index")
else:
messages.error(request, _("Invalid input"))
form = RemoveForm(
object_to_remove=process,
remove_post_url=reverse("process:remove", args=(id,)),
cancel_url=reverse("process:index"),
)
context = {
"form": form,
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
def edit_status_view(request: HttpRequest, id: str):
""" Changes only the status of a process
Args:
request (The incoming request):
id ():
Returns:
"""
process = get_object_or_404(Process, id=id)
old_state = process.state
form = ChoicesColumnForm(request.POST or None, choices=ProcessStateEnum.as_choices(drop_empty_choice=True))
if request.method == "POST":
if form.is_valid():
process.state = int(form.cleaned_data.get("select", -1))
process.save()
_from = PROCESS_STATE_STRINGS.get(old_state)
_to = PROCESS_STATE_STRINGS.get(process.state)
messages.info(request, _("{} status changed from {} to {}").format(process.intervention.title, _from, _to))
else:
messages.error(request, form.errors)
return redirect("process:index")
@login_required
def add_compensation_view(request: HttpRequest, id: str):
""" Adds a new compensation to a process
Args:
request (HttpRequest): The incoming request
id (str): The process' id
Returns:
"""
process = get_object_or_404(Process, id=id)
comp = Compensation()
comp.process = process
comp.save()
messages.info(request, _("Please fill in the data for this compensation"))
return redirect("compensation:edit", id=comp.id)
#template = ""
#context = {}
#context = BaseContext(request, context).context
#return render(request, template, context)

20
requirements.txt Normal file
View File

@ -0,0 +1,20 @@
asgiref==3.3.1
certifi==2020.11.8
chardet==3.0.4
Django==3.1.3
django-autocomplete-light==3.8.1
django-debug-toolbar==3.1.1
django-filter==2.4.0
django-fontawesome-5==1.0.18
django-simple-sso==0.14.1
django-tables2==2.3.3
idna==2.10
itsdangerous==1.1.0
pkg-resources==0.0.0
psycopg2==2.8.6
pytz==2020.4
requests==2.25.0
six==1.15.0
sqlparse==0.4.1
urllib3==1.26.2
webservices==0.7

View File

@ -0,0 +1,22 @@
{% load i18n %}
<div class="row">
<div class="small-12 columns menu-container">
<div class="mobile-menu">
<ul>
<li class="menu-trigger">
<button>{% trans 'Menu' %}</button>
</li>
</ul>
</div>
<ul class="dkd_mm_section_list" data-level="1">
<li class="dkd_mm_entry">
<span class="dkd_mm_link" role=button>
<a href="{% url 'home' %}" target="_self" title="{% trans 'Home' %}">
{% trans 'Home' %}
</a>
</span>
</li>
</ul>
</div>
</div>

View File

@ -0,0 +1,128 @@
{% load i18n %}
<div class="row">
<div class="small-12 columns menu-container">
<div class="mobile-menu">
<ul>
<li class="menu-trigger">
<button>{% trans 'Menu' %}</button>
</li>
</ul>
</div>
<ul class="dkd_mm_section_list" data-level="1">
<li class="dkd_mm_entry">
<span class="dkd_mm_link" role=button><a href="{% url 'home' %}" target="_self">{% trans 'Home' %}</a></span>
</li>
<li class="dkd_mm_entry dkd_mm_sub_link">
<span class="dkd_mm_link" role=button><a href="{% url 'process:index' %}" target="_self">{% trans 'Process' %}</a></span>
<ul class="dkd_mm_section_list" data-level="2">
<li class="dkd_mm_section_title">
<span class="dkd_mm_section_title_link">{% trans 'Process management' %}</span>
</li>
<li class="dkd_mm_entry">
<span class="dkd_mm_link" role=button><a href="{% url 'process:index' %}" target="_self">{% trans 'Show process' %}</a></span>
</li>
<li class="dkd_mm_entry">
<span class="dkd_mm_link" role=button><a href="{% url 'process:new' %}" target="_self">{% trans 'New process' %}</a></span>
</li>
</ul>
</li>
<li class="dkd_mm_entry dkd_mm_sub_link">
<span class="dkd_mm_link" role=button><a href="{% url 'intervention:index' %}" target="_self">{% trans 'Intervention' %}</a></span>
<ul class="dkd_mm_section_list" data-level="2">
<li class="dkd_mm_section_title">
<span class="dkd_mm_section_title_link">{% trans 'Intervention management' %}</span>
</li>
<li class="dkd_mm_entry">
<span class="dkd_mm_link" role=button><a href="{% url 'intervention:index' %}" target="_self">{% trans 'Show intervention' %}</a></span>
</li>
<li class="dkd_mm_entry">
<span class="dkd_mm_link" role=button><a href="{% url 'intervention:new' %}" target="_self">{% trans 'New intervention' %}</a></span>
</li>
</ul>
</li>
<li class="dkd_mm_entry dkd_mm_sub_link">
<span class="dkd_mm_link" role=button><a href="{% url 'compensation:index' %}" target="_self">{% trans 'Compensation' %}</a></span>
<ul class="dkd_mm_section_list" data-level="2">
<li class="dkd_mm_section_title">
<span class="dkd_mm_section_title_link">{% trans 'Compensation management' %}</span>
</li>
<li class="dkd_mm_entry">
<span class="dkd_mm_link" role=button><a href="{% url 'compensation:index' %}" target="_self">{% trans 'Show compensation' %}</a></span>
</li>
<li class="dkd_mm_entry">
<span class="dkd_mm_link" role=button><a href="{% url 'compensation:new' %}" target="_self">{% trans 'New compensation' %}</a></span>
</li>
</ul>
</li>
<li class="dkd_mm_entry dkd_mm_sub_link">
<span class="dkd_mm_link" role=button><a href="{% url 'compensation:account-index' %}" target="_self">{% trans 'Eco-account' %}</a></span>
<ul class="dkd_mm_section_list" data-level="2">
<li class="dkd_mm_section_title">
<span class="dkd_mm_section_title_link">{% trans 'Eco-account management' %}</span>
</li>
<li class="dkd_mm_entry">
<span class="dkd_mm_link" role=button><a href="{% url 'compensation:account-index' %}" target="_self">{% trans 'Show eco-accounts' %}</a></span>
</li>
<li class="dkd_mm_entry">
<span class="dkd_mm_link" role=button><a href="{% url 'compensation:account-new' %}" target="_self">{% trans 'New eco-account' %}</a></span>
</li>
<li class="dkd_mm_entry">
<span class="dkd_mm_link" role=button><a href="{% url 'compensation:account-index' %}" target="_self">{% trans 'Withdraw from eco-account' %}</a></span>
</li>
</ul>
</li>
<li class="dkd_mm_entry dkd_mm_sub_link">
<span class="dkd_mm_link" role=button><a href="{% url 'compensation:index' %}" target="_self">EMA Alte Rechtslage</a></span>
<ul class="dkd_mm_section_list" data-level="2">
<li class="dkd_mm_section_title">
<span class="dkd_mm_section_title_link">Maßnahmen aus Ersatzzahlungen - alte Rechtslage (EMA)</span>
</li>
<li class="dkd_mm_entry">
<span class="dkd_mm_link" role=button><a href="{% url 'compensation:index' %}" target="_self">{% trans 'Show actions' %}</a></span>
</li>
<li class="dkd_mm_entry">
<span class="dkd_mm_link" role=button><a href="{% url 'compensation:new' %}" target="_self">{% trans 'New action' %}</a></span>
</li>
</ul>
</li>
<li class="dkd_mm_entry dkd_mm_sub_link">
<span class="dkd_mm_link" role=button><a href="{% url 'compensation:index' %}" target="_self">{% trans 'Organization' %}</a></span>
<ul class="dkd_mm_section_list" data-level="2">
<li class="dkd_mm_section_title">
<span class="dkd_mm_section_title_link">{% trans 'Organization' %}</span>
</li>
<li class="dkd_mm_entry">
<span class="dkd_mm_link" role=button><a href="" target="_self">{% trans 'Import / Export' %}</a></span>
</li>
<li class="dkd_mm_entry">
<span class="dkd_mm_link" role=button><a href="" target="_self">{% trans 'Annual report' %}</a></span>
</li>
<li class="dkd_mm_entry">
<span class="dkd_mm_link" role=button><a href="" target="_self">{% trans 'Settings' %}</a></span>
</li>
<li class="dkd_mm_entry">
<span class="dkd_mm_link" role=button><a href="{{ wiki_url }}" target="_blank">{% trans 'Help' %}</a></span>
</li>
</ul>
</li>
<li class="dkd_mm_entry dkd_mm_sub_link">
<span class="dkd_mm_link" role=button><a href="{% url 'compensation:index' %}" target="_self">{% trans 'User' %}: {{ user }}</a></span>
<ul class="dkd_mm_section_list" data-level="2">
<li class="dkd_mm_section_title">
<span class="dkd_mm_section_title_link">{% trans 'User' %}: {{ user }}</span>
</li>
<li class="dkd_mm_entry">
<span class="dkd_mm_link" role=button><a href="{% url 'logout' %}" target="_self">{% trans 'Logout' %}</a></span>
</li>
</ul>
</li>
</ul>
</div>
</div>
<div class="user-role">
{% trans 'You are currently working as ' %}
<strong>{{ current_role.type }} ({{ current_role.org }})</strong>
<a href="{% url 'home' %}">{% trans 'Change...' %}</a>
</div>

100
templates/base.html Normal file
View File

@ -0,0 +1,100 @@
<!DOCTYPE html>
{% load static i18n fontawesome_5 %}
<html lang="{{ language }}">
<head>
<meta charset="UTF-8">
<title>{{ base_title }}</title>
<link rel="stylesheet" href="{% static 'css/messages.css' %}">
<link rel="stylesheet" href="{% static 'css/mulewf.css' %}">
<link rel="stylesheet" href="{% static 'css/konova.css' %}">
<script type="text/javascript" src="{% static 'js/jquery-3.5.1.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/mulewf.min.js' %}"></script>
{% fontawesome_5_static %}
{% block head %}
{% endblock %}
</head>
<body>
{% if render_header %}
<header class="header">
<div class="background-wrap gray-600">
<div class="row logo-search-area">
<div class="small-6 medium-4 columns">
<h3>{{ base_frontend_title }}</h3>
</div>
<div class="medium-4 columns hide-for-small-only"></div>
<div class="small-6 medium-4 columns last">
<a href="{% url 'home' %}">
<img class="logo" alt="{% trans 'Home' %}"
title="{% trans 'Home' %}"
src="{% static 'images/rlp-logos-MUEEF.png' %}"/>
<noscript>
<img title="rlp" alt="rlp-logo" src="{% static 'images/rlp-logos-MUEEF.png' %}" width="192" height="84" />
</noscript>
</a>
</div>
</div>
</div>
</header>
{% endif %}
<div class="body-content">
<nav class="main-menu static" role="navigation">
{% block navbar %}
{% if user.is_authenticated %}
{% include 'authenticated-user-navbar.html' %}
{% else %}
{% include 'anonymous-user-navbar.html' %}
{% endif %}
{% endblock %}
</nav>
<div class="row">
<div class="large-12 columns">
{% for message in messages %}
<div class="{{ message.tags }}">
{{ message }}
</div>
{% endfor %}
</div>
<div class="small-12 columns">
<div class="column" style="margin-top: 50px;">
{% block body %}
<div class="small-12 medium-3 columns">
{% block body_left %}
&nbsp;
{% endblock %}
</div>
<div class="small-12 medium-6 columns">
{% block body_middle %}
{% endblock %}
</div>
<div class="small-12 medium-3 columns">
{% block body_right %}
&nbsp;
{% endblock %}
</div>
{% endblock %}
</div>
</div>
</div>
</div>
<footer class="footer">
{% block footer %}
<div class="background-wrap gray-500 line"></div>
<div class="row">
<div class="large-6 medium-6 columns">
<h2 class="h6">{% trans 'About this site' %}</h2>
<ul class="no-bullet">
<li><a href="" target="_self">Impressum</a></li>
<li><a href="" target="_self">{% trans 'Privacy policy' %}</a></li>
</ul>
</div>
</div>
{% endblock %}
</footer>
</body>
</html>

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% block body %}
<div class="style-2">
{% include 'table.html' %}
</div>
{% endblock %}

View File

@ -0,0 +1,54 @@
{% load i18n %}
{% block head %}
{{ form.media }}
{% endblock %}
{% block body %}
<div class="table-container">
<h4>
{{ form.form_title }}
</h4>
{% if form.form_caption is not None %}
<div>
{{ form.form_caption }}
</div>
{% endif %}
<form method="post" action="{{ form.action_url }}">
{% csrf_token %}
<table>
<tbody>
{% for field in form %}
<tr title="{{ field.help_text }}" class="{% if field.errors %}error{% endif %}">
<th scope="row" class="small-3">
<div>{{ field.label }}<span class="label-required">{% if field.field.required %}*{% endif %}</span></div>
<small>{{ field.help_text }}</small>
</th>
<td class="small-12">
{{ field }}
{% for error in field.errors %}
<b>{{ error }}</b>
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="label-required">{% trans 'Fields with * are required.' %}</div>
<div class="row">
<div class="large-1 columns">
<a href="{{ form.cancel_redirect }}">
<button class="button small append-value" type="button" title="{% trans 'Cancel' %}">{% trans 'Cancel' %}</button>
</a>
</div>
<div class="large-10 columns">
<button class="button small append-value right" type="submit" title="{% trans 'Save' %}">{% trans 'Save' %}</button>
</div>
</div>
</form>
</div>
{% endblock %}
{% block footer %}
{{ form.media }}
{% endblock %}

44
templates/table.html Normal file
View File

@ -0,0 +1,44 @@
{% load django_tables2 %}
{% load i18n static %}
<div class="rows">
{% if table.title is not None %}
<div class="rows">
<h3>
{{ table.title }}
</h3>
</div>
{% endif %}
<div class="column large-2">
{% if table.user.is_superuser and table.add_new_entries %}
<a href="{{ table.add_new_url }}">
<button class="button small" title="{% trans 'New entry' %}">
<i class="fas fa-plus"></i>
{% trans 'New' %}
</button>
</a>
{% else %}
&nbsp;
{% endif %}
</div>
<div class="column large-8">
&nbsp;
</div>
<div class="column large-2 dropdown-area">
<form method="get">
{{ table.filter.form.as_p }}
</form>
<div class="header-meta-dropdown">
<button data-dropdown="rpp-choice" aria-controls="rpp-choice" aria-expanded="false" class="custom-dropdown left">{% trans 'Results per page' %}</button>
<ul id="rpp-choice" class="custom-dropdown-content" data-dropdown-content aria-hidden="true" style="position: absolute; left: -99999px; top: 25px; right: auto;">
{% for rpp_option in table.results_per_page_choices %}
<li class="{% if table.results_per_page_chosen == rpp_option %}selected{% endif %}">
<a class="" href="{% querystring table.results_per_page_parameter=rpp_option %}">{{ rpp_option }}</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<div class="row">
{% render_table table %}
</div>