Compare commits

...

12 Commits
1.10 ... cicd

Author SHA1 Message Date
cfa4abd9ac .gitea/workflows/UnitTests.yaml aktualisiert
Some checks failed
Gitea Actions Demo / Run Tests (push) Failing after 20s
2025-10-10 16:41:45 +02:00
30cc665f05 .gitea/workflows/UnitTests.yaml aktualisiert
Some checks failed
Gitea Actions Demo / Run Tests (push) Failing after 19s
2025-10-10 16:40:17 +02:00
80690a9c8c .gitea/workflows/UnitTests.yaml aktualisiert
Some checks failed
Gitea Actions Demo / Run Tests (push) Failing after 20s
2025-10-10 15:49:32 +02:00
29f51663a5 .gitea/workflows/UnitTests.yaml aktualisiert
Some checks failed
Gitea Actions Demo / Run Tests (push) Failing after 29s
2025-10-10 15:45:04 +02:00
8913e63997 .gitea/workflows/UnitTests.yaml aktualisiert
Some checks failed
Gitea Actions Demo / Run Tests (push) Failing after 21s
2025-10-10 15:34:31 +02:00
46956e971c .gitea/workflows/UnitTests.yaml aktualisiert
Some checks failed
Gitea Actions Demo / Run Tests (push) Failing after 9s
2025-10-10 15:19:35 +02:00
4252fadc8d .gitea/workflows/UnitTests.yaml hinzugefügt
Some checks are pending
Gitea Actions Demo / Run Tests (push) Waiting to run
2025-10-10 15:18:18 +02:00
d6cabd5f6c Merge pull request 'sso_propagation_extension' (#483) from sso_propagation_extension into master
Reviewed-on: #483
2025-09-22 12:37:16 +02:00
63a824f9d9 # Geometry form fix
* fixes bugs in tests
* refactors and simplifies geometry merging on GeometryForm
2025-09-12 13:22:35 +02:00
a12c2fb57e # Propagation extension
* adds sso_identifier as new User model attribute
* refactors minor code snippets on user-propagation data resolving
* adds updating of username based on propagation data
* adds sso_identifier on admin backend view
2025-09-12 09:24:02 +02:00
23bc79ee3b Merge pull request '# Hotfix #480' (#481) from 480_API_error into master
Reviewed-on: #481
2025-08-18 08:46:47 +02:00
07bac26a58 # Hotfix #480
* (potentially) fixes a bug occuring on non multipolygon geometries processed in an api call
* simplifies casting into multipolygon
* simplifies casting into rlp srid (epsg:25832)
2025-08-18 08:46:21 +02:00
13 changed files with 173 additions and 41 deletions

View File

@@ -0,0 +1,36 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
on: [push]
jobs:
Run Tests:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
- name: Install dependencies if requirements.txt exists
run: |
python3 -m venv venv
source venv/bin/activate
if [ -f requirements.txt ]; then
pip install --no-deps -r requirements.txt
else
echo "No requirements.txt found, skipping installation."
fi
- name: Read .env from server and write to current directory
run: |
cat /home/jhaeder/.env > .env
- name: Run unit tests
run: |
source venv/bin/activate
python3 manage.py test

View File

@@ -12,7 +12,7 @@ from django.contrib.gis import geos
from django.urls import reverse from django.urls import reverse
from api.tests.v1.share.test_api_sharing import BaseAPIV1TestCase 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): class APIV1UpdateTestCase(BaseAPIV1TestCase):
@@ -64,7 +64,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
put_props = put_body["properties"] put_props = put_body["properties"]
put_geom = geos.fromstr(json.dumps(put_body)) 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_geom, self.intervention.geometry.geom)
self.assertEqual(put_props["title"], self.intervention.title) self.assertEqual(put_props["title"], self.intervention.title)
self.assertNotEqual(modified_on, self.intervention.modified) self.assertNotEqual(modified_on, self.intervention.modified)
@@ -94,7 +94,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
put_props = put_body["properties"] put_props = put_body["properties"]
put_geom = geos.fromstr(json.dumps(put_body)) 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_geom, self.compensation.geometry.geom)
self.assertEqual(put_props["title"], self.compensation.title) self.assertEqual(put_props["title"], self.compensation.title)
self.assertNotEqual(modified_on, self.compensation.modified) self.assertNotEqual(modified_on, self.compensation.modified)
@@ -124,7 +124,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
put_props = put_body["properties"] put_props = put_body["properties"]
put_geom = geos.fromstr(json.dumps(put_body)) 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_geom, self.eco_account.geometry.geom)
self.assertEqual(put_props["title"], self.eco_account.title) self.assertEqual(put_props["title"], self.eco_account.title)
self.assertNotEqual(modified_on, self.eco_account.modified) self.assertNotEqual(modified_on, self.eco_account.modified)
@@ -156,7 +156,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
put_props = put_body["properties"] put_props = put_body["properties"]
put_geom = geos.fromstr(json.dumps(put_body)) 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_geom, self.ema.geometry.geom)
self.assertEqual(put_props["title"], self.ema.title) self.assertEqual(put_props["title"], self.ema.title)
self.assertNotEqual(modified_on, self.ema.modified) self.assertNotEqual(modified_on, self.ema.modified)

