Initial
This commit is contained in:
commit
c14e9466fb
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# Project exclude paths
|
||||
/venv/
|
||||
/.idea/
|
||||
*/migrations/
|
0
compensation/__init__.py
Normal file
0
compensation/__init__.py
Normal file
46
compensation/admin.py
Normal file
46
compensation/admin.py
Normal 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
5
compensation/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CompensationConfig(AppConfig):
|
||||
name = 'compensation'
|
14
compensation/forms.py
Normal file
14
compensation/forms.py
Normal 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
145
compensation/models.py
Normal 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
9
compensation/settings.py
Normal 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
118
compensation/tables.py
Normal 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
3
compensation/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
27
compensation/urls.py
Normal file
27
compensation/urls.py
Normal 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
110
compensation/views.py
Normal 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
0
intervention/__init__.py
Normal file
18
intervention/admin.py
Normal file
18
intervention/admin.py
Normal 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
5
intervention/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class InterventionConfig(AppConfig):
|
||||
name = 'intervention'
|
220
intervention/forms.py
Normal file
220
intervention/forms.py
Normal 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
91
intervention/models.py
Normal 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
9
intervention/settings.py
Normal 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
87
intervention/tables.py
Normal 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)
|
47
intervention/templates/intervention/open.html
Normal file
47
intervention/templates/intervention/open.html
Normal 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
3
intervention/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
19
intervention/urls.py
Normal file
19
intervention/urls.py
Normal 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
171
intervention/views.py
Normal 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
0
kspneo/__init__.py
Normal file
32
kspneo/admin.py
Normal file
32
kspneo/admin.py
Normal 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
16
kspneo/asgi.py
Normal 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
42
kspneo/autocompletes.py
Normal 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
53
kspneo/contexts.py
Normal 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
94
kspneo/decorators.py
Normal 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
40
kspneo/enums.py
Normal 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
141
kspneo/forms.py
Normal 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
|
153
kspneo/management/commands/setup.py
Normal file
153
kspneo/management/commands/setup.py
Normal 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")
|
58
kspneo/management/commands/setup_test_data.py
Normal file
58
kspneo/management/commands/setup_test_data.py
Normal 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
104
kspneo/models.py
Normal 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
50
kspneo/settings.py
Normal 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
|
57
kspneo/static/css/kspneo.css
Normal file
57
kspneo/static/css/kspneo.css
Normal 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;
|
||||
}
|
43
kspneo/static/css/messages.css
Normal file
43
kspneo/static/css/messages.css
Normal 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
9877
kspneo/static/css/mulewf.css
Normal file
File diff suppressed because it is too large
Load Diff
BIN
kspneo/static/fonts/rlp-icons.woff
Normal file
BIN
kspneo/static/fonts/rlp-icons.woff
Normal file
Binary file not shown.
BIN
kspneo/static/images/csm_rlp-logo_8de1241483.png
Normal file
BIN
kspneo/static/images/csm_rlp-logo_8de1241483.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.3 KiB |
BIN
kspneo/static/images/header-bg.png
Normal file
BIN
kspneo/static/images/header-bg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
BIN
kspneo/static/images/menu-bg.png
Normal file
BIN
kspneo/static/images/menu-bg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 935 B |
BIN
kspneo/static/images/rlp-logos-MUEEF.png
Normal file
BIN
kspneo/static/images/rlp-logos-MUEEF.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
2
kspneo/static/js/jquery-3.5.1.min.js
vendored
Normal file
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
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
6445
kspneo/static/js/mulewf.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
12
kspneo/sub_settings/context_settings.py
Normal file
12
kspneo/sub_settings/context_settings.py
Normal 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
|
230
kspneo/sub_settings/django_settings.py
Normal file
230
kspneo/sub_settings/django_settings.py
Normal 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,
|
||||
},
|
||||
},
|
||||
}
|
5
kspneo/templates/kspneo/choiceColumnForm.html
Normal file
5
kspneo/templates/kspneo/choiceColumnForm.html
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
<form action="{{ form.action_url }}" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
</form>
|
8
kspneo/templates/kspneo/form.html
Normal file
8
kspneo/templates/kspneo/form.html
Normal file
@ -0,0 +1,8 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block body %}
|
||||
<div class="column">
|
||||
{% include 'generic_table_form.html' %}
|
||||
</div>
|
||||
{% endblock %}
|
44
kspneo/templates/kspneo/home.html
Normal file
44
kspneo/templates/kspneo/home.html
Normal 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 %}
|
7
kspneo/templatetags/__init__.py
Normal file
7
kspneo/templatetags/__init__.py
Normal 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
|
||||
|
||||
"""
|
17
kspneo/templatetags/custom_tags.py
Normal file
17
kspneo/templatetags/custom_tags.py
Normal 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
47
kspneo/urls.py
Normal 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)),
|
||||
]
|
22
kspneo/utils/generators.py
Normal file
22
kspneo/utils/generators.py
Normal 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
52
kspneo/utils/mailer.py
Normal 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
45
kspneo/utils/session.py
Normal 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
121
kspneo/utils/tables.py
Normal 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
67
kspneo/views.py
Normal 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
16
kspneo/wsgi.py
Normal 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()
|
BIN
locale/de/LC_MESSAGES/django.mo
Normal file
BIN
locale/de/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1723
locale/de/LC_MESSAGES/django.po
Normal file
1723
locale/de/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
0
logs/error.log
Normal file
0
logs/error.log
Normal file
22
manage.py
Executable file
22
manage.py
Executable 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
0
organisation/__init__.py
Normal file
15
organisation/admin.py
Normal file
15
organisation/admin.py
Normal 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
5
organisation/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class OrganisationConfig(AppConfig):
|
||||
name = 'organisation'
|
26
organisation/enums.py
Normal file
26
organisation/enums.py
Normal 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
18
organisation/models.py
Normal 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
22
organisation/settings.py
Normal 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
3
organisation/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
12
organisation/urls.py
Normal file
12
organisation/urls.py
Normal 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
3
organisation/views.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
0
process/__init__.py
Normal file
0
process/__init__.py
Normal file
38
process/admin.py
Normal file
38
process/admin.py
Normal 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
5
process/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ProcessConfig(AppConfig):
|
||||
name = 'process'
|
202
process/enums.py
Normal file
202
process/enums.py
Normal 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
218
process/forms.py
Normal 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
158
process/models.py
Normal 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
19
process/settings.py
Normal 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
130
process/tables.py
Normal 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)
|
95
process/templates/process/open.html
Normal file
95
process/templates/process/open.html
Normal 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
3
process/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
22
process/urls.py
Normal file
22
process/urls.py
Normal 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
217
process/views.py
Normal 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
20
requirements.txt
Normal 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
|
22
templates/anonymous-user-navbar.html
Normal file
22
templates/anonymous-user-navbar.html
Normal 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>
|
128
templates/authenticated-user-navbar.html
Normal file
128
templates/authenticated-user-navbar.html
Normal 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
100
templates/base.html
Normal 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 %}
|
||||
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div class="small-12 medium-6 columns">
|
||||
{% block body_middle %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div class="small-12 medium-3 columns">
|
||||
{% block body_right %}
|
||||
|
||||
{% 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>
|
7
templates/generic_index.html
Normal file
7
templates/generic_index.html
Normal file
@ -0,0 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="style-2">
|
||||
{% include 'table.html' %}
|
||||
</div>
|
||||
{% endblock %}
|
54
templates/generic_table_form.html
Normal file
54
templates/generic_table_form.html
Normal 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
44
templates/table.html
Normal 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 %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="column large-8">
|
||||
|
||||
</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>
|
Loading…
Reference in New Issue
Block a user