diff --git a/api/tests/v1/update/test_api_update.py b/api/tests/v1/update/test_api_update.py index ffaa6870..faf6eadb 100644 --- a/api/tests/v1/update/test_api_update.py +++ b/api/tests/v1/update/test_api_update.py @@ -12,7 +12,7 @@ from django.contrib.gis import geos from django.urls import reverse from api.tests.v1.share.test_api_sharing import BaseAPIV1TestCase -from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP +from konova.models import Geometry class APIV1UpdateTestCase(BaseAPIV1TestCase): @@ -64,7 +64,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase): put_props = put_body["properties"] put_geom = geos.fromstr(json.dumps(put_body)) - put_geom.transform(DEFAULT_SRID_RLP) + put_geom = Geometry.cast_to_rlp_srid(put_geom) self.assertEqual(put_geom, self.intervention.geometry.geom) self.assertEqual(put_props["title"], self.intervention.title) self.assertNotEqual(modified_on, self.intervention.modified) @@ -94,7 +94,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase): put_props = put_body["properties"] put_geom = geos.fromstr(json.dumps(put_body)) - put_geom.transform(DEFAULT_SRID_RLP) + put_geom = Geometry.cast_to_rlp_srid(put_geom) self.assertEqual(put_geom, self.compensation.geometry.geom) self.assertEqual(put_props["title"], self.compensation.title) self.assertNotEqual(modified_on, self.compensation.modified) @@ -124,7 +124,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase): put_props = put_body["properties"] put_geom = geos.fromstr(json.dumps(put_body)) - put_geom.transform(DEFAULT_SRID_RLP) + put_geom = Geometry.cast_to_rlp_srid(put_geom) self.assertEqual(put_geom, self.eco_account.geometry.geom) self.assertEqual(put_props["title"], self.eco_account.title) self.assertNotEqual(modified_on, self.eco_account.modified) @@ -156,7 +156,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase): put_props = put_body["properties"] put_geom = geos.fromstr(json.dumps(put_body)) - put_geom.transform(DEFAULT_SRID_RLP) + put_geom = Geometry.cast_to_rlp_srid(put_geom) self.assertEqual(put_geom, self.ema.geometry.geom) self.assertEqual(put_props["title"], self.ema.title) self.assertNotEqual(modified_on, self.ema.modified) diff --git a/api/utils/serializer/serializer.py b/api/utils/serializer/serializer.py index c3096cc1..f4a35bdc 100644 --- a/api/utils/serializer/serializer.py +++ b/api/utils/serializer/serializer.py @@ -13,7 +13,7 @@ from django.contrib.gis.geos import GEOSGeometry from django.core.paginator import Paginator from django.db.models import Q -from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP +from konova.models import Geometry from konova.utils.message_templates import DATA_UNSHARED @@ -145,8 +145,8 @@ class AbstractModelAPISerializer: if isinstance(geojson, dict): geojson = json.dumps(geojson) geometry = geos.fromstr(geojson) - if geometry.srid != DEFAULT_SRID_RLP: - geometry.transform(DEFAULT_SRID_RLP) + geometry = Geometry.cast_to_rlp_srid(geometry) + geometry = Geometry.cast_to_multipolygon(geometry) return geometry def _get_obj_from_db(self, id, user): diff --git a/compensation/tests/ecoaccount/test_workflow.py b/compensation/tests/ecoaccount/test_workflow.py index 85f7db54..27eb0a99 100644 --- a/compensation/tests/ecoaccount/test_workflow.py +++ b/compensation/tests/ecoaccount/test_workflow.py @@ -86,7 +86,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase): new_title = self.create_dummy_string() new_identifier = self.create_dummy_string() new_comment = self.create_dummy_string() - new_geometry = MultiPolygon(srid=4326) # Create an empty geometry + new_geometry = self.create_dummy_geometry() test_conservation_office = self.get_conservation_office_code() test_deductable_surface = self.eco_account.deductable_surface + 100 @@ -103,7 +103,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase): "identifier": new_identifier, "title": new_title, "comment": new_comment, - "geom": new_geometry.geojson, + "geom": self.create_geojson(new_geometry), "surface": test_deductable_surface, "conservation_office": test_conservation_office.id } diff --git a/compensation/views/compensation/report.py b/compensation/views/compensation/report.py index 3176c153..96081627 100644 --- a/compensation/views/compensation/report.py +++ b/compensation/views/compensation/report.py @@ -12,11 +12,12 @@ from django.utils.translation import gettext_lazy as _ from compensation.models import Compensation from konova.contexts import BaseContext +from konova.decorators import uuid_required from konova.forms import SimpleGeomForm from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.generators import generate_qr_code - +@uuid_required def report_view(request: HttpRequest, id: str): """ Renders the public report view diff --git a/compensation/views/eco_account/report.py b/compensation/views/eco_account/report.py index ba8212bb..f61a7bfc 100644 --- a/compensation/views/eco_account/report.py +++ b/compensation/views/eco_account/report.py @@ -12,11 +12,13 @@ from django.utils.translation import gettext_lazy as _ from compensation.models import EcoAccount from konova.contexts import BaseContext +from konova.decorators import uuid_required from konova.forms import SimpleGeomForm from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.generators import generate_qr_code +@uuid_required def report_view(request: HttpRequest, id: str): """ Renders the public report view diff --git a/ema/tests/test_workflow.py b/ema/tests/test_workflow.py index c6228112..7fb3c6a3 100644 --- a/ema/tests/test_workflow.py +++ b/ema/tests/test_workflow.py @@ -84,7 +84,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase): new_title = self.create_dummy_string() new_identifier = self.create_dummy_string() new_comment = self.create_dummy_string() - new_geometry = MultiPolygon(srid=4326) # Create an empty geometry + new_geometry = self.create_dummy_geometry() # Create an empty geometry test_conservation_office = self.get_conservation_office_code() check_on_elements = { @@ -99,7 +99,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase): "identifier": new_identifier, "title": new_title, "comment": new_comment, - "geom": new_geometry.geojson, + "geom": self.create_geojson(new_geometry), "conservation_office": test_conservation_office.id } self.client_user.post(url, post_data) diff --git a/ema/views/report.py b/ema/views/report.py index 1da1ba6e..93af6211 100644 --- a/ema/views/report.py +++ b/ema/views/report.py @@ -12,11 +12,12 @@ from django.utils.translation import gettext_lazy as _ from ema.models import Ema from konova.contexts import BaseContext +from konova.decorators import uuid_required from konova.forms import SimpleGeomForm from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.generators import generate_qr_code - +@uuid_required def report_view(request:HttpRequest, id: str): """ Renders the public report view diff --git a/intervention/tests/unit/test_forms.py b/intervention/tests/unit/test_forms.py index 1c4306c2..6435e21e 100644 --- a/intervention/tests/unit/test_forms.py +++ b/intervention/tests/unit/test_forms.py @@ -124,7 +124,7 @@ class EditInterventionFormTestCase(NewInterventionFormTestCase): self.assertIsNotNone(obj.responsible.handler) self.assertEqual(obj.title, data["title"]) self.assertEqual(obj.comment, data["comment"]) - self.assertTrue(test_geom.equals_exact(obj.geometry.geom, 0.000001)) + self.assert_equal_geometries(test_geom, obj.geometry.geom) self.assertEqual(obj.legal.binding_date, today) self.assertEqual(obj.legal.registration_date, today) diff --git a/konova/decorators.py b/konova/decorators.py index e10328df..6c16939a 100644 --- a/konova/decorators.py +++ b/konova/decorators.py @@ -11,7 +11,7 @@ from uuid import UUID from bootstrap_modal_forms.mixins import is_ajax from django.contrib import messages -from django.http import Http404 +from django.core.exceptions import BadRequest from django.shortcuts import redirect, get_object_or_404, render from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -185,7 +185,7 @@ def uuid_required(function): try: uuid = UUID(uuid) except ValueError: - raise Http404( + raise BadRequest( "Invalid UUID" ) return function(request, *args, **kwargs) diff --git a/konova/forms/geometry_form.py b/konova/forms/geometry_form.py index 3f885fae..1c21546b 100644 --- a/konova/forms/geometry_form.py +++ b/konova/forms/geometry_form.py @@ -27,7 +27,7 @@ class SimpleGeomForm(BaseForm): """ read_only = True geometry_simplified = False - output = JSONField( + geom = JSONField( label=_("Geometry"), help_text=_(""), label_suffix="", @@ -55,27 +55,26 @@ class SimpleGeomForm(BaseForm): geom = "" self.empty = True - self.initialize_form_field("output", geom) + self.initialize_form_field("geom", geom) def is_valid(self): super().is_valid() is_valid = True # Get geojson from form - geom = self.data["output"] + geom = self.data["geom"] if geom is None or len(geom) == 0: # empty geometry is a valid geometry - self.cleaned_data["output"] = MultiPolygon(srid=DEFAULT_SRID_RLP).ewkt + self.cleaned_data["geom"] = MultiPolygon(srid=DEFAULT_SRID_RLP).ewkt return is_valid geom = json.loads(geom) # Write submitted data back into form field to make sure invalid geometry # will be rendered again on failed submit - self.initialize_form_field("output", self.data["output"]) + self.initialize_form_field("geom", self.data["geom"]) - # Read geojson into gdal geometry - # HINT: This can be simplified if the geojson format holds data in epsg:4326 (GDAL provides direct creation for - # this case) + # Initialize features list with empty MultiPolygon, so that an empty input will result in a + # proper empty MultiPolygon object features = [] features_json = geom.get("features", []) accepted_ogr_types = [ @@ -98,33 +97,35 @@ class SimpleGeomForm(BaseForm): g = self.__flatten_geom_to_2D(g) if g.geom_type not in accepted_ogr_types: - self.add_error("output", _("Only surfaces allowed. Points or lines must be buffered.")) + self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered.")) is_valid &= False return is_valid is_valid &= self.__is_area_valid(g) - - polygon = Polygon.from_ewkt(g.ewkt) - is_valid &= polygon.valid - if not polygon.valid: - self.add_error("output", polygon.valid_reason) + g = Polygon.from_ewkt(g.ewkt) + is_valid &= g.valid + if not g.valid: + self.add_error("geom", g.valid_reason) return is_valid - features.append(polygon) + if isinstance(g, Polygon): + features.append(g) + elif isinstance(g, MultiPolygon): + features.extend(list(g)) # Unionize all geometry features into one new MultiPolygon - form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP) - for feature in features: - form_geom = form_geom.union(feature) + if features: + form_geom = MultiPolygon(*features, srid=DEFAULT_SRID_RLP).unary_union + else: + form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP) # Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided. - if form_geom.geom_type != "MultiPolygon": - form_geom = MultiPolygon(form_geom, srid=DEFAULT_SRID_RLP) + form_geom = Geometry.cast_to_multipolygon(form_geom) # Write unioned Multipolygon into cleaned data if self.cleaned_data is None: self.cleaned_data = {} - self.cleaned_data["output"] = form_geom.ewkt + self.cleaned_data["geom"] = form_geom.ewkt return is_valid @@ -134,7 +135,7 @@ class SimpleGeomForm(BaseForm): Returns: """ - geom = self.cleaned_data.get("output") + geom = self.cleaned_data.get("geom") g = gdal.OGRGeometry(geom, srs=DEFAULT_SRID_RLP) num_vertices = g.num_coords @@ -150,7 +151,7 @@ class SimpleGeomForm(BaseForm): if not is_area_valid: self.add_error( - "output", + "geom", _("Geometry must be greater than 1m². Currently is {}m²").format( float(geom.area) ) @@ -193,14 +194,14 @@ class SimpleGeomForm(BaseForm): if self.instance is None or self.instance.geometry is None: raise LookupError geometry = self.instance.geometry - geometry.geom = self.cleaned_data.get("output", MultiPolygon(srid=DEFAULT_SRID_RLP)) + geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)) geometry.modified = action geometry.save() except LookupError: # No geometry or linked instance holding a geometry exist --> create a new one! geometry = Geometry.objects.create( - geom=self.cleaned_data.get("output", MultiPolygon(srid=DEFAULT_SRID_RLP)), + geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)), created=action, ) diff --git a/konova/models/geometry.py b/konova/models/geometry.py index 2db60e4d..82a51dbc 100644 --- a/konova/models/geometry.py +++ b/konova/models/geometry.py @@ -11,6 +11,7 @@ from django.contrib.gis.db.models import MultiPolygonField from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.db import models, transaction from django.utils import timezone +from django.contrib.gis.geos import MultiPolygon from konova.models import BaseResource, UuidModel from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP @@ -384,6 +385,36 @@ class Geometry(BaseResource): return complexity_factor + @staticmethod + def cast_to_multipolygon(input_geom): + """ If input_geom is not a MultiPolygon, cast to MultiPolygon + + Args: + input_geom (): + + Returns: + output_geom + """ + output_geom = input_geom + if not isinstance(input_geom, MultiPolygon): + output_geom = MultiPolygon(input_geom, srid=DEFAULT_SRID_RLP) + return output_geom + + @staticmethod + def cast_to_rlp_srid(input_geom): + """ If input_geom is not of RLP SRID (25832), cast to RLP SRID + + Args: + input_geom (): + + Returns: + output_geom + """ + output_geom = input_geom + if output_geom.srid != DEFAULT_SRID_RLP: + output_geom.transform(DEFAULT_SRID_RLP) + return output_geom + class GeometryConflict(UuidModel): """ diff --git a/konova/static/images/error_imgs/croc_technician_400.png b/konova/static/images/error_imgs/croc_technician_400.png new file mode 100644 index 00000000..4ce5b449 Binary files /dev/null and b/konova/static/images/error_imgs/croc_technician_400.png differ diff --git a/konova/static/images/error_imgs/croc_technician_500.png b/konova/static/images/error_imgs/croc_technician_500.png new file mode 100644 index 00000000..35e29ca7 Binary files /dev/null and b/konova/static/images/error_imgs/croc_technician_500.png differ diff --git a/konova/tests/test_views.py b/konova/tests/test_views.py index 95fb8367..4977bfee 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -469,7 +469,7 @@ class BaseTestCase(TestCase): eco_account.save() return eco_account - def assert_equal_geometries(self, geom1: MultiPolygon, geom2: MultiPolygon, tolerance = 0.001): + def assert_equal_geometries(self, geom1: MultiPolygon, geom2: MultiPolygon, tolerance=0.001): """ Assert for geometries to be equal Transforms the geometries to matching srids before checking @@ -491,7 +491,10 @@ class BaseTestCase(TestCase): # transformation from one coordinate system into the other, which is valid geom1.transform(geom2.srid) geom2.transform(geom1.srid) - self.assertTrue(geom1.equals_exact(geom2, tolerance) or geom2.equals_exact(geom1, tolerance)) + self.assertTrue( + geom1.equals_exact(geom2, tolerance=tolerance), + msg=f"Difference is {abs(geom1.area - geom2.area)} with {geom1.area} and {geom2.area} in a tolerance of {tolerance}" + ) class BaseViewTestCase(BaseTestCase): diff --git a/konova/urls.py b/konova/urls.py index 8dc9a01f..c82c06ac 100644 --- a/konova/urls.py +++ b/konova/urls.py @@ -42,5 +42,6 @@ urlpatterns = [ path('client/proxy/wfs', ClientProxyParcelWFS.as_view(), name="client-proxy-wfs"), ] +handler400 = "konova.views.error.get_400_view" handler404 = "konova.views.error.get_404_view" handler500 = "konova.views.error.get_500_view" diff --git a/konova/views/error.py b/konova/views/error.py index 6a85948a..4bc7e6f8 100644 --- a/konova/views/error.py +++ b/konova/views/error.py @@ -25,6 +25,20 @@ def get_404_view(request: HttpRequest, exception=None): return render(request, "404.html", context, status=404) +def get_400_view(request: HttpRequest, exception=None): + """ Returns a 400 handling view + + Args: + request (): + exception (): + + Returns: + + """ + context = BaseContext.context + return render(request, "400.html", context, status=400) + + def get_500_view(request: HttpRequest): """ Returns a 404 handling view diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index 2a3ee10f..b56f73a1 100644 Binary files a/locale/de/LC_MESSAGES/django.mo and b/locale/de/LC_MESSAGES/django.mo differ diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index eb35247c..ee08ee81 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -45,7 +45,7 @@ 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: 2025-05-12 14:22+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -1303,8 +1303,8 @@ msgstr "Kompensation {} bearbeitet" msgid "Edit {}" msgstr "Bearbeite {}" -#: compensation/views/compensation/report.py:34 -#: compensation/views/eco_account/report.py:34 ema/views/report.py:34 +#: compensation/views/compensation/report.py:35 +#: compensation/views/eco_account/report.py:35 ema/views/report.py:35 #: intervention/views/report.py:35 msgid "Report {}" msgstr "Bericht {}" @@ -1928,11 +1928,11 @@ msgstr "Kontrolle am" msgid "Other" msgstr "Sonstige" -#: konova/sub_settings/django_settings.py:157 +#: konova/sub_settings/django_settings.py:156 msgid "German" msgstr "" -#: konova/sub_settings/django_settings.py:158 +#: konova/sub_settings/django_settings.py:157 msgid "English" msgstr "" @@ -2287,18 +2287,6 @@ msgstr "Momentane Daten noch nicht geprüft" 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" - -#: konova/utils/messenger.py:72 -msgid "Check it out" -msgstr "Schauen Sie rein" - -#: konova/utils/messenger.py:73 -msgid "{} has been checked successfully by user {}! {}" -msgstr "{} wurde erfolgreich vom Nutzer {} geprüft! {}" - #: konova/utils/quality.py:32 msgid "missing" msgstr "fehlend" @@ -2389,6 +2377,18 @@ msgstr "Alle" msgid "News" msgstr "Neuigkeiten" +#: templates/400.html:7 +msgid "Request was invalid" +msgstr "Anfrage fehlerhaft" + +#: templates/400.html:10 +msgid "There seems to be a problem with the link you opened." +msgstr "Es scheint ein Problem mit dem Link zu geben, den Sie geöffnet haben." + +#: templates/400.html:11 +msgid "Make sure the URL is valid (no whitespaces, properly copied, ...)." +msgstr "Stellen Sie sicher, dass die URL gültig ist (keine Leerzeichen, fehlerfrei kopiert, ...)." + #: templates/404.html:7 msgid "Not found" msgstr "Nicht gefunden" @@ -2884,7 +2884,8 @@ 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." +"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!" @@ -2912,11 +2913,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:30 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:31 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 +2946,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:87 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:164 msgid "" "ATTENTION!\n" "\n" @@ -2966,7 +2967,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:197 msgid "Leave team" msgstr "Team verlassen" @@ -2998,7 +2999,7 @@ 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:233 msgid "Edit notifications" msgstr "Benachrichtigungen bearbeiten" @@ -3112,7 +3113,7 @@ msgstr "Token noch nicht freigeschaltet" msgid "Valid until" msgstr "Läuft ab am" -#: user/views/api_token.py:33 +#: user/views/api_token.py:34 msgid "User API token" msgstr "API Nutzer Token" diff --git a/templates/400.html b/templates/400.html new file mode 100644 index 00000000..58e29443 --- /dev/null +++ b/templates/400.html @@ -0,0 +1,21 @@ +{% extends 'public_base.html' %} +{% load i18n fontawesome_5 static %} + +{% block body %} +
+
+
+ +
+
+