View File

@@ -13,7 +13,7 @@ from django.contrib.gis.geos import GEOSGeometry
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db.models import Q 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 from konova.utils.message_templates import DATA_UNSHARED
@@ -145,8 +145,8 @@ class AbstractModelAPISerializer:
if isinstance(geojson, dict): if isinstance(geojson, dict):
geojson = json.dumps(geojson) geojson = json.dumps(geojson)
geometry = geos.fromstr(geojson) geometry = geos.fromstr(geojson)
if geometry.srid != DEFAULT_SRID_RLP: geometry = Geometry.cast_to_rlp_srid(geometry)
geometry.transform(DEFAULT_SRID_RLP) geometry = Geometry.cast_to_multipolygon(geometry)
return geometry return geometry
def _get_obj_from_db(self, id, user): def _get_obj_from_db(self, id, user):

View File

@@ -86,7 +86,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
new_title = self.create_dummy_string() new_title = self.create_dummy_string()
new_identifier = self.create_dummy_string() new_identifier = self.create_dummy_string()
new_comment = 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_conservation_office = self.get_conservation_office_code()
test_deductable_surface = self.eco_account.deductable_surface + 100 test_deductable_surface = self.eco_account.deductable_surface + 100
@@ -103,7 +103,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
"identifier": new_identifier, "identifier": new_identifier,
"title": new_title, "title": new_title,
"comment": new_comment, "comment": new_comment,
"geom": new_geometry.geojson, "geom": self.create_geojson(new_geometry),
"surface": test_deductable_surface, "surface": test_deductable_surface,
"conservation_office": test_conservation_office.id "conservation_office": test_conservation_office.id
} }

View File

@@ -84,7 +84,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
new_title = self.create_dummy_string() new_title = self.create_dummy_string()
new_identifier = self.create_dummy_string() new_identifier = self.create_dummy_string()
new_comment = 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() test_conservation_office = self.get_conservation_office_code()
check_on_elements = { check_on_elements = {
@@ -99,7 +99,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
"identifier": new_identifier, "identifier": new_identifier,
"title": new_title, "title": new_title,
"comment": new_comment, "comment": new_comment,
"geom": new_geometry.geojson, "geom": self.create_geojson(new_geometry),
"conservation_office": test_conservation_office.id "conservation_office": test_conservation_office.id
} }
self.client_user.post(url, post_data) self.client_user.post(url, post_data)

View File

@@ -124,7 +124,7 @@ class EditInterventionFormTestCase(NewInterventionFormTestCase):
self.assertIsNotNone(obj.responsible.handler) self.assertIsNotNone(obj.responsible.handler)
self.assertEqual(obj.title, data["title"]) self.assertEqual(obj.title, data["title"])
self.assertEqual(obj.comment, data["comment"]) 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.binding_date, today)
self.assertEqual(obj.legal.registration_date, today) self.assertEqual(obj.legal.registration_date, today)

View File

