Compare commits

..

25 Commits

Author SHA1 Message Date
mpeltriaux 58d3c54920 Merge pull request '# Mass delete script' (#576) from 573_Script_mass_delete into master
Reviewed-on: #576
2026-08-17 15:20:46 +02:00
mpeltriaux dc16efcc64 # Mass delete script
* extends mass delete command so that not only entries can be marked as deleted but also finally be deleted from the database
2026-08-17 15:19:42 +02:00
mpeltriaux 906d6335ff Merge pull request '# Remove ExternalIdentifier' (#575) from 574_Remove_externalIdentifier into master
Reviewed-on: #575
2026-08-17 14:37:00 +02:00
mpeltriaux 6772ddb5c8 # Remove ExternalIdentifier
* adds removing of ExternalIdentifier entries if a BaseObject is removed from the database
* removes unused import statements
* adds ExternalIdentifier view to admin backend
2026-08-17 14:35:42 +02:00
mpeltriaux ea1276e40b Merge pull request '# HOTFIX OEK report' (#570) from fix_oek_report_empty_deduction_list into master
Reviewed-on: #570
2026-08-04 06:54:42 +02:00
mpeltriaux ee4701f63f # HOTFIX OEK report
* fixes wrong table layout on empty deduction list of a recorded oek
2026-08-04 06:53:29 +02:00
mpeltriaux 87e4ee726c Merge pull request '# Extended OEK report' (#568) from 564_Show_deductions_on_EcoAccount_report into master
Reviewed-on: #568
2026-08-03 13:07:34 +02:00
mpeltriaux cbe309f114 # Extended OEK report
* adds available rest surface of eco account to public report
* adds amount of deducted surface per each deduction to public report
2026-08-03 13:06:51 +02:00
mpeltriaux 2aa625a6ac Merge pull request '# Quickfix' (#566) from 565_Unify_geometry_quality_checking into master
Reviewed-on: #566
2026-07-16 15:43:06 +02:00
mpeltriaux d39e758df4 # Quickfix
* fixes bug for better API response on geometry invalidities
2026-07-16 15:42:29 +02:00
mpeltriaux 690d11e966 Merge pull request '# Mass delete command' (#562) from 561_Mass_delete_command_via_identifier into master
Reviewed-on: #562
2026-07-16 10:38:43 +02:00
mpeltriaux 02ce78551c # Mass delete command
* introduces new command for mass deleting entries based on specific pattern in identifier
    * command guides user through parameter collecting process
2026-07-16 10:38:00 +02:00
mpeltriaux 112b74f5cd Merge pull request '# Hotfix' (#559) from hotfix_API_error_msg_on_wrong_spatial_ref into master
Reviewed-on: #559
2026-07-10 19:02:51 +02:00
mpeltriaux 22a92cb3df # Hotfix
* adds special error message treatment for non-EPSG:4326 geometries
2026-07-10 19:01:24 +02:00
mpeltriaux f5aaac2acf Merge pull request '# Basemap' (#557) from 546_Remove_WebAtlasRP_service into master
Reviewed-on: #557
2026-07-10 14:01:38 +02:00
mpeltriaux 775e40e9ae # Basemap
* removes discontinued webatlas wms layers from config.json
* enables basemap_grau as default wms layer
2026-07-10 14:00:36 +02:00
mpeltriaux e794f3fff2 Merge pull request '# Improvement PUT API' (#556) from 554_API_PUT_external_identifier into master
Reviewed-on: #556
2026-07-10 13:59:22 +02:00
mpeltriaux b4f2e3232a # Improvement PUT API
* improves error processing and response on PUT endpoint
* consolidates improved error processing into central method for all API endpoints
2026-07-10 13:58:23 +02:00
mpeltriaux b9c453fdd2 Merge pull request '549 rework geometry conflict fetching' (#552) from 549_Rework_GeometryConflict_fetching into master
Reviewed-on: #552
2026-06-27 09:31:08 +02:00
mpeltriaux 625c591122 # Tests
* updates tests
* enhances workflow of geometry conflict fetching
2026-06-25 08:55:50 +02:00
mpeltriaux f551763798 # Performance boost
* boosts performance of geometry conflict fetching 50-75%
2026-06-25 08:27:09 +02:00
mpeltriaux 08884cb370 Merge pull request '# HOTFIX' (#550) from hotfix_indexErrorOnAPIGet into master
Reviewed-on: #550
2026-06-20 12:10:51 +02:00
mpeltriaux 9b5defec6d # HOTFIX
* fixes bug where unfetchable data entries of a GeometryConflict would result in an error via API
2026-06-20 12:10:13 +02:00
mpeltriaux 0425430e65 Merge pull request '# HOTFIX' (#547) from hotfix_indexErrorOnAPIGet into master
Reviewed-on: #547
2026-06-20 09:27:55 +02:00
mpeltriaux fec313445d # HOTFIX
* fixes a bug where detected GeometryConflicts with deleted entries would case an IndexError
2026-06-20 09:27:02 +02:00
16 changed files with 424 additions and 84 deletions
+12
View File
@@ -1,5 +1,6 @@
from django.contrib import admin
from api.models import ExternalIdentifier
from api.models.token import APIUserToken, OAuthToken
@@ -28,6 +29,17 @@ class OAuthTokenAdmin(admin.ModelAdmin):
"refresh_token",
]
class ExternalIdentifierAdmin(admin.ModelAdmin):
list_display = [
"external_id",
"internal_id",
"created",
]
search_fields = [
"external_id",
"internal_id",
]
admin.site.register(APIUserToken, APITokenAdmin)
admin.site.register(OAuthToken, OAuthTokenAdmin)
admin.site.register(ExternalIdentifier, ExternalIdentifierAdmin)
+20 -3
View File
@@ -6,6 +6,7 @@ Created on: 24.01.22
"""
import json
import uuid
from abc import abstractmethod
from django.contrib.gis import geos
@@ -155,6 +156,11 @@ class AbstractModelAPISerializer:
if isinstance(geojson, dict):
geojson = json.dumps(geojson)
geometry = geos.fromstr(geojson)
if not geometry.valid:
raise ValueError(f"Invalid geometry: {geometry.valid_reason}")
is_4326 = Geometry.is_valid_4326(geometry)
if not is_4326:
raise ValueError("Geometry not in EPSG:4326 (WGS84). Unknown spatial reference system.")
geometry = Geometry.cast_to_rlp_srid(geometry)
geometry = Geometry.cast_to_multipolygon(geometry)
return geometry
@@ -171,13 +177,20 @@ class AbstractModelAPISerializer:
Returns:
"""
if not id:
raise ValueError("No id provided. The id is expected to live in the URL!")
# First if there is an external identifier linked to an internal one, so we can continue with the internal
try:
ext_id = ExternalIdentifier.objects.get(external_id=id)
id = ext_id.internal_id
except ObjectDoesNotExist:
# No external id found - let's hope the given id exists internally
pass
# Not found as external id - let's check whether this is a valid uuid and therefore
# potentially an internal id
try:
uuid.UUID(id)
except ValueError:
raise AssertionError(f"'{id}' is neither a known external identifier nor a valid uuid.")
obj = self.model.objects.get(
id=id,
@@ -217,7 +230,11 @@ class AbstractModelAPISerializer:
conflict_geometries = geometry.get_conflict_geometries()
for geom in conflict_geometries:
try:
data = geom.get_data_objects(["identifier", "id"])[0]
data = geom.get_data_objects(["identifier", "id"])
if len(data) == 0:
# expected behaviour in case of deleted data object
continue
data = data[0]
except KeyError:
raise AssertionError(f"Geometry {geom.id} is not attached to any entries. Contact an admin!")
ids.append(
+15 -18
View File
@@ -10,6 +10,7 @@ from json import JSONDecodeError
from django.core.exceptions import ObjectDoesNotExist
from django.http import JsonResponse, HttpRequest
from django.views.decorators.csrf import csrf_exempt
from api.utils.serializer.v1.compensation import CompensationAPISerializerV1
from api.utils.serializer.v1.deduction import DeductionAPISerializerV1
@@ -28,6 +29,20 @@ class AbstractAPIViewV1(AbstractAPIView):
super().__init__(*args, **kwargs)
self.serializer = self.serializer()
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
try:
return super().dispatch(request, *args, **kwargs)
except (JSONDecodeError,
AssertionError,
ValueError,
PermissionError) as e:
return self._return_error_response(e, 400)
except ObjectDoesNotExist as e:
return self._return_error_response(e, 404)
except Exception as e:
return self._return_error_response(e, 500)
def get(self, request: HttpRequest, id=None):
""" Handles the GET request
@@ -40,17 +55,12 @@ class AbstractAPIViewV1(AbstractAPIView):
Returns:
response (JsonResponse)
"""
try:
self.rpp = int(request.GET.get("rpp", self.rpp))
self.page_number = int(request.GET.get("p", self.page_number))
self.serializer.rpp = self.rpp
self.serializer.page_number = self.page_number
self.serializer.prepare_lookup(id, self.user)
data = self.serializer.fetch_and_serialize()
except Exception as e:
return self._return_error_response(e, 500)
return self._return_response(request, data)
def post(self, request: HttpRequest):
@@ -64,16 +74,9 @@ class AbstractAPIViewV1(AbstractAPIView):
Returns:
response (JsonResponse)
"""
try:
body = request.body.decode("utf-8")
body = json.loads(body)
created_id = self.serializer.create_model_from_json(body, self.user)
except (JSONDecodeError,
AssertionError,
ValueError,
PermissionError,
ObjectDoesNotExist) as e:
return self._return_error_response(e, 400)
return JsonResponse({"id": created_id})
def put(self, request: HttpRequest, id=None):
@@ -88,12 +91,9 @@ class AbstractAPIViewV1(AbstractAPIView):
Returns:
response (JsonResponse)
"""
try:
body = request.body.decode("utf-8")
body = json.loads(body)
updated_id = self.serializer.update_model_from_json(id, body, self.user)
except Exception as e:
return self._return_error_response(e, 500)
return JsonResponse({"id": updated_id})
def delete(self, request: HttpRequest, id=None):
@@ -107,10 +107,7 @@ class AbstractAPIViewV1(AbstractAPIView):
response (JsonResponse)
"""
try:
success = self.serializer.delete_entry(id, self.user)
except Exception as e:
return self._return_error_response(e, 500)
return JsonResponse(
{
"success": success,
+2 -2
View File
@@ -325,8 +325,8 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
self.identifier = self.generate_new_identifier()
super().save(*args, **kwargs)
def mark_as_deleted(self, user, send_mail: bool = True):
super().mark_as_deleted(user, send_mail)
def mark_as_deleted(self, user, send_mail: bool = True, comment: str|None = None):
super().mark_as_deleted(user, send_mail, comment)
if user is not None:
self.intervention.mark_as_edited(user, edit_comment=COMPENSATION_REMOVED_TEMPLATE.format(self.identifier))
@@ -30,17 +30,45 @@
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans 'Available' %}</th>
<td>
{{available_total|floatformat:2}} / {{obj.deductable_surface|default_if_none:0.00|floatformat:2}} m²
{% with available as value %}
{% include 'konova/widgets/progressbar.html' %}
{% endwith %}
</td>
</tr>
<tr>
<th scope="row">{% trans 'Deductions for' %}</th>
<td class="align-middle">
<table class="table table-hover">
<th scope="col">
{% translate 'Intervention' %}
</th>
<th scope="col">
{% translate 'Amount' %}
</th>
{% for deduction in deductions %}
<tr>
<td>
<a href="{% url 'intervention:report' deduction.intervention__id %}">
{{deduction.intervention__identifier}} - {{deduction.intervention__title}}
</a>
<br>
</td>
<td>{{ deduction.surface|floatformat:2 }} m²</td>
</tr>
{% empty %}
{% trans 'None' %}
<tr>
<td>
{% translate 'None' %}
</td>
<td>
/
</td>
</tr>
{% endfor %}
</table>
</td>
</tr>
<tr>
+11 -1
View File
@@ -66,7 +66,15 @@ class EcoAccountPublicReportView(AbstractPublicReportView):
deductions = acc.deductions.all() \
.distinct("intervention") \
.select_related("intervention") \
.values_list("intervention__id", "intervention__identifier", "intervention__title", named=True)
.values_list("intervention__id",
"intervention__identifier",
"intervention__title",
"surface",
named=True)
# Calculate rest of available surface for deductions
available_total = acc.deductable_rest
available_relative = acc.get_deductable_rest_relative()
context = {
"obj": acc,
@@ -86,6 +94,8 @@ class EcoAccountPublicReportView(AbstractPublicReportView):
"actions": actions,
"deductions": deductions,
"tables_scrollable": False,
"available": available_relative,
"available_total": available_total,
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
+1 -1
View File
@@ -1,6 +1,6 @@
from django.contrib import admin
from intervention.models import Intervention, Responsibility, Legal, Revocation, InterventionDocument
from intervention.models import Intervention
from konova.admin import AbstractDocumentAdmin, BaseObjectAdmin
+3 -2
View File
@@ -306,7 +306,7 @@ class Intervention(BaseObject,
self.set_unchecked()
return action
def mark_as_deleted(self, user, send_mail: bool = True):
def mark_as_deleted(self, user, send_mail: bool = True, comment: str|None = None):
""" Extends base mark_as_delete functionality
Removes related deductions from the database, which results in updating the deductable_rest of the
@@ -315,11 +315,12 @@ class Intervention(BaseObject,
Args:
user (User): The performing user
send_mail (bool): Whether to send an info mail
comment (str|None): The delete comment
Returns:
"""
super().mark_as_deleted(user, send_mail)
super().mark_as_deleted(user, send_mail, comment)
# Remove pending deductions to free booked capacities
deductions = self.deductions.all()
+1 -3
View File
@@ -7,10 +7,7 @@ Created on: 22.07.21
"""
from django.contrib import admin
from konova.models import Geometry, Deadline, GeometryConflict, Parcel, District, Municipal, ParcelGroup, Resubmission
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
from user.models import UserAction
class GeometryAdmin(admin.ModelAdmin):
@@ -123,6 +120,7 @@ class BaseResourceAdmin(admin.ModelAdmin):
class BaseObjectAdmin(BaseResourceAdmin, DeletableObjectMixinAdmin):
search_fields = [
"id",
"identifier",
"title",
]
@@ -0,0 +1,211 @@
"""
Author: Michel Peltriaux
Created on: 16.07.26
"""
from django.db.models import QuerySet
from compensation.models import Compensation, EcoAccount
from ema.models import Ema
from intervention.models import Intervention
from konova.management.commands.setup import BaseKonovaCommand
from user.models import User
class Command(BaseKonovaCommand):
help = "Mass-deletes entries according to user input"
_DELETE_CLS = None
_DELETE_COMMENT = None
_DELETE_USER = None
_DELETE_FROM_DB = False
def handle(self, *args, **options):
self._get_delete_user()
self._get_object_type()
filter = self._get_filter()
self._get_delete_comment()
self._show_config(filter)
entries = self._collect_entries(filter)
self._process_delete(entries)
def _get_delete_user(self):
self._DELETE_USER = User.objects.filter(
is_superuser=True,
is_staff=True
).order_by(
"id"
).first()
if not self._DELETE_USER:
self._write_error("No admin-staff user could be found. Please check the admin users!")
def _get_object_type(self):
object_types = {
"eiv": Intervention,
"kom": Compensation,
"oek": EcoAccount,
"ema": Ema,
}
object_types_str = "|".join(object_types.keys())
object_type_select = input(f"Which object type shall be processed? ({object_types_str}): ").lower()
try:
self._DELETE_CLS = object_types[object_type_select]
if self._DELETE_CLS == EcoAccount:
oek_deduction_remove_understood = input(f"Please be aware that deleting OEKs will result in TOTAL REMOVING of their deductions without any chance to restore them. Do you want to continue? (y|n): ")
oek_deduction_remove_understood = oek_deduction_remove_understood.lower() == "y"
if not oek_deduction_remove_understood:
self._write_error("Removing of OEK canceled. Abort.")
exit(-1)
except KeyError:
self._write_error(f"'{object_type_select}' not a valid option of {object_types_str}. Abort.")
exit(-1)
def _get_filter(self):
filter_attribute = input("What do you want to filter entries by? (identifier|created_by_user): ").lower()
filter = {}
try:
assert filter_attribute in ["identifier", "created_by_user"]
if filter_attribute == "identifier":
filter_attribute = "identifier__contains"
elif filter_attribute == "created_by_user":
filter_attribute = "created__user__username"
except AssertionError:
self._write_error(f"'{filter_attribute}' unknown. Abort")
exit(-1)
filter_val = input("Use this value to search for: ")
try:
assert len(filter_val) > 0
except AssertionError:
self._write_error(f"'{filter_val}' is empty. Abort.")
exit(-1)
filter[filter_attribute] = filter_val
return filter
def _get_delete_comment(self):
self._DELETE_COMMENT = input("Comment to store in delete action: ")
def _show_config(self, filter: dict):
assert self._DELETE_CLS is not None
assert self._DELETE_COMMENT is not None
self._write_warning("You are about to delete entries with:")
self._write_warning(f" Object type: {self._DELETE_CLS.__name__}")
self._write_warning(f" Filter by: '{filter}'")
self._write_warning(f" Delete performing user: '{self._DELETE_USER}'")
def _collect_entries(self, filter: dict):
return self._DELETE_CLS.objects.filter(
deleted__isnull=True
).filter(
**filter
)
def _process_delete(self, entries: QuerySet):
""" Wrapping delete method. Forces user to decide whether entries will be marked as deleted or shall
be removed completely from database
Args:
entries (QuerySet): Entries to be processed
Returns:
"""
delete_type = input("Do you want 1) mark entries as deleted or 2) remove them from the database? (1|2): ")
try:
delete_type = int(delete_type)
assert delete_type in (1, 2)
self._DELETE_FROM_DB = delete_type == 2
except (ValueError, AssertionError) as e:
self._write_error(f"Entered unknown '{delete_type}'. Abort.")
exit(-1)
if self._DELETE_FROM_DB:
self._process_delete_from_db(entries)
else:
self._process_mark_as_deleted(entries)
def _process_mark_as_deleted(self, entries: QuerySet):
""" Method do mark chosen entries as deleted
Args:
entries (QuerySet): Entries to be processed
Returns:
"""
self._write_warning(f" Comment for delete action: '{self._DELETE_COMMENT}'")
self._check_entries_sample(entries)
entries_count = entries.count()
i = 0
for element in entries:
if i % 10 == 0:
self._write_warning(f" Deleted {i}/{entries_count}")
element.mark_as_deleted(user=self._DELETE_USER, send_mail=False, comment=self._DELETE_COMMENT)
i += 1
self._write_warning(f" Deleted {entries_count}/{entries_count}")
self._write_success("Entries deleted. Bye.")
def _process_delete_from_db(self, entries: QuerySet):
""" Method do REMOVE entries from the database
Args:
entries (QuerySet): Entries to be processed
Returns:
"""
self._check_entries_sample(entries)
entries_count = entries.count()
input_num_entries = input(f"THIS IS THE FINAL WARNING! {entries_count} ENTRIES WILL BE REMOVED FROM THE DATABASE! ENTER THE NUMBER OF ENTRIES TO BE DELETED TO CONTINUE: ")
try:
input_num_entries = int(input_num_entries)
except ValueError:
self._write_error(f"'{input_num_entries}' is not an integer. Abort.")
exit(-1)
if entries_count == input_num_entries:
i = 0
for element in entries:
if i % 10 == 0:
self._write_warning(f" Deleted {i}/{entries_count}")
#element.delete() # call individual delete routine to enforce custom delete logic (not called on bulk deleting!)
i += 1
self._write_warning(f" Deleted {entries_count}/{entries_count}")
self._write_success("Entries deleted. Bye.")
else:
self._write_error(f"'{input_num_entries}' was not correct. Abort.")
exit(-1)
def _check_entries_sample(self, entries):
""" Shows a sample from the entries on the terminal, so the user can decide whether the results are as expected
Args:
entries (QuerySet): Entries to be processed
Returns:
"""
entries_count = entries.count()
if entries_count == 0:
self._write_success(f"Found {entries_count} matches to delete. Nothing to do here. Bye.")
exit(0)
self._write_warning(
f"Found {entries_count} matches to delete. Please double-check whether these samples from the results are what you expected: ")
first_three_elements = entries[:3]
for element in first_three_elements:
self._write_warning(f" {element.identifier} ({element.id})")
delete_input = input("Are you ready to delete them now? (y|n): ").lower()
delete_now = delete_input == "y"
if not delete_now:
self._write_error(f"Entered '{delete_input}'. Abort.")
exit(-1)
+39 -3
View File
@@ -113,7 +113,7 @@ class Geometry(BaseResource):
objs (list): The list of objects
"""
objs = []
stop_searching = False
# Some related data sets can be processed rather easily
regular_sets = [
self.intervention_set,
@@ -129,10 +129,15 @@ class Geometry(BaseResource):
else:
objs += set_objs
stop_searching = len(objs) > 0
if stop_searching:
break
if not stop_searching:
# ... but we need a special treatment for compensations, since they can be deleted directly OR inherit their
# de-facto-deleted status from their deleted parent intervention
comp_objs = self.compensation_set.filter(
Q(deleted=None) & Q(intervention__deleted=None)
Q(deleted=None) | Q(intervention__deleted=None)
)
if limit_to_attrs:
objs += comp_objs.values(*limit_to_attrs)
@@ -142,11 +147,22 @@ class Geometry(BaseResource):
def get_data_object(self, limit_to_attrs: list = None):
"""
Getter for the specific data object which is related to this geometry
Getter for the specific data object which is related to this geometry.
!!! Only returns undeleted entries !!!
Returns:
result (str|None): Returns the desired attributes or None if the data object is marked as deleted
"""
objs = self.get_data_objects(limit_to_attrs)
assert (len(objs) <= 1)
try:
result = objs.pop()
except IndexError:
# If this happens, we just processed a GeometryConflict with an entry which is marked as deleted.
# Therefore we return None
result = None
return result
def update_parcels(self):
@@ -446,6 +462,26 @@ class Geometry(BaseResource):
conflict_geoms = Geometry.objects.filter(id__in=conflict_geoms_id)
return conflict_geoms
@staticmethod
def is_valid_4326(geometry):
""" Checks whether a given geometry's coordinates are in a valid range to be of EPSG:4326
Args:
geometry: The geometry
Returns:
ret_val (bool): Whether the geometry is valid EPSG:4326
"""
if not geometry.centroid.coords:
# No coordinates at all found, therefore technically proper 4326
return True
try:
lat,lon = geometry.centroid.coords
return (-90.0 <= lat <= 90.0) and (-180.0 <= lon <= 180.0)
except IndexError:
return False
class GeometryConflict(UuidModel):
"""
+34 -3
View File
@@ -92,13 +92,15 @@ class DeletableObjectMixin(models.Model):
class Meta:
abstract = True
def mark_as_deleted(self, user, send_mail: bool = True):
def mark_as_deleted(self, user, send_mail: bool = True, comment: str|None = None):
""" Mark an entry as deleted
Does not delete from database but sets a timestamp for being deleted on and which user deleted the object
Args:
user (User): The performing user
send_mail (bool): Whether to send mails to users and teams
comment (str|None): The comment for delete action
Returns:
@@ -109,7 +111,7 @@ class DeletableObjectMixin(models.Model):
return
with transaction.atomic():
action = UserActionLogEntry.get_deleted_action(user)
action = UserActionLogEntry.get_deleted_action(user, comment)
self.deleted = action
self.log.add(action)
@@ -144,6 +146,33 @@ class BaseObject(BaseResource, DeletableObjectMixin):
class Meta:
abstract = True
def delete(self, using=None, keep_parents=False):
""" Base deleting of a BaseObject instance
Args:
using:
keep_parents:
Returns:
"""
self.__delete_external_identifier()
super().delete(using, keep_parents)
def __delete_external_identifier(self):
""" Checks on existing external identifier linking and drops them
Returns:
"""
from api.models import ExternalIdentifier
try:
external_identifier = ExternalIdentifier.objects.filter(internal_id=self.id)
external_identifier.delete()
except ObjectDoesNotExist:
pass
@abstractmethod
def set_status_messages(self, request: HttpRequest):
raise NotImplementedError
@@ -679,7 +708,9 @@ class GeoReferencedMixin(models.Model):
conflicting_geometries = self.geometry.get_conflict_geometries()
data_object_identifiers = []
for conflicting_geom in conflicting_geometries:
data_object_identifiers.append(conflicting_geom.get_data_object(["identifier"]))
data_obj_id = conflicting_geom.get_data_object(["identifier"])
if data_obj_id:
data_object_identifiers.append(data_obj_id)
add_message = len(data_object_identifiers) > 0
if add_message:
+5 -14
View File
@@ -85,12 +85,6 @@ class GeometryTestCase(BaseTestCase):
)
def test_get_data_objects(self):
num_objs_with_geom = 0
self.assertEqual(
len(self.geom_1.get_data_objects()),
num_objs_with_geom
)
objs = [
self.intervention,
self.compensation,
@@ -98,16 +92,13 @@ class GeometryTestCase(BaseTestCase):
self.ema,
]
for obj in objs:
obj.geometry = self.geom_1
if not obj.geometry:
obj.geometry = Geometry.objects.create(geom=self.create_dummy_geometry())
obj.save()
num_objs_with_geom += 1
geom_objs = self.geom_1.get_data_objects()
self.assertEqual(
len(geom_objs),
num_objs_with_geom
)
self.assertIn(obj, geom_objs)
data_objs = obj.geometry.get_data_objects()
self.assertEqual(len(data_objs), 1)
self.assertIn(obj, data_objs)
def test_as_feature_collection(self):
geometry = self.geom_1.geom
+5
View File
@@ -0,0 +1,5 @@
"""
Author: Michel Peltriaux
Created on: 16.07.26
"""
+5
View File
@@ -0,0 +1,5 @@
"""
Author: Michel Peltriaux
Created on: 16.07.26
"""
+1 -3
View File
@@ -56,11 +56,9 @@
],
"layers":
[
{ "id": "webatlas_farbe", "folder": "bg", "type": "WMS", "order": -1, "title": "WebatlasRP farbig", "attribution": "LVermGeo", "url": "https://maps.service24.rlp.de/gisserver/services/RP/RP_WebAtlasRP/MapServer/WmsServer?", "name": "RP_WebAtlasRP", "active": true},
{ "id": "webatlas_grau", "folder": "bg", "type": "WMS", "order": -1, "title": "WebatlasRP grau", "attribution": "LVermGeo", "url": "https://maps.service24.rlp.de/gisserver/services/RP/RP_ETRS_Gt/MapServer/WmsServer?", "name": "0", "active": false },
{ "id": "luftbilder", "folder": "bg", "type": "WMS", "order": -1, "title": "Luftbilder", "attribution": "LVermGeo", "url": "https://geo4.service24.rlp.de/wms/rp_dop20.fcgi?", "name": "rp_dop20", "active": false },
{ "id": "basemap_farbe", "folder": "bg", "type": "WMS", "order": -1, "title": "BasemapDE farbig", "attribution": "BKG", "url": "https://sgx.geodatenzentrum.de/wms_basemapde?", "name": "de_basemapde_web_raster_farbe", "active": false },
{ "id": "basemap_grau", "folder": "bg", "type": "WMS", "order": -1, "title": "BasemapDE grau", "attribution": "BKG", "url": "https://sgx.geodatenzentrum.de/wms_basemapde?", "name": "de_basemapde_web_raster_grau", "active": false },
{ "id": "basemap_grau", "folder": "bg", "type": "WMS", "order": -1, "title": "BasemapDE grau", "attribution": "BKG", "url": "https://sgx.geodatenzentrum.de/wms_basemapde?", "name": "de_basemapde_web_raster_grau", "active": true },
{ "id": "dtk_farbe", "folder": "bg", "type": "WMS", "order": -1, "title": "DTK5 farbig", "attribution": "LVermGeo", "url": "https://geo4.service24.rlp.de/wms/dtk5_rp.fcgi?", "name": "rp_dtk5", "active": false },
{ "id": "dtk_grau", "folder": "bg", "type": "WMS", "order": -1, "title": "DTK5 grau", "attribution": "LVermGeo", "url": "https://geo4.service24.rlp.de/wms/dtk5_rp.fcgi?", "name": "rp_dtk5_grau", "active": false },