Compare commits

..

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

21 changed files with 534 additions and 238 deletions

View File

@ -51,7 +51,7 @@ class APIUserToken(models.Model):
if token_obj.valid_until is not None and token_obj.valid_until < _today: if token_obj.valid_until is not None and token_obj.valid_until < _today:
raise PermissionError("Token validity expired") raise PermissionError("Token validity expired")
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise PermissionError("Token unknown") raise PermissionError("Credentials invalid")
return token_obj.user return token_obj.user

View File

@ -50,19 +50,14 @@ class AbstractAPIView(View):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
try: try:
# Fetch the proper user from the given request header token # 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) 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: if ksp_user != token_user.username:
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:
raise PermissionError(f"Invalid token for {ksp_user}") raise PermissionError(f"Invalid token for {ksp_user}")
self.user = token_user else:
self.user = token_user
request.user = self.user request.user = self.user
if not self.user.is_default_user(): if not self.user.is_default_user():

View File

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

View File

@ -34,9 +34,7 @@ class Command(BaseKonovaCommand):
def recalculate_parcels(self, options: dict): def recalculate_parcels(self, options: dict):
force_all = options.get("force_all", False) force_all = options.get("force_all", False)
geometry_objects = Geometry.objects.filter( geometry_objects = Geometry.objects.all().exclude(
geom__isempty=False,
).exclude(
geom=None geom=None
) )

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

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

View File