{% fa5_icon 'question-circle' %}400

+

{% trans 'Request was invalid' %}

+
+
+
+

+ {% trans 'There seems to be a problem with the link you opened.' %} + {% trans 'Make sure the URL is valid (no whitespaces, properly copied, ...).' %} +

+
+{% endblock %} \ No newline at end of file diff --git a/templates/500.html b/templates/500.html index f3839716..b53b072f 100644 --- a/templates/500.html +++ b/templates/500.html @@ -1,10 +1,17 @@ {% extends 'public_base.html' %} -{% load i18n fontawesome_5 %} +{% load i18n fontawesome_5 static %} {% block body %}
-

{% fa5_icon 'fire-extinguisher' %} {% fa5_icon 'fire-alt' %} 500

-

{% trans 'Server Error' %}

+
+
+ +
+
+

{% fa5_icon 'fire-alt' %} 500

+

{% trans 'Server Error' %}

+
+

{% trans 'Something happened. Admins have been informed. We are working on it!' %} diff --git a/user/admin.py b/user/admin.py index ff848e53..9b4e0c1f 100644 --- a/user/admin.py +++ b/user/admin.py @@ -15,6 +15,7 @@ class UserNotificationAdmin(admin.ModelAdmin): class UserAdmin(admin.ModelAdmin): list_display = [ "id", + "sso_identifier", "username", "first_name", "last_name", diff --git a/user/migrations/0010_user_sso_identifier.py b/user/migrations/0010_user_sso_identifier.py new file mode 100644 index 00000000..dce49c14 --- /dev/null +++ b/user/migrations/0010_user_sso_identifier.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.6 on 2025-09-12 06:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0009_user_oauth_token'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='sso_identifier', + field=models.CharField(blank=True, db_comment='Identifies the account based on an unique identifier from the SSO system', max_length=255, null=True), + ), + ] diff --git a/user/models/user.py b/user/models/user.py index 33390885..4acffb0b 100644 --- a/user/models/user.py +++ b/user/models/user.py @@ -6,6 +6,7 @@ Created on: 15.11.21 """ from django.contrib.auth.models import AbstractUser +from django.core.exceptions import ObjectDoesNotExist from django.db import models @@ -32,6 +33,12 @@ class User(AbstractUser): db_comment="OAuth token for the user", related_name="+" ) + sso_identifier = models.CharField( + blank=True, + null=True, + db_comment="Identifies the account based on an unique identifier from the SSO system", + max_length=255, + ) def is_notification_setting_set(self, notification_enum: UserNotificationEnum): return self.notifications.filter( @@ -264,4 +271,48 @@ class User(AbstractUser): self.oauth_token.delete() self.oauth_token = token self.save() - return self \ No newline at end of file + return self + + @staticmethod + def resolve_user_using_propagation_data(data: dict): + """ Fetches user from db by the given data from propagation process + + Args: + data (dict): json containing user information from the sso system + + Returns: + user (User): The resolved user + """ + username = data.get("username", None) + sso_identifier = data.get("sso_identifier", None) + if not username and not sso_identifier: + raise AssertionError("No username or sso identifier provided") + + try: + user = User.objects.get(username=username) + except ObjectDoesNotExist: + try: + user = User.objects.get(sso_identifier=sso_identifier) + except ObjectDoesNotExist: + raise ObjectDoesNotExist("No user with this username or sso identifier was found") + + return user + + def update_user_using_propagation_data(self, data: dict): + """ Update user data based on propagation data from sso system + + Args: + data (dict): json containing user information from the sso system + + Returns: + user (User): The updated user + """ + skipable_attrs = { + "is_staff", + "is_superuser", + } + for _attr, _val in data.items(): + if _attr in skipable_attrs: + continue + setattr(self, _attr, _val) + return self diff --git a/user/views/propagate.py b/user/views/propagate.py index 3afb6fcf..bb076502 100644 --- a/user/views/propagate.py +++ b/user/views/propagate.py @@ -44,17 +44,8 @@ class PropagateUserView(View): try: status = "updated" - user = User.objects.get(username=body.get('username')) - # Update user data, excluding some changes - skipable_attrs = { - "username", - "is_staff", - "is_superuser", - } - for _attr, _val in body.items(): - if _attr in skipable_attrs: - continue - setattr(user, _attr, _val) + user = User.resolve_user_using_propagation_data(body) + user = user.update_user_using_propagation_data(body) except ObjectDoesNotExist: user = User(**body) status = "created"