Compare commits

..

No commits in common. "master" and "1.9.2" have entirely different histories.

34 changed files with 634 additions and 513 deletions

View File

@ -24,7 +24,6 @@ DEFAULT_FROM_EMAIL=service@ksp.de
# Proxy
PROXY=CHANGE_ME
MAP_PROXY_HOST_WHITELIST=CHANGE_ME_1,CHANGE_ME_2
GEOPORTAL_RLP_USER=CHANGE_ME
GEOPORTAL_RLP_PASSWORD=CHANGE_ME
@ -38,7 +37,6 @@ SSO_SERVER_BASE_URL=https://login.naturschutz.rlp.de
OAUTH_CODE_VERIFIER=CHANGE_ME
OAUTH_CLIENT_ID=CHANGE_ME
OAUTH_CLIENT_SECRET=CHANGE_ME
PROPAGATION_SECRET=CHANGE_ME
# RabbitMQ
## For connections to EGON

View File

@ -51,7 +51,7 @@ class APIUserToken(models.Model):
if token_obj.valid_until is not None and token_obj.valid_until < _today:
raise PermissionError("Token validity expired")
except ObjectDoesNotExist:
raise PermissionError("Token unknown")
raise PermissionError("Credentials invalid")
return token_obj.user
@ -155,25 +155,3 @@ class OAuthToken(UuidModel):
return user
def revoke(self) -> int:
""" Revokes the OAuth2 token of the user
(/o/revoke_token/ indeed removes the corresponding access token on provider side and invalidates the
submitted refresh token in one step)
Returns:
revocation_status_code (int): HTTP status code for revocation of refresh_token
"""
revoke_url = f"{SSO_SERVER_BASE}o/revoke_token/"
token = self.refresh_token
revocation_status_code = requests.post(
revoke_url,
data={
'token': token,
'token_type_hint': "refresh_token",
},
auth=(OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET),
).status_code
return revocation_status_code

View File

@ -50,18 +50,13 @@ class AbstractAPIView(View):
def dispatch(self, request, *args, **kwargs):
try:
# Fetch the proper user from the given request header token
token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)
ksp_token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)
ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None)
token_user = APIUserToken.get_user_from_token(ksp_token)
if not token and not ksp_user:
bearer_token = request.headers.get("authorization", None)
if not bearer_token:
raise PermissionError("No token provided")
token = bearer_token.split(" ")[1]
token_user = APIUserToken.get_user_from_token(token)
if ksp_user and ksp_user != token_user.username:
if ksp_user != token_user.username:
raise PermissionError(f"Invalid token for {ksp_user}")
else:
self.user = token_user
request.user = self.user

View File

@ -82,8 +82,8 @@ class Command(BaseKonovaCommand):
atom_id = element.find("atomid").text
selectable = element.find("selectable").text.lower()
selectable = bool_map.get(selectable, False)
short_name = element.find("shortname").text or ""
long_name = element.find("longname").text or ""
short_name = element.find("shortname").text
long_name = element.find("longname").text
is_archived = bool_map.get((element.find("archive").text.lower()), False)
code = KonovaCode.objects.get_or_create(

View File

@ -1,25 +0,0 @@
# Generated by Django 5.0.8 on 2024-08-26 16:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('codelist', '0002_migrate_975_to_288'),
]
operations = [
migrations.AlterField(
model_name='konovacode',
name='long_name',
field=models.CharField(blank=True, default="", max_length=1000),
preserve_default=False,
),
migrations.AlterField(
model_name='konovacode',
name='short_name',
field=models.CharField(blank=True, default="", help_text='Short version of long name', max_length=500),
preserve_default=False,
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 5.0.8 on 2024-08-26 16:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('codelist', '0003_alter_konovacode_long_name_and_more'),
('compensation', '0015_alter_compensation_after_states_and_more'),
]
operations = [
migrations.AlterField(
model_name='compensationstate',
name='biotope_type_details',
field=models.ManyToManyField(blank=True, limit_choices_to={'code_lists__in': [288], 'is_archived': False, 'is_selectable': True}, related_name='+', to='codelist.konovacode'),
),
]

View File

@ -6,11 +6,11 @@ Created on: 26.10.22
"""
import zipfile
from datetime import datetime
from io import BytesIO
from django.core.mail import EmailMessage
from django.utils import timezone
from django.utils.datetime_safe import datetime
from analysis.utils.excel.excel import TempExcelFile
from analysis.utils.report import TimespanReport

View File

@ -1,88 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.01.22
"""
import datetime
from django.contrib.gis.db.models.functions import Area
from konova.management.commands.setup import BaseKonovaCommand
from konova.models import Geometry, ParcelIntersection
class Command(BaseKonovaCommand):
help = "Recalculates parcels for entries with geometry but missing parcel information"
def add_arguments(self, parser):
parser.add_argument(
"--force-all",
action="store_true",
default=False,
help="If Attribute set, all entries parcels will be recalculated"
)
def handle(self, *args, **options):
try:
self.recalculate_parcels(options)
except KeyboardInterrupt:
self._break_line()
exit(-1)
def recalculate_parcels(self, options: dict):
force_all = options.get("force_all", False)
geometry_objects = Geometry.objects.filter(
geom__isempty=False,
).exclude(
geom=None
)
if not force_all:
# Fetch all intersections
intersection_objs = ParcelIntersection.objects.filter(
geometry__in=geometry_objects
)
# Just take the geometry ids, which seem to have intersections
geom_with_intersection_ids = intersection_objs.values_list(
"geometry__id",
flat=True
)
# ... and resolve into Geometry objects again ...
intersected_geom_objs = Geometry.objects.filter(
id__in=geom_with_intersection_ids
)
# ... to be able to use the way more efficient difference() function ...
geometry_objects_ids = geometry_objects.difference(intersected_geom_objs).values_list("id", flat=True)
# ... so we can resolve these into proper Geometry objects again for further annotation usage
geometry_objects = Geometry.objects.filter(id__in=geometry_objects_ids)
self._write_warning("=== Update parcels and districts ===")
# Order geometries by size to process smaller once at first
geometries = geometry_objects.annotate(
area=Area("geom")
).order_by(
'area'
)
self._write_warning(f"Process parcels for {geometries.count()} geometry entries now ...")
i = 0
num_geoms = geometries.count()
geoms_with_errors = {}
for geometry in geometries:
self._write_warning(f"--- {datetime.datetime.now()} Process {geometry.id} now ...")
try:
geometry.update_parcels()
self._write_warning(f"--- Processed {geometry.get_underlying_parcels().count()} underlying parcels")
except Exception as e:
geoms_with_errors[geometry.id] = str(e)
i += 1
self._write_warning(f"--- {i}/{num_geoms} processed")
self._write_success("Updating parcels done!")
for key, val in geoms_with_errors.items():
self._write_error(f" Error on {key}: {val}")
self._write_success(f"{num_geoms - len(geoms_with_errors)} geometries successfuly recalculated!")
self._break_line()

View File

@ -0,0 +1,51 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 19.08.21
"""
from django.core.management import BaseCommand
from intervention.models import Intervention
class Command(BaseCommand):
help = "Performs test on collisions using the identifier generation"
def handle(self, *args, **options):
identifiers = {}
max_iterations = 100000
try:
collisions = 0
len_ids = len(identifiers)
while len_ids < max_iterations:
tmp_intervention = Intervention()
_id = tmp_intervention.generate_new_identifier()
len_ids = len(identifiers)
if _id not in identifiers:
if len_ids % (max_iterations/5) == 0:
print(len_ids)
identifiers[_id] = None
else:
collisions += 1
print("+++ Collision after {} identifiers +++".format(len_ids))
except KeyboardInterrupt:
self._break_line()
exit(-1)
print(
"\n{} collisions in {} identifiers; Collision rate {}%".format(
collisions,
len_ids,
(collisions / len_ids)*100,
)
)
def _break_line(self):
""" Simply prints a line break
Returns:
"""
self.stdout.write("\n")

View File

@ -0,0 +1,54 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.01.22
"""
import datetime
from django.contrib.gis.db.models.functions import Area
from konova.management.commands.setup import BaseKonovaCommand
from konova.models import Geometry, Parcel, District
class Command(BaseKonovaCommand):
help = "Checks the database' sanity and removes unused entries"
def handle(self, *args, **options):
try:
self.update_all_parcels()
except KeyboardInterrupt:
self._break_line()
exit(-1)
def update_all_parcels(self):
num_parcels_before = Parcel.objects.count()
num_districts_before = District.objects.count()
self._write_warning("=== Update parcels and districts ===")
# Order geometries by size to process smaller once at first
geometries = Geometry.objects.all().exclude(
geom=None
).annotate(area=Area("geom")).order_by(
'area'
)
self._write_warning(f"Process parcels for {geometries.count()} geometry entries now ...")
i = 0
num_geoms = geometries.count()
for geometry in geometries:
self._write_warning(f"--- {datetime.datetime.now()} Process {geometry.id} now ...")
geometry.update_parcels()
self._write_warning(f"--- Processed {geometry.get_underlying_parcels().count()} underlying parcels")
i += 1
self._write_warning(f"--- {i}/{num_geoms} processed")
num_parcels_after = Parcel.objects.count()
num_districts_after = District.objects.count()
if num_parcels_after != num_parcels_before:
self._write_error(f"Parcels have changed: {num_parcels_before} to {num_parcels_after} entries. You should run the sanitize command.")
if num_districts_after != num_districts_before:
self._write_error(f"Districts have changed: {num_districts_before} to {num_districts_after} entries. You should run the sanitize command.")
self._write_success("Updating parcels done!")
self._break_line()

View File

@ -8,7 +8,7 @@ Created on: 15.11.21
import json
from django.contrib.gis.db.models import MultiPolygonField
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.core.exceptions import ObjectDoesNotExist
from django.db import models, transaction
from django.utils import timezone
@ -223,17 +223,6 @@ class Geometry(BaseResource):
)
parcel_obj.updated_on = _now
parcels_to_update.append(parcel_obj)
except MultipleObjectsReturned:
parcel_obj = Parcel.make_unique(
district=district,
municipal=municipal,
parcel_group=parcel_group,
flr=flr_val,
flrstck_nnr=flrstck_nnr,
flrstck_zhlr=flrstck_zhlr,
)
parcel_obj.updated_on = _now
parcels_to_update.append(parcel_obj)
except ObjectDoesNotExist:
# If not existing, create object but do not commit, yet
parcel_obj = Parcel(
@ -377,10 +366,11 @@ class Geometry(BaseResource):
diff = geom_envelope - self.geom
if diff.area == 0:
complexity_factor = 1
ratio = 1
else:
complexity_factor = self.geom.area / diff.area
ratio = self.geom.area / diff.area
complexity_factor = 1 - ratio
return complexity_factor

View File

@ -5,7 +5,7 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.12.21
"""
from django.db import models, transaction
from django.db import models
from konova.models import UuidModel
@ -158,46 +158,6 @@ class Parcel(UuidModel):
def __str__(self):
return f"{self.parcel_group} | {self.flr} | {self.flrstck_zhlr} | {self.flrstck_nnr}"
@classmethod
def make_unique(cls, **kwargs):
""" Checks for duplicates of a Parcel, choose a (now) unique one,
repairs relations for ParcelIntersection and removes duplicates.
Args:
**kwargs ():
Returns:
unique_true (Parcel): The new unique 'true one'
"""
parcel_objs = Parcel.objects.filter(**kwargs)
if not parcel_objs.exists():
return None
# Get one of the found parcels and use it as new 'true one'
unique_parcel = parcel_objs.first()
# separate it from the rest
parcel_objs = parcel_objs.exclude(id=unique_parcel.id)
if not parcel_objs.exists():
# There are no duplicates - all good, just return
return unique_parcel
# Fetch existing intersections, which still point on the duplicated parcels
intersection_objs = ParcelIntersection.objects.filter(
parcel__in=parcel_objs
)
# Change each intersection, so they point on the 'true one' parcel from now on
for intersection in intersection_objs:
intersection.parcel = unique_parcel
intersection.save()
# Remove the duplicated parcels
parcel_objs.delete()
return unique_parcel
class ParcelIntersection(UuidModel):
"""

View File

@ -66,6 +66,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'django.contrib.gis',
'django.contrib.humanize',
'simple_sso.sso_server',
'django_tables2',
'bootstrap_modal_forms',
'fontawesome_5',

View File

@ -5,7 +5,6 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 31.01.22
"""
from konova.sub_settings.django_settings import env
# MAPS
DEFAULT_LAT = 50.00
@ -29,6 +28,3 @@ LANIS_ZOOM_LUT = {
1000: 30,
500: 31,
}
MAP_PROXY_HOST_WHITELIST = env.list("MAP_PROXY_HOST_WHITELIST")
i = 0

View File

@ -16,5 +16,3 @@ OAUTH_CODE_VERIFIER = env("OAUTH_CODE_VERIFIER")
OAUTH_CLIENT_ID = env("OAUTH_CLIENT_ID")
OAUTH_CLIENT_SECRET = env("OAUTH_CLIENT_SECRET")
PROPAGATION_SECRET = env("PROPAGATION_SECRET")

View File

@ -91,6 +91,3 @@ INTERVENTION_HAS_REVOCATIONS_TEMPLATE = _("This intervention has {} revocations"
DATA_CHECKED_ON_TEMPLATE = _("Checked on {} by {}")
DATA_CHECKED_PREVIOUSLY_TEMPLATE = _("Data has changed since last check on {} by {}")
DATA_IS_UNCHECKED = _("Current data not checked yet")
# API TOKEN SETTINGS
NEW_API_TOKEN_GENERATED = _("New token generated. Administrators need to validate.")

78
konova/utils/messenger.py Normal file
View File

@ -0,0 +1,78 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 17.08.21
"""
from collections import Iterable
import requests
from user.models import User
from django.utils.translation import gettext_lazy as _
from konova.settings import SSO_SERVER_BASE, SSO_PUBLIC_KEY, PROXIES
from konova.sub_settings.context_settings import BASE_TITLE_SHORT
class Messenger:
""" Used to send messages to the SSO server.
Messages can be seen by the user the next time they login on their SSO dashboard.
Documentation for SSO Server-Client communication can be found here:
https://git.naturschutz.rlp.de/SGD-Nord/arnova/wiki/Messages
"""
server_url = "{}communication/message/".format(SSO_SERVER_BASE)
def __init__(self, users: Iterable, subject: str = None, body: str = None, type: str = None):
self.users = users
self.msg_subject = subject
self.msg_body = body
self.msg_type = type
def send(self):
""" Sends a message
"""
if self.msg_body is None or len(self.msg_body) == 0:
raise AttributeError("No message body set")
headers = {
"x-services-public-key": SSO_PUBLIC_KEY
}
for user in self.users:
data = {
"type": self.msg_type,
"sender": BASE_TITLE_SHORT,
"receiver": user.username,
"subject": self.msg_subject,
"body": self.msg_body,
}
requests.post(
self.server_url,
data=data,
headers=headers,
proxies=PROXIES
)
def send_object_checked(self, obj_identifier: str, performing_user: User, detail_view_url: str = ""):
""" Wraps sending of a message related to the checking of an object, like an intervention
Args:
obj_identifier (str): The object's identifier (e.g. 'EIV-123'
performing_user (User): The user who performed the checking
detail_view_url (str): If a direct link to the object shall be added to the message, it can be provided here
Returns:
"""
self.msg_subject = _("{} checked").format(obj_identifier)
if len(detail_view_url) > 0:
detail_view_url = _('<a href="{}">Check it out</a>').format(detail_view_url)
self.msg_body = _("{} has been checked successfully by user {}! {}").format(
obj_identifier,
performing_user.username,
detail_view_url
)
self.send()

View File

@ -55,11 +55,11 @@ class ParcelFetcher:
content = json.loads(response.content.decode("utf-8"))
except JSONDecodeError:
content = {}
_next = content.get("next", None)
next = content.get("next", None)
fetched_parcels = content.get("results", [])
self.results += fetched_parcels
if _next:
self.get_parcels(_next)
if next:
self.get_parcels(next)
return self.results

View File

@ -5,6 +5,7 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.gis.geos import MultiPolygon
from django.http import HttpResponse, HttpRequest
from django.shortcuts import get_object_or_404

View File

@ -24,13 +24,5 @@ class LogoutView(View):
Returns:
A redirect
"""
user = request.user
try:
oauth_token = user.oauth_token
if oauth_token:
oauth_token.revoke()
except AttributeError:
pass
logout(request)
return redirect(SSO_SERVER_BASE)

View File

@ -9,7 +9,6 @@ import json
from json import JSONDecodeError
import requests
import urllib3.util
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, HttpRequest
from django.utils.decorators import method_decorator
@ -19,7 +18,6 @@ from django.utils.translation import gettext_lazy as _
from requests.auth import HTTPDigestAuth
from konova.sub_settings.lanis_settings import MAP_PROXY_HOST_WHITELIST
from konova.sub_settings.proxy_settings import PROXIES, GEOPORTAL_RLP_USER, GEOPORTAL_RLP_PASSWORD
@ -34,13 +32,6 @@ class BaseClientProxyView(View):
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def _check_with_whitelist(self, url):
parsed_url = urllib3.util.parse_url(url)
parsed_url_host = parsed_url.host
whitelist = set(MAP_PROXY_HOST_WHITELIST)
is_allowed = parsed_url_host in whitelist
return is_allowed
def perform_url_call(self, url, headers={}, auth=None):
""" Generic proxied call
@ -68,11 +59,6 @@ class ClientProxyParcelSearch(BaseClientProxyView):
def get(self, request: HttpRequest):
url = request.META.get("QUERY_STRING")
is_url_allowed = self._check_with_whitelist(url)
if not is_url_allowed:
raise PermissionError(f"Proxied url '{url}' is not allowed!")
content, response_code = self.perform_url_call(url)
try:
body = json.loads(content)

View File

@ -115,10 +115,10 @@ class OAuthCallbackView(View):
if status_code_invalid:
raise RuntimeError(f"OAuth access token could not be fetched: {access_code_response.text}")
oauth_token = OAuthToken.from_access_token_response(access_code_response_body, received_on)
oauth_token.save()
user = oauth_token.update_and_get_user()
user.oauth_replace_token(oauth_token)
oauth_access_token = OAuthToken.from_access_token_response(access_code_response_body, received_on)
oauth_access_token.save()
user = oauth_access_token.update_and_get_user()
user.oauth_replace_token(oauth_access_token)
login(request, user)
return redirect("home")

Binary file not shown.

View File

@ -38,14 +38,13 @@
#: konova/forms/modals/remove_form.py:23
#: konova/forms/modals/resubmission_form.py:22
#: konova/forms/modals/resubmission_form.py:38 konova/forms/remove_form.py:25
#: konova/tests/unit/test_forms.py:59 user/forms/modals/api_token.py:17
#: user/forms/user.py:39
#: konova/tests/unit/test_forms.py:59 user/forms/user.py:39
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-08 15:26+0100\n"
"POT-Creation-Date: 2024-08-19 10:32+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -374,6 +373,7 @@ msgid "Identifier"
msgstr "Kennung"
#: compensation/forms/compensation.py:33 intervention/forms/intervention.py:33
#: user/forms/user.py:77
msgid "Generated automatically - not editable"
msgstr "Automatisch generiert - nicht bearbeitbar"
@ -1795,7 +1795,8 @@ msgstr "Bearbeitender Nutzer"
msgid ""
"Search for entries where this person has been participated according to log "
"history"
msgstr "Sucht nach Einträgen, an denen diese Person gearbeitet hat"
msgstr ""
"Sucht nach Einträgen, an denen diese Person gearbeitet hat"
#: konova/forms/base_form.py:23 templates/form/collapsable/form.html:62
msgid "Save"
@ -1857,7 +1858,6 @@ msgstr ""
"Ich, {} {}, bestätige, dass diese Daten wieder entzeichnet werden müssen."
#: konova/forms/modals/remove_form.py:22 konova/forms/remove_form.py:24
#: user/forms/modals/api_token.py:16
msgid "Confirm"
msgstr "Bestätige"
@ -2283,10 +2283,6 @@ msgstr ""
msgid "Current data not checked yet"
msgstr "Momentane Daten noch nicht geprüft"
#: konova/utils/message_templates.py:96
msgid "New token generated. Administrators need to validate."
msgstr "Neuer Token generiert. Administratoren sind informiert."
#: konova/utils/messenger.py:70
msgid "{} checked"
msgstr "{} geprüft"
@ -2325,7 +2321,7 @@ msgstr "Home"
msgid "Log"
msgstr "Log"
#: konova/views/map_proxy.py:84
#: konova/views/map_proxy.py:70
msgid ""
"The external service is currently unavailable.<br>Please try again in a few "
"moments..."
@ -2835,8 +2831,10 @@ msgid "Reports"
msgstr "Berichte"
#: templates/navbars/navbar.html:57
#, fuzzy
#| msgid "Admins"
msgid "Admin"
msgstr ""
msgstr "Administratoren"
#: templates/navbars/navbar.html:59 user/templates/user/index.html:31
msgid "Settings"
@ -2875,21 +2873,6 @@ msgstr ""
"Falls die Geometrie nicht leer ist, werden die Flurstücke aktuell berechnet. "
"Bitte laden Sie diese Seite in ein paar Augenblicken erneut..."
#: user/forms/modals/api_token.py:25
msgid "Generate API Token"
msgstr "API Token generieren"
#: user/forms/modals/api_token.py:29
msgid ""
"You are about to create a new API token. The existing one will not be usable "
"afterwards."
msgstr ""
"Wenn Sie fortfahren, generieren Sie einen neuen API Token. Ihren existierenden werden Sie dann nicht länger nutzen können."
#: user/forms/modals/api_token.py:31
msgid "A new token needs to be validated by an administrator!"
msgstr "Neue Tokens müssen durch Administratoren freigeschaltet werden!"
#: user/forms/modals/team.py:20 user/forms/modals/team.py:24
#: user/forms/team.py:17 user/forms/team.py:22
msgid "Team name"
@ -2912,11 +2895,11 @@ msgstr ""
"Mehrfachauswahl möglich - Sie können nur Nutzer wählen, die noch nicht "
"Mitglieder dieses Teams sind. Geben Sie den ganzen Nutzernamen an."
#: user/forms/modals/team.py:56 user/tests/unit/test_forms.py:29
#: user/forms/modals/team.py:56 user/tests/unit/test_forms.py:31
msgid "Create new team"
msgstr "Neues Team anlegen"
#: user/forms/modals/team.py:57 user/tests/unit/test_forms.py:30
#: user/forms/modals/team.py:57 user/tests/unit/test_forms.py:32
msgid ""
"You will become the administrator for this group by default. You do not need "
"to add yourself to the list of members."
@ -2945,11 +2928,11 @@ msgid "There must be at least one admin on this team."
msgstr "Es muss mindestens einen Administrator für das Team geben."
#: user/forms/modals/team.py:160 user/templates/user/team/index.html:60
#: user/tests/unit/test_forms.py:86
#: user/tests/unit/test_forms.py:88
msgid "Edit team"
msgstr "Team bearbeiten"
#: user/forms/modals/team.py:187 user/tests/unit/test_forms.py:163
#: user/forms/modals/team.py:187 user/tests/unit/test_forms.py:165
msgid ""
"ATTENTION!\n"
"\n"
@ -2966,7 +2949,7 @@ msgstr ""
"Sind Sie sicher, dass Sie dieses Team löschen möchten?"
#: user/forms/modals/team.py:197 user/templates/user/team/index.html:56
#: user/tests/unit/test_forms.py:196
#: user/tests/unit/test_forms.py:198
msgid "Leave team"
msgstr "Team verlassen"
@ -2998,10 +2981,22 @@ msgstr "Benachrichtigungen"
msgid "Select the situations when you want to receive a notification"
msgstr "Wann wollen Sie per E-Mail benachrichtigt werden?"
#: user/forms/user.py:38 user/tests/unit/test_forms.py:232
#: user/forms/user.py:38 user/tests/unit/test_forms.py:234
msgid "Edit notifications"
msgstr "Benachrichtigungen bearbeiten"
#: user/forms/user.py:73
msgid "Token"
msgstr ""
#: user/forms/user.py:88 user/tests/unit/test_forms.py:260
msgid "Create new token"
msgstr "Neuen Token generieren"
#: user/forms/user.py:89 user/tests/unit/test_forms.py:261
msgid "A new token needs to be validated by an administrator!"
msgstr "Neue Tokens müssen durch Administratoren freigeschaltet werden!"
#: user/models/user_action.py:23
msgid "Unrecorded"
msgstr "Entzeichnet"
@ -3056,7 +3051,7 @@ msgid "Manage teams"
msgstr ""
#: user/templates/user/index.html:53 user/templates/user/team/index.html:19
#: user/views/views.py:135
#: user/views/views.py:171
msgid "Teams"
msgstr ""
@ -3092,58 +3087,270 @@ msgstr "API Einstellungen"
msgid "Current token"
msgstr "Aktueller Token"
#: user/templates/user/token.html:15
msgid "Create new token"
msgstr "Neuen Token generieren"
#: user/templates/user/token.html:23
#: user/templates/user/token.html:14
msgid "Authenticated by admins"
msgstr "Von Admin freigeschaltet"
#: user/templates/user/token.html:27
#: user/templates/user/token.html:18
msgid "Token has been verified and can be used"
msgstr "Token wurde freigeschaltet und kann verwendet werden"
#: user/templates/user/token.html:29
#: user/templates/user/token.html:20
msgid "Token waiting for verification"
msgstr "Token noch nicht freigeschaltet"
#: user/templates/user/token.html:33
#: user/templates/user/token.html:24
msgid "Valid until"
msgstr "Läuft ab am"
#: user/views/api_token.py:33
msgid "User API token"
msgstr "API Nutzer Token"
#: user/views/views.py:33
#: user/views/views.py:35
msgid "User settings"
msgstr "Einstellungen"
#: user/views/views.py:59
#: user/views/views.py:61
msgid "Notifications edited"
msgstr "Benachrichtigungen bearbeitet"
#: user/views/views.py:71
#: user/views/views.py:73
msgid "User notifications"
msgstr "Benachrichtigungen"
#: user/views/views.py:147
#: user/views/views.py:96
msgid "New token generated. Administrators need to validate."
msgstr "Neuer Token generiert. Administratoren sind informiert."
#: user/views/views.py:107
msgid "User API token"
msgstr "API Nutzer Token"
#: user/views/views.py:183
msgid "New team added"
msgstr "Neues Team hinzugefügt"
#: user/views/views.py:162
#: user/views/views.py:198
msgid "Team edited"
msgstr "Team bearbeitet"
#: user/views/views.py:177
#: user/views/views.py:213
msgid "Team removed"
msgstr "Team gelöscht"
#: user/views/views.py:192
#: user/views/views.py:228
msgid "You are not a member of this team"
msgstr "Sie sind kein Mitglied dieses Teams"
#: user/views/views.py:199
#: user/views/views.py:235
msgid "Left Team"
msgstr "Team verlassen"
#~ msgid "close"
#~ msgstr "Schließen"
#~ msgid "Options"
#~ msgstr "Optionen"
#~ msgid "Commands"
#~ msgstr "Befehle"
#~ msgid "Missing command."
#~ msgstr "Befehl fehlt"
#~ msgid "Missing argument"
#~ msgstr "Argument fehlt"
#~ msgid "Missing option"
#~ msgstr "Option fehlt"
#~ msgid "Missing parameter"
#~ msgstr "Parameter fehlt"
#~ msgid "Messages"
#~ msgstr "Nachrichten"
#~ msgid "This field is required."
#~ msgstr "Pflichtfeld"
#~ msgid "Monday"
#~ msgstr "Montag"
#~ msgid "Tuesday"
#~ msgstr "Dienstag"
#~ msgid "Wednesday"
#~ msgstr "Mittwoch"
#~ msgid "Thursday"
#~ msgstr "Donnerstag"
#~ msgid "Friday"
#~ msgstr "Freitag"
#~ msgid "Saturday"
#~ msgstr "Samstag"
#~ msgid "Sunday"
#~ msgstr "Sonntag"
#~ msgid "Mon"
#~ msgstr "Mo"
#~ msgid "Tue"
#~ msgstr "Di"
#~ msgid "Wed"
#~ msgstr "Mi"
#~ msgid "Thu"
#~ msgstr "Do"
#~ msgid "Fri"
#~ msgstr "Fr"
#~ msgid "Sat"
#~ msgstr "Sa"
#~ msgid "Sun"
#~ msgstr "So"
#~ msgid "January"
#~ msgstr "Januar"
#~ msgid "February"
#~ msgstr "Februar"
#~ msgid "March"
#~ msgstr "März"
#~ msgid "May"
#~ msgstr "Mai"
#~ msgid "June"
#~ msgstr "Juni"
#~ msgid "July"
#~ msgstr "Juli"
#~ msgid "October"
#~ msgstr "Oktober"
#~ msgid "December"
#~ msgstr "Dezember"
#~ msgid "mar"
#~ msgstr "mär"
#~ msgid "may"
#~ msgstr "mai"
#~ msgid "oct"
#~ msgstr "okt"
#~ msgid "dec"
#~ msgstr "dez"
#~ msgctxt "abbrev. month"
#~ msgid "March"
#~ msgstr "Mär"
#~ msgctxt "abbrev. month"
#~ msgid "May"
#~ msgstr "Mai"
#~ msgctxt "abbrev. month"
#~ msgid "June"
#~ msgstr "Juni"
#~ msgctxt "abbrev. month"
#~ msgid "July"
#~ msgstr "Juli"
#~ msgctxt "abbrev. month"
#~ msgid "Oct."
#~ msgstr "Okt."
#~ msgctxt "abbrev. month"
#~ msgid "Dec."
#~ msgstr "Dez."
#~ msgctxt "alt. month"
#~ msgid "January"
#~ msgstr "Januar"
#~ msgctxt "alt. month"
#~ msgid "February"
#~ msgstr "Februar"
#~ msgctxt "alt. month"
#~ msgid "March"
#~ msgstr "März"
#~ msgctxt "alt. month"
#~ msgid "May"
#~ msgstr "Mai"
#~ msgctxt "alt. month"
#~ msgid "June"
#~ msgstr "Juni"
#~ msgctxt "alt. month"
#~ msgid "July"
#~ msgstr "Juli"
#~ msgctxt "alt. month"
#~ msgid "October"
#~ msgstr "Oktober"
#~ msgctxt "alt. month"
#~ msgid "December"
#~ msgstr "Dezember"
#~ msgid "or"
#~ msgstr "oder"
#~ msgid ""
#~ "Deductable surface can not be larger than existing surfaces in after "
#~ "states"
#~ msgstr ""
#~ "Die abbuchbare Fläche darf die Gesamtfläche der Zielzustände nicht "
#~ "überschreiten"
#~ msgid ""
#~ "Deductable surface can not be smaller than the sum of already existing "
#~ "deductions. Please contact the responsible users for the deductions!"
#~ msgstr ""
#~ "Es wurde bereits mehr Fläche abgebucht, als Sie nun als abbuchbar "
#~ "einstellen wollen. Kontaktieren Sie die für die Abbuchungen "
#~ "verantwortlichen Nutzer!"
#~ msgid "Added deadline"
#~ msgstr "Frist/Termin hinzugefügt"
#~ msgid "Change default configuration for your KSP map"
#~ msgstr "Karteneinstellungen ändern"
#~ msgid "Map settings"
#~ msgstr "Karte"
#~ msgid "There are errors on this intervention:"
#~ msgstr "Es liegen Fehler in diesem Eingriff vor:"
#~ msgid "Before"
#~ msgstr "Vor"
#~ msgid "Groups"
#~ msgstr "Gruppen"
#~ msgid "Show more..."
#~ msgstr "Mehr anzeigen..."
#~ msgid "Kreis"
#~ msgstr "Kreis"
#~ msgid "Gemarkung"
#~ msgstr "Gemarkung"
#~ msgid "Loading..."
#~ msgstr "Lade..."
#~ msgid "Who handles the eco-account"
#~ msgstr "Wer für die Herrichtung des Ökokontos verantwortlich ist"

View File

@ -1,63 +1,65 @@
amqp==5.3.1
amqp==5.2.0
asgiref==3.8.1
async-timeout==5.0.1
async-timeout==4.0.3
beautifulsoup4==4.13.0b2
billiard==4.2.1
cached-property==2.0.1
billiard==4.2.0
cached-property==1.5.2
celery==5.4.0
certifi==2024.12.14
cffi==1.17.1
certifi==2024.7.4
cffi==1.17.0
chardet==5.2.0
charset-normalizer==3.4.0
click==8.1.8
charset-normalizer==3.3.2
click==8.1.7
click-didyoumean==0.3.1
click-plugins==1.1.1
click-repl==0.3.0
coverage==7.6.9
cryptography==44.0.0
Deprecated==1.2.15
Django==5.1.4
coverage==7.5.4
cryptography==43.0.0
Deprecated==1.2.14
Django==5.0.8
django-autocomplete-light==3.11.0
django-bootstrap-modal-forms==3.0.5
django-bootstrap4==24.4
django-bootstrap-modal-forms==3.0.4
django-bootstrap4==24.3
django-environ==0.11.2
django-filter==24.3
django-fontawesome-5==1.0.18
django-oauth-toolkit==3.0.1
django-tables2==2.7.1
et_xmlfile==2.0.0
gunicorn==23.0.0
idna==3.10
importlib_metadata==8.5.0
django-oauth-toolkit==2.4.0
django-simple-sso==1.2.0
django-tables2==2.7.0
et-xmlfile==1.1.0
gunicorn==22.0.0
idna==3.7
importlib_metadata==8.2.0
itsdangerous==0.24
jwcrypto==1.5.6
kombu==5.4.0rc1
oauthlib==3.2.2
openpyxl==3.2.0b1
packaging==24.2
packaging==24.1
pika==1.3.2
pillow==11.0.0
prompt_toolkit==3.0.48
psycopg==3.2.3
psycopg-binary==3.2.3
pillow==10.4.0
prompt_toolkit==3.0.47
psycopg==3.2.1
psycopg-binary==3.2.1
pycparser==2.22
pyparsing==3.2.0
pyparsing==3.1.2
pypng==0.20220715.0
pyproj==3.7.0
pyproj==3.6.1
python-dateutil==2.9.0.post0
pytz==2024.2
pytz==2024.1
PyYAML==6.0.2
qrcode==7.3.1
redis==5.1.0b6
requests==2.32.3
requests<2.32.0 # kombu 5.4.0rc1 depends on requests<2.32.0
six==1.16.0
soupsieve==2.5
sqlparse==0.5.1
typing_extensions==4.12.2
tzdata==2024.2
urllib3==2.3.0
tzdata==2024.1
urllib3==2.2.2
vine==5.1.0
wcwidth==0.2.13
webservices==0.7
wrapt==1.16.0
xmltodict==0.14.2
zipp==3.21.0
xmltodict==0.13.0
zipp==3.19.2

View File

@ -17,11 +17,10 @@ class ShareUserAutocomplete(Select2QuerySetView):
"""
def get_queryset(self):
qs = User.objects.none()
if self.request.user.is_anonymous:
return qs
if self.q:
return User.objects.none()
qs = User.objects.all()
if self.q:
# Due to privacy concerns only a full username match will return the proper user entry
qs = qs.filter(
Q(username=self.q) |
@ -42,13 +41,13 @@ class ShareTeamAutocomplete(Select2QuerySetView):
"""
def get_queryset(self):
qs = Team.objects.none()
if self.request.user.is_anonymous:
return qs
if self.q:
return Team.objects.none()
qs = Team.objects.filter(
deleted__isnull=True
)
if self.q:
# Due to privacy concerns only a full username match will return the proper user entry
q_parts = self.q.split(" ")
q = Q()
for part in q_parts:

View File

@ -1,49 +0,0 @@
"""
Author: Michel Peltriaux
Created on: 08.01.25
"""
from django import forms
from django.utils.translation import gettext_lazy as _
from api.models import APIUserToken
from konova.forms.modals import BaseModalForm
from konova.utils.mailer import Mailer
class NewAPITokenModalForm(BaseModalForm):
confirm = forms.BooleanField(
label=_("Confirm"),
label_suffix=_(""),
widget=forms.CheckboxInput(),
required=True,
)
def __init__(self, *args, **kwargs):
self.template = "modal/modal_form.html"
super().__init__(*args, **kwargs)
self.form_title = _("Generate API Token")
self.form_caption = ""
if self.__user_has_api_token():
self.form_caption = _("You are about to create a new API token. The existing one will not be usable afterwards.")
self.form_caption += "\n"
self.form_caption += _("A new token needs to be validated by an administrator!")
# Disable automatic w-100 setting for this type of modal form. Looks kinda strange
self.fields["confirm"].widget.attrs["class"] = ""
def __user_has_api_token(self):
return self.instance.api_token is not None
def save(self):
user = self.instance
if user.api_token is not None:
user.api_token.delete()
user.api_token = APIUserToken.objects.create()
user.save()
mailer = Mailer()
mailer.send_mail_verify_api_token(user)
return user.api_token

View File

@ -66,3 +66,48 @@ class UserNotificationForm(BaseForm):
id__in=selected_notification_ids,
)
self.user.notifications.set(notifications)
class UserAPITokenForm(BaseForm):
token = forms.CharField(
label=_("Token"),
label_suffix="",
max_length=255,
required=True,
help_text=_("Generated automatically - not editable"),
widget=GenerateInput(
attrs={
"class": "form-control",
"url": reverse_lazy("api:generate-new-token"),
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Create new token")
self.form_caption = _("A new token needs to be validated by an administrator!")
self.action_url = reverse("user:api-token")
self.cancel_redirect = reverse("user:index")
# Make direct token editing by user impossible. Instead set the proper url for generating a new token
self.initialize_form_field("token", None)
self.fields["token"].widget.attrs["readonly"] = True
def save(self):
""" Saves the form data
Returns:
api_token (APIUserToken)
"""
user = self.instance
new_token = self.cleaned_data["token"]
if user.api_token is not None:
user.api_token.delete()
new_token = APIUserToken.objects.create(
token=new_token
)
user.api_token = new_token
user.save()
return new_token

View File

@ -8,16 +8,7 @@
<table class="table table-hover">
<tr>
<th scope="row">{% trans 'Current token' %}</th>
<td>
<div class="row">
<div class="col-10">{{ user.api_token.token }}</div>
<div class="col-2">
<button class="btn btn-default btn-modal" data-form-url="{% url 'user:api-token-new' %}" title="{% trans 'Create new token' %}">
{% fa5_icon 'dice' %}
</button>
</div>
</div>
</td>
<td>{{ user.api_token.token }}</td>
</tr>
<tr>
<th scope="row">{% trans 'Authenticated by admins' %}</th>
@ -36,9 +27,7 @@
</table>
</div>
</div>
{% with 'btn-modal' as btn_class %}
{% include 'modal/modal_form_script.html' %}
{% endwith %}
<hr>
{% include 'form/table/generic_table_form.html' %}
{% endblock %}

View File

@ -5,14 +5,15 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 12.09.23
"""
from django.core.exceptions import ObjectDoesNotExist
from django.test import RequestFactory
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from api.models import APIUserToken
from konova.tests.test_views import BaseTestCase
from user.forms.modals.api_token import NewAPITokenModalForm
from user.forms.modals.team import NewTeamModalForm, EditTeamModalForm, RemoveTeamModalForm, LeaveTeamModalForm
from user.forms.user import UserNotificationForm
from user.forms.user import UserNotificationForm, UserAPITokenForm
from user.models import Team, UserAction, UserNotification
@ -251,28 +252,35 @@ class UserNotificationFormTestCase(BaseTestCase):
self.assertIn(selected_notification, self.user.notifications.all())
class ApiTokenFormTestCase(BaseTestCase):
def test_new_token_and_recreating_token(self):
request = RequestFactory().request()
request.user = self.user
request.POST = {
"confirm": True
class UserAPITokenFormTestCase(BaseTestCase):
def test_init(self):
form = UserAPITokenForm(
instance=self.user
)
self.assertEqual(form.form_title, str(_("Create new token")))
self.assertEqual(form.form_caption, str(_("A new token needs to be validated by an administrator!")))
self.assertEqual(form.action_url, reverse("user:api-token"))
self.assertEqual(form.cancel_redirect, reverse("user:index"))
self.assertIsNone(form.fields["token"].initial)
self.assertTrue(form.fields["token"].widget.attrs["readonly"])
def test_save(self):
data = {
"token": APIUserToken().token
}
form = UserAPITokenForm(
data,
instance=self.user
)
self.assertTrue(form.is_valid(), msg=form.errors)
self.assertIsNone(self.user.api_token)
form = NewAPITokenModalForm(request.POST, instance=self.user)
form.save()
self.user.refresh_from_db()
token = self.user.api_token
self.assertFalse(token.is_active)
self.assertIsNone(token.valid_until)
self.assertIsNotNone(token.token)
old_token = token.token
form.save()
self.user.refresh_from_db()
new_token = self.user.api_token
self.assertNotEqual(new_token.token, old_token)
self.assertFalse(new_token.is_active)
self.assertIsNone(new_token.valid_until)
token = form.save()
self.assertEqual(self.user.api_token, token)
new_token = form.save()
self.assertEqual(self.user.api_token, new_token)
try:
token.refresh_from_db()
self.fail("Token should be deleted and not be fetchable anymore")
except ObjectDoesNotExist:
pass

View File

@ -9,7 +9,6 @@ from django.urls import path
from user.autocomplete.share import ShareUserAutocomplete, ShareTeamAutocomplete
from user.autocomplete.team import TeamAdminAutocomplete
from user.views.api_token import APITokenView, new_api_token_view
from user.views.propagate import PropagateUserView
from user.views.views import *
@ -18,8 +17,7 @@ urlpatterns = [
path("", index_view, name="index"),
path("propagate/", PropagateUserView.as_view(), name="propagate"),
path("notifications/", notifications_view, name="notifications"),
path("token/api", APITokenView.as_view(), name="api-token"),
path("token/api/new", new_api_token_view, name="api-token-new"),
path("token/api", api_token_view, name="api-token"),
path("contact/<id>", contact_view, name="contact"),
path("team/", index_team_view, name="team-index"),
path("team/new", new_team_view, name="team-new"),

View File

@ -1,57 +0,0 @@
"""
Author: Michel Peltriaux
Created on: 08.01.25
"""
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest
from django.shortcuts import render
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import View
from django.utils.translation import gettext_lazy as _
from konova.contexts import BaseContext
from konova.decorators import default_group_required
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import NEW_API_TOKEN_GENERATED
from user.forms.modals.api_token import NewAPITokenModalForm
class APITokenView(View):
@method_decorator(login_required)
@method_decorator(default_group_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def get(self, request: HttpRequest):
template = "user/token.html"
user = request.user
context = {
"user": user,
TAB_TITLE_IDENTIFIER: _("User API token"),
}
context = BaseContext(request, context).context
return render(request, template, context)
def new_api_token_view(request: HttpRequest):
""" Function based view for processing ModalForm
(Currently ModalForms only work properly with function based views)
Args:
request ():
Returns:
"""
user = request.user
form = NewAPITokenModalForm(request.POST or None, instance=user, request=request)
return form.process_request(
request=request,
msg_success=NEW_API_TOKEN_GENERATED,
redirect_url=reverse("user:api-token"),
)

View File

@ -16,7 +16,7 @@ from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from konova.sub_settings.sso_settings import PROPAGATION_SECRET
from konova.sub_settings.sso_settings import OAUTH_CLIENT_ID
from user.models import User
@ -35,9 +35,9 @@ class PropagateUserView(View):
def post(self, request: HttpRequest, *args, **kwargs):
# Decrypt
encrypted_body = request.body
_hash = hashlib.md5()
_hash.update(PROPAGATION_SECRET.encode("utf-8"))
key = base64.urlsafe_b64encode(_hash.hexdigest().encode("utf-8"))
hash = hashlib.md5()
hash.update(OAUTH_CLIENT_ID.encode("utf-8"))
key = base64.urlsafe_b64encode(hash.hexdigest().encode("utf-8"))
fernet = Fernet(key)
body = fernet.decrypt(encrypted_body).decode("utf-8")
body = json.loads(body)

View File

@ -3,17 +3,19 @@ from django.contrib.auth.decorators import login_required
from django.urls import reverse
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.mailer import Mailer
from konova.utils.message_templates import FORM_INVALID
from user.forms.modals.team import NewTeamModalForm, EditTeamModalForm, RemoveTeamModalForm, LeaveTeamModalForm
from user.forms.modals.user import UserContactForm
from user.forms.team import TeamDataForm
from user.forms.user import UserNotificationForm
from user.forms.user import UserNotificationForm, UserAPITokenForm
from user.models import User, Team
from django.http import HttpRequest, Http404
from django.shortcuts import render, redirect, get_object_or_404
from django.utils.translation import gettext_lazy as _
from konova.contexts import BaseContext
from konova.decorators import any_group_check, login_required_modal
from konova.decorators import any_group_check, default_group_required, login_required_modal
@login_required
@ -74,6 +76,40 @@ def notifications_view(request: HttpRequest):
return render(request, template, context)
@login_required
@default_group_required
def api_token_view(request: HttpRequest):
""" Handles the request for user api frontend settings
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "user/token.html"
user = request.user
form = UserAPITokenForm(request.POST or None, instance=user)
if request.method == "POST":
if form.is_valid():
token = form.save()
messages.info(request, _("New token generated. Administrators need to validate."))
mailer = Mailer()
mailer.send_mail_verify_api_token(user)
return redirect("user:api-token")
else:
messages.error(request, FORM_INVALID, extra_tags="danger")
elif request.method != "GET":
raise NotImplementedError
context = {
"user": user,
"form": form,
TAB_TITLE_IDENTIFIER: _("User API token"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required_modal
@login_required
def contact_view(request: HttpRequest, id: str):