@ -91,6 +91,3 @@ INTERVENTION_HAS_REVOCATIONS_TEMPLATE = _("This intervention has {} revocations"
DATA_CHECKED_ON_TEMPLATE = _("Checked on {} by {}") DATA_CHECKED_ON_TEMPLATE = _("Checked on {} by {}")
DATA_CHECKED_PREVIOUSLY_TEMPLATE = _("Data has changed since last check on {} by {}") DATA_CHECKED_PREVIOUSLY_TEMPLATE = _("Data has changed since last check on {} by {}")
DATA_IS_UNCHECKED = _("Current data not checked yet") 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

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

View File

@ -25,12 +25,9 @@ class LogoutView(View):
A redirect A redirect
""" """
user = request.user user = request.user
try: oauth_token = user.oauth_token
oauth_token = user.oauth_token if oauth_token:
if oauth_token: oauth_token.revoke()
oauth_token.revoke()
except AttributeError:
pass
logout(request) logout(request)
return redirect(SSO_SERVER_BASE) return redirect(SSO_SERVER_BASE)

Binary file not shown.

View File

@ -38,14 +38,13 @@
#: konova/forms/modals/remove_form.py:23 #: konova/forms/modals/remove_form.py:23
#: konova/forms/modals/resubmission_form.py:22 #: konova/forms/modals/resubmission_form.py:22
#: konova/forms/modals/resubmission_form.py:38 konova/forms/remove_form.py:25 #: 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 #: konova/tests/unit/test_forms.py:59 user/forms/user.py:39
#: user/forms/user.py:39
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -374,6 +373,7 @@ msgid "Identifier"
msgstr "Kennung" msgstr "Kennung"
#: compensation/forms/compensation.py:33 intervention/forms/intervention.py:33 #: compensation/forms/compensation.py:33 intervention/forms/intervention.py:33
#: user/forms/user.py:77
msgid "Generated automatically - not editable" msgid "Generated automatically - not editable"
msgstr "Automatisch generiert - nicht bearbeitbar" msgstr "Automatisch generiert - nicht bearbeitbar"
@ -1795,7 +1795,8 @@ msgstr "Bearbeitender Nutzer"
msgid "" msgid ""
"Search for entries where this person has been participated according to log " "Search for entries where this person has been participated according to log "
"history" "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 #: konova/forms/base_form.py:23 templates/form/collapsable/form.html:62
msgid "Save" msgid "Save"
@ -1857,7 +1858,6 @@ msgstr ""
"Ich, {} {}, bestätige, dass diese Daten wieder entzeichnet werden müssen." "Ich, {} {}, bestätige, dass diese Daten wieder entzeichnet werden müssen."
#: konova/forms/modals/remove_form.py:22 konova/forms/remove_form.py:24 #: konova/forms/modals/remove_form.py:22 konova/forms/remove_form.py:24
#: user/forms/modals/api_token.py:16
msgid "Confirm" msgid "Confirm"
msgstr "Bestätige" msgstr "Bestätige"
@ -2283,10 +2283,6 @@ msgstr ""
msgid "Current data not checked yet" msgid "Current data not checked yet"
msgstr "Momentane Daten noch nicht geprüft" 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 #: konova/utils/messenger.py:70
msgid "{} checked" msgid "{} checked"
msgstr "{} geprüft" msgstr "{} geprüft"
@ -2325,7 +2321,7 @@ msgstr "Home"
msgid "Log" msgid "Log"
msgstr "Log" msgstr "Log"
#: konova/views/map_proxy.py:84 #: konova/views/map_proxy.py:70
msgid "" msgid ""
"The external service is currently unavailable.<br>Please try again in a few " "The external service is currently unavailable.<br>Please try again in a few "
"moments..." "moments..."
@ -2835,8 +2831,10 @@ msgid "Reports"
msgstr "Berichte" msgstr "Berichte"
#: templates/navbars/navbar.html:57 #: templates/navbars/navbar.html:57
#, fuzzy
#| msgid "Admins"
msgid "Admin" msgid "Admin"
msgstr "" msgstr "Administratoren"
#: templates/navbars/navbar.html:59 user/templates/user/index.html:31 #: templates/navbars/navbar.html:59 user/templates/user/index.html:31
msgid "Settings" msgid "Settings"
@ -2875,21 +2873,6 @@ msgstr ""
"Falls die Geometrie nicht leer ist, werden die Flurstücke aktuell berechnet. " "Falls die Geometrie nicht leer ist, werden die Flurstücke aktuell berechnet. "
"Bitte laden Sie diese Seite in ein paar Augenblicken erneut..." "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/modals/team.py:20 user/forms/modals/team.py:24
#: user/forms/team.py:17 user/forms/team.py:22 #: user/forms/team.py:17 user/forms/team.py:22
msgid "Team name" msgid "Team name"
@ -2912,11 +2895,11 @@ msgstr ""
"Mehrfachauswahl möglich - Sie können nur Nutzer wählen, die noch nicht " "Mehrfachauswahl möglich - Sie können nur Nutzer wählen, die noch nicht "
"Mitglieder dieses Teams sind. Geben Sie den ganzen Nutzernamen an." "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" msgid "Create new team"
msgstr "Neues Team anlegen" 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 "" msgid ""
"You will become the administrator for this group by default. You do not need " "You will become the administrator for this group by default. You do not need "
"to add yourself to the list of members." "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." 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/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" msgid "Edit team"
msgstr "Team bearbeiten" 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 "" msgid ""
"ATTENTION!\n" "ATTENTION!\n"
"\n" "\n"
@ -2966,7 +2949,7 @@ msgstr ""
"Sind Sie sicher, dass Sie dieses Team löschen möchten?" "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/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" msgid "Leave team"
msgstr "Team verlassen" msgstr "Team verlassen"
@ -2998,10 +2981,22 @@ msgstr "Benachrichtigungen"
msgid "Select the situations when you want to receive a notification" msgid "Select the situations when you want to receive a notification"
msgstr "Wann wollen Sie per E-Mail benachrichtigt werden?" 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" msgid "Edit notifications"
msgstr "Benachrichtigungen bearbeiten" 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 #: user/models/user_action.py:23
msgid "Unrecorded" msgid "Unrecorded"
msgstr "Entzeichnet" msgstr "Entzeichnet"
@ -3056,7 +3051,7 @@ msgid "Manage teams"
msgstr "" msgstr ""
#: user/templates/user/index.html:53 user/templates/user/team/index.html:19 #: 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" msgid "Teams"
msgstr "" msgstr ""
@ -3092,58 +3087,270 @@ msgstr "API Einstellungen"
msgid "Current token" msgid "Current token"
msgstr "Aktueller Token" msgstr "Aktueller Token"
#: user/templates/user/token.html:15 #: user/templates/user/token.html:14
msgid "Create new token"
msgstr "Neuen Token generieren"
#: user/templates/user/token.html:23
msgid "Authenticated by admins" msgid "Authenticated by admins"
msgstr "Von Admin freigeschaltet" 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" msgid "Token has been verified and can be used"
msgstr "Token wurde freigeschaltet und kann verwendet werden" 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" msgid "Token waiting for verification"
msgstr "Token noch nicht freigeschaltet" msgstr "Token noch nicht freigeschaltet"
#: user/templates/user/token.html:33 #: user/templates/user/token.html:24
msgid "Valid until" msgid "Valid until"
msgstr "Läuft ab am" msgstr "Läuft ab am"
#: user/views/api_token.py:33 #: user/views/views.py:35
msgid "User API token"
msgstr "API Nutzer Token"
#: user/views/views.py:33
msgid "User settings" msgid "User settings"
msgstr "Einstellungen" msgstr "Einstellungen"
#: user/views/views.py:59 #: user/views/views.py:61
msgid "Notifications edited" msgid "Notifications edited"
msgstr "Benachrichtigungen bearbeitet" msgstr "Benachrichtigungen bearbeitet"
#: user/views/views.py:71 #: user/views/views.py:73
msgid "User notifications" msgid "User notifications"
msgstr "Benachrichtigungen" 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" msgid "New team added"
msgstr "Neues Team hinzugefügt" msgstr "Neues Team hinzugefügt"
#: user/views/views.py:162 #: user/views/views.py:198
msgid "Team edited" msgid "Team edited"
msgstr "Team bearbeitet" msgstr "Team bearbeitet"
#: user/views/views.py:177 #: user/views/views.py:213
msgid "Team removed" msgid "Team removed"
msgstr "Team gelöscht" msgstr "Team gelöscht"
#: user/views/views.py:192 #: user/views/views.py:228
msgid "You are not a member of this team" msgid "You are not a member of this team"
msgstr "Sie sind kein Mitglied dieses Teams" msgstr "Sie sind kein Mitglied dieses Teams"
#: user/views/views.py:199 #: user/views/views.py:235
msgid "Left Team" msgid "Left Team"
msgstr "Team verlassen" 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

@ -24,11 +24,13 @@ django-environ==0.11.2
django-filter==24.3 django-filter==24.3
django-fontawesome-5==1.0.18 django-fontawesome-5==1.0.18
django-oauth-toolkit==3.0.1 django-oauth-toolkit==3.0.1
django-simple-sso==1.2.0
django-tables2==2.7.1 django-tables2==2.7.1
et_xmlfile==2.0.0 et_xmlfile==2.0.0
gunicorn==23.0.0 gunicorn==23.0.0
idna==3.10 idna==3.10
importlib_metadata==8.5.0 importlib_metadata==8.5.0
itsdangerous==0.24
jwcrypto==1.5.6 jwcrypto==1.5.6
kombu==5.4.0rc1 kombu==5.4.0rc1
oauthlib==3.2.2 oauthlib==3.2.2

View File

@ -17,11 +17,10 @@ class ShareUserAutocomplete(Select2QuerySetView):
""" """
def get_queryset(self): def get_queryset(self):
qs = User.objects.none()
if self.request.user.is_anonymous: if self.request.user.is_anonymous:
return qs return User.objects.none()
qs = User.objects.all()
if self.q: if self.q:
qs = User.objects.all()
# Due to privacy concerns only a full username match will return the proper user entry # Due to privacy concerns only a full username match will return the proper user entry
qs = qs.filter( qs = qs.filter(
Q(username=self.q) | Q(username=self.q) |
@ -42,13 +41,13 @@ class ShareTeamAutocomplete(Select2QuerySetView):
""" """
def get_queryset(self): def get_queryset(self):
qs = Team.objects.none()
if self.request.user.is_anonymous: if self.request.user.is_anonymous:
return qs return Team.objects.none()
qs = Team.objects.filter(
deleted__isnull=True
)
if self.q: if self.q:
qs = Team.objects.filter( # Due to privacy concerns only a full username match will return the proper user entry
deleted__isnull=True
)
q_parts = self.q.split(" ") q_parts = self.q.split(" ")
q = Q() q = Q()
for part in q_parts: 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, id__in=selected_notification_ids,
) )
self.user.notifications.set(notifications) 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"> <table class="table table-hover">
<tr> <tr>
<th scope="row">{% trans 'Current token' %}</th> <th scope="row">{% trans 'Current token' %}</th>
<td> <td>{{ user.api_token.token }}</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>
</tr> </tr>
<tr> <tr>
<th scope="row">{% trans 'Authenticated by admins' %}</th> <th scope="row">{% trans 'Authenticated by admins' %}</th>
@ -36,9 +27,7 @@
</table> </table>
</div> </div>
</div> </div>
<hr>
{% with 'btn-modal' as btn_class %} {% include 'form/table/generic_table_form.html' %}
{% include 'modal/modal_form_script.html' %}
{% endwith %}
{% endblock %} {% endblock %}

View File

@ -5,14 +5,15 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 12.09.23 Created on: 12.09.23
""" """
from django.core.exceptions import ObjectDoesNotExist
from django.test import RequestFactory from django.test import RequestFactory
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from api.models import APIUserToken
from konova.tests.test_views import BaseTestCase 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.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 from user.models import Team, UserAction, UserNotification
@ -251,28 +252,35 @@ class UserNotificationFormTestCase(BaseTestCase):
self.assertIn(selected_notification, self.user.notifications.all()) self.assertIn(selected_notification, self.user.notifications.all())
class ApiTokenFormTestCase(BaseTestCase): class UserAPITokenFormTestCase(BaseTestCase):
def test_new_token_and_recreating_token(self): def test_init(self):
request = RequestFactory().request() form = UserAPITokenForm(
request.user = self.user instance=self.user
request.POST = { )
"confirm": True 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) self.assertIsNone(self.user.api_token)
form = NewAPITokenModalForm(request.POST, instance=self.user) token = form.save()
form.save() self.assertEqual(self.user.api_token, token)
self.user.refresh_from_db() new_token = form.save()
token = self.user.api_token self.assertEqual(self.user.api_token, new_token)
self.assertFalse(token.is_active) try:
self.assertIsNone(token.valid_until) token.refresh_from_db()
self.assertIsNotNone(token.token) self.fail("Token should be deleted and not be fetchable anymore")
except ObjectDoesNotExist:
old_token = token.token pass
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)

View File

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

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

@ -3,17 +3,19 @@ from django.contrib.auth.decorators import login_required
from django.urls import reverse from django.urls import reverse
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER 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.team import NewTeamModalForm, EditTeamModalForm, RemoveTeamModalForm, LeaveTeamModalForm
from user.forms.modals.user import UserContactForm from user.forms.modals.user import UserContactForm
from user.forms.team import TeamDataForm 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 user.models import User, Team
from django.http import HttpRequest, Http404 from django.http import HttpRequest, Http404
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from konova.contexts import BaseContext 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 @login_required
@ -74,6 +76,40 @@ def notifications_view(request: HttpRequest):
return render(request, template, context) 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_modal
@login_required @login_required
def contact_view(request: HttpRequest, id: str): def contact_view(request: HttpRequest, id: str):