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

0
kspneo/__init__.py Normal file
View File

32
kspneo/admin.py Normal file
View File

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

16
kspneo/asgi.py Normal file
View File

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

42
kspneo/autocompletes.py Normal file
View File

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

53
kspneo/contexts.py Normal file
View File

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

94
kspneo/decorators.py Normal file
View File

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

40
kspneo/enums.py Normal file
View File

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

141
kspneo/forms.py Normal file
View File

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

View File

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

View File

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

104
kspneo/models.py Normal file
View File

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

50
kspneo/settings.py Normal file
View File

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

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

47
kspneo/urls.py Normal file
View File

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

View File

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

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

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

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

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

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

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

67
kspneo/views.py Normal file
View File

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

16
kspneo/wsgi.py Normal file
View File

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