@@ -72,9 +72,8 @@ class SimpleGeomForm(BaseForm):
# will be rendered again on failed submit # will be rendered again on failed submit
self.initialize_form_field("geom", self.data["geom"]) self.initialize_form_field("geom", self.data["geom"])
# Read geojson into gdal geometry # Initialize features list with empty MultiPolygon, so that an empty input will result in a
# HINT: This can be simplified if the geojson format holds data in epsg:4326 (GDAL provides direct creation for # proper empty MultiPolygon object
# this case)
features = [] features = []
features_json = geom.get("features", []) features_json = geom.get("features", [])
accepted_ogr_types = [ accepted_ogr_types = [
@@ -102,23 +101,25 @@ class SimpleGeomForm(BaseForm):
return is_valid return is_valid
is_valid &= self.__is_area_valid(g) is_valid &= self.__is_area_valid(g)
g = Polygon.from_ewkt(g.ewkt)
polygon = Polygon.from_ewkt(g.ewkt) is_valid &= g.valid
is_valid &= polygon.valid if not g.valid:
if not polygon.valid: self.add_error("geom", g.valid_reason)
self.add_error("geom", polygon.valid_reason)
return is_valid 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 # Unionize all geometry features into one new MultiPolygon
form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP) if features:
for feature in features: form_geom = MultiPolygon(*features, srid=DEFAULT_SRID_RLP).unary_union
form_geom = form_geom.union(feature) else:
form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
# Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided. # Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided.
if form_geom.geom_type != "MultiPolygon": form_geom = Geometry.cast_to_multipolygon(form_geom)
form_geom = MultiPolygon(form_geom, srid=DEFAULT_SRID_RLP)
# Write unioned Multipolygon into cleaned data # Write unioned Multipolygon into cleaned data
if self.cleaned_data is None: if self.cleaned_data is None:

View File

@@ -11,6 +11,7 @@ from django.contrib.gis.db.models import MultiPolygonField
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.db import models, transaction from django.db import models, transaction
from django.utils import timezone from django.utils import timezone
from django.contrib.gis.geos import MultiPolygon
from konova.models import BaseResource, UuidModel from konova.models import BaseResource, UuidModel
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
@@ -383,6 +384,36 @@ class Geometry(BaseResource):
return complexity_factor 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): class GeometryConflict(UuidModel):
""" """

View File

@@ -469,7 +469,7 @@ class BaseTestCase(TestCase):
eco_account.save() eco_account.save()
return eco_account 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 """ Assert for geometries to be equal
Transforms the geometries to matching srids before checking 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 # transformation from one coordinate system into the other, which is valid
geom1.transform(geom2.srid) geom1.transform(geom2.srid)
geom2.transform(geom1.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): class BaseViewTestCase(BaseTestCase):

View File

@@ -15,6 +15,7 @@ class UserNotificationAdmin(admin.ModelAdmin):
class UserAdmin(admin.ModelAdmin): class UserAdmin(admin.ModelAdmin):
list_display = [ list_display = [
"id", "id",
"sso_identifier",
"username", "username",
"first_name", "first_name",
"last_name", "last_name",

View File

@@ -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),
),
]

View File

@@ -6,6 +6,7 @@ Created on: 15.11.21
""" """
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ObjectDoesNotExist
from django.db import models from django.db import models
@@ -32,6 +33,12 @@ class User(AbstractUser):
db_comment="OAuth token for the user", db_comment="OAuth token for the user",
related_name="+" 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): def is_notification_setting_set(self, notification_enum: UserNotificationEnum):
return self.notifications.filter( return self.notifications.filter(
@@ -264,4 +271,48 @@ class User(AbstractUser):
self.oauth_token.delete() self.oauth_token.delete()
self.oauth_token = token self.oauth_token = token
self.save() self.save()
return self 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

View File

@@ -44,17 +44,8 @@ class PropagateUserView(View):
try: try:
status = "updated" status = "updated"
user = User.objects.get(username=body.get('username')) user = User.resolve_user_using_propagation_data(body)
# Update user data, excluding some changes user = user.update_user_using_propagation_data(body)
skipable_attrs = {
"username",
"is_staff",
"is_superuser",
}
for _attr, _val in body.items():
if _attr in skipable_attrs:
continue
setattr(user, _attr, _val)
except ObjectDoesNotExist: except ObjectDoesNotExist:
user = User(**body) user = User(**body)
status = "created" status = "created"