Compare commits

..

62 Commits
v1.0 ... v1.1

Author SHA1 Message Date
029b9cf794 Revert "File number public reports"
This reverts commit 90fffb9576.
2022-11-28 13:50:31 +01:00
059972b4cd Merge pull request 'File number public reports' (#252) from remove_file_numbers_public_report into master
Reviewed-on: SGD-Nord/konova#252
2022-11-28 07:28:52 +01:00
90fffb9576 File number public reports
* removes file numbers from public reports
2022-11-28 07:28:09 +01:00
155d9d1d38 Merge pull request '222_Annual_reports' (#251) from 222_Annual_reports into master
Reviewed-on: SGD-Nord/konova#251
2022-11-25 09:18:04 +01:00
b8d9343682 Command response dynamic
* adds a check whether the mail could be sent properly or not and changes the resulting response
2022-11-25 09:17:15 +01:00
4ab713a908 Merge pull request '249_Last_modified_created' (#250) from 249_Last_modified_created into master
Reviewed-on: SGD-Nord/konova#250
2022-11-25 09:05:48 +01:00
f2c5e7ae01 Tests
* extends test for new behaviour of newly created entries
2022-11-25 09:05:06 +01:00
e048d44c95 #249 Created as modified
* fills modified attribute on new entries with created value automatically
* adds default ordering by last modified on table overviews
2022-11-25 08:27:42 +01:00
ad02a62eaf Merge pull request 'hotfix_geometry_save_race_condition' (#246) from hotfix_geometry_save_race_condition into master
Reviewed-on: SGD-Nord/konova#246
2022-11-23 16:06:50 +01:00
49d02b31f5 API - Geometry empty
* removes mapping of empty geometry to None due to general switch to empty geometry usage
2022-11-23 16:05:27 +01:00
0b2cf2a0a4 Geometry race condition fix
* fixes race condition for geometry conflict and parcel calculation
* harmonizes empty geometries from None/MultiPolygonEmpty to MultiPolygonEmpty
2022-11-23 13:51:05 +01:00
618cf3a756 Merge pull request '243_GDALException_on_null' (#244) from 243_GDALException_on_null into master
Reviewed-on: SGD-Nord/konova#244
2022-11-23 07:13:17 +01:00
e06e0e8306 Further fixes
* fixes race condition on geometry conflict calculation if performed in background process
* simplifies access to smaller buffered geometry
* adds mapping of "qm"->"m2" for UnitChoice in API usage for backwards compatibility
2022-11-22 15:38:03 +01:00
870d822c3a #243 Feature without geometry
* fixes GDALException in case of provided feature (import) without geometry content
* modifies 500.html template to inform the user about the admins being informed automatically
2022-11-22 14:49:51 +01:00
cf874225c1 Hotfix
* adds missing migration
2022-11-18 16:22:24 +01:00
25cd104cf6 Merge pull request 'Updates LANIS link' (#240) from lanis_link_fix into master
Reviewed-on: SGD-Nord/konova#240
2022-11-18 13:32:12 +01:00
ec0a5fefbd Merge pull request '#238 Fix' (#239) from 238_Type_error_on_eco_account_edit into master
Reviewed-on: SGD-Nord/konova#239
2022-11-18 13:29:11 +01:00
c50632e7a0 Updates LANIS link
* changes LANIS link to new layer declaration
2022-11-18 13:28:13 +01:00
3cd23b1761 #238 Fix
* adds casting from Decimal() to primitive float for proper calculation
2022-11-18 13:24:36 +01:00
e664fbbb7d Merge pull request 'Egon Payment compatibility' (#237) from fix_egon_payment_sum into master
Reviewed-on: SGD-Nord/konova#237
2022-11-18 07:54:58 +01:00
feed774679 Egon Payment compatibility
* EGON expects the payment amount to be a localized string instead of float
    * adds transformation for this
2022-11-18 07:40:35 +01:00
746ee7a283 Merge pull request 'fix_comp_action_units' (#235) from fix_comp_action_units into master
Reviewed-on: SGD-Nord/konova#235
2022-11-18 06:52:59 +01:00
b0f9ee4ac0 Z-axis geometry upload fix
* adds clamping of 3D geometries to 2D geometries if uploaded using the map importer
* extends tests for payment-document linkage
* fixes bug in team-admin selection where autocomplete could not be resolved properly
2022-11-17 13:01:40 +01:00
814f35c426 Fix CompensationAction unit None
* adds correct declaration of unit (qm -> m2) for template rendering
* adds migration to transform existing qm units to m2
2022-11-17 10:13:22 +01:00
7c4940729c Merge pull request 'post_release_fixes' (#233) from post_release_fixes into master
Reviewed-on: SGD-Nord/konova#233
2022-11-17 06:54:42 +01:00
7d0c405f58 #232 Payment document linkage
* adds error message on intervention view if a payment has been added but no document has been uploaded yet
* adds same check to quality checker, meaning no intervention can be recorded which has a payment but no document
* adds trigger for sending data to egon on uploading a document in case of an already existing payment
* adds translations
2022-11-16 16:11:42 +01:00
cbb137a902 #230 Shared users not rendered on compensation
* fixes non rendering of shared users on shared data
2022-11-16 13:30:07 +01:00
14fee4474f #229 Shared users mandatory on admin
* changes mandatory state of users and teams on admin backend to optional (as expected by the model)
* adds team selection to admin backend
2022-11-16 13:27:57 +01:00
deb97fbbf3 #228 Parcel filter
* changes parcel related filter fields from CharField to NumberField to avoid unexpected behaviour on non-numerical input
2022-11-16 13:18:52 +01:00
eb2d01eeea #231 Report geometry
* should fix report geometry cast problem in cases where MultiPolygon(srid=4326) is taken as default
2022-11-16 12:20:16 +01:00
933332c1ef Merge pull request 'Team mail fix' (#226) from 225_Team_mails_always_sent into master
Reviewed-on: SGD-Nord/konova#226
2022-11-14 07:23:15 +01:00
11e5d82086 Team mail fix
* filters team members by notification settings before sending team mails
2022-11-14 07:19:04 +01:00
7650e0bf73 Command
* adds new 'generate_report' command
   * generates TimeSpanReports for given conservation offices
   * zips reports into archive
   * sents archive to ADMINS mails
2022-10-26 15:44:33 +02:00
b1fe9ed9cb Bugfix
* fixes bug in excel report creation
* fixes order in laws of generated excel sheet
2022-10-26 10:35:15 +02:00
ddb1e82fbc Merge pull request 'minor_order_by_timestamp_improvement' (#223) from minor_order_by_timestamp_improvement into master
Reviewed-on: SGD-Nord/konova#223
2022-10-19 07:34:29 +02:00
1a8034fa20 Landing page shared count fix
* fixes bug where count of shared entries on landing page would ignore team-shared entries
* restore prior editable column icon rendering
2022-10-19 07:24:00 +02:00
a203d73471 Missing geometry html improvement
* improves rendering of missing geometry icons on table
2022-10-14 08:07:33 +02:00
e6c0d8b1cf Map client holes fix
* fixes bug where holes in stored geometries would not be rendered properly on initial loading the map client
    * drawback: multiple polygons are treated as a single feature on the map client. Not a real issue but maybe we can find a better solution to this
* quality of life: renders geometry area in m² on detail and report view
2022-10-14 08:02:08 +02:00
20c9950b7f Table improvements
* enhances visualization of editable column on tables
    * simplifies code
* enhances visualization of parcel_group column on tables
* WIP: Ordering on intervention table is odd. Same results are being displayed on page 2. Needs further analysis and fixing!
2022-10-12 16:26:01 +02:00
729a8f991c New help page link
* changes the help link to match the new starter help page
2022-10-12 10:20:04 +02:00
f4a1dd17b1 Map settings button removed
* removes map settings button, since there is no implementation and it is unclear, whether this will ever become a real feature
2022-10-12 10:18:57 +02:00
8e73387032 New icon for missing geometry
* introduces "..." as new icon for missing geometry entries
    * moves former explanatory message into title attribute for mouse hover activation
2022-10-12 10:15:27 +02:00
97b7156b9c Quality of Life improvements
* disables ordering of parcel_group ("Gemarkung") column on tables
    * ordering can not be done properly due to more complex nature of this column's content
* introduces "Keine Geometrie vorhanden" message instead of hour glass icon on entries where no geometry has been entered yet
* properly orders last_modified column by moving null values to the lower end of the ordering
2022-10-12 10:01:23 +02:00
18242d2cba Merge pull request '217_Prevent_recorded_deduction_deletion' (#220) from 217_Prevent_recorded_deduction_deletion into master
Reviewed-on: SGD-Nord/konova#220
2022-10-12 09:01:49 +02:00
2c20069dea Test extension
* adds further checks on EcoAccount tests for proper updating of new deductable_rest attribute
2022-10-12 08:59:38 +02:00
87b01e8fdd Fixes account deletion with deductions
* adds a warning on removing an eco account if there are still deductions
    * this way a user needs to get rid of these deductions first
2022-10-11 16:47:16 +02:00
bce271ceaa Fix for recorded deduction
* fixes bug where deduction of a recorded intervention could be deleted from the eco account detail view
* improves check_for_recorded_instance() logic
* improves rendering of detail view on compensation-like objects to highlight missing data
2022-10-11 16:32:12 +02:00
ac443bd9eb Merge pull request 'fix_acc_sorting_availability' (#218) from fix_acc_sorting_availability into master
Reviewed-on: SGD-Nord/konova#218
2022-10-11 15:21:23 +02:00
eb248be6f4 Fixes EcoAccount availability ordering
* adds db based table ordering for EcoAccountTable
2022-10-11 15:20:11 +02:00
ba7ae0b0b2 Hotfix for ordering of availability
* fixes error 500 in case of ordering by availability
2022-10-11 13:31:00 +02:00
27c7802760 Merge pull request 'Map client update' (#216) from map_tst into master
Reviewed-on: SGD-Nord/konova#216
2022-10-07 13:57:45 +02:00
6c8227ed17 Map client update
* updates map client to most recent code
* fixes bug on as_feature_collection which led to problems on initial loading in map
2022-10-07 13:56:43 +02:00
757598970b Merge pull request 'Netgis client update' (#214) from map_client_update_05102022 into master
Reviewed-on: SGD-Nord/konova#214
2022-10-05 11:01:55 +02:00
f38de97cf4 Netgis client update
* fixes address search results placement in scrollable context
* fixes import issues for gml and geojson
* updates basic configuration for map layers
2022-10-05 11:01:11 +02:00
750afdff08 Merge pull request '#208 API non existing atomID' (#212) from 208_API_non_existing_values into master
Reviewed-on: SGD-Nord/konova#212
2022-09-29 10:45:44 +02:00
b4cc919b02 #208 API non existing atomID
* handles error response for non existing atomID properly according to isse #208
2022-09-29 10:42:32 +02:00
49f57a4bf6 Merge pull request 'Fix geometry conflict message' (#211) from hotfix_geom_conflict_msg_with_deleted_entry into master
Reviewed-on: SGD-Nord/konova#211
2022-09-29 10:30:06 +02:00
c5f8e0c705 Fix geometry conflict message
* fixes bug where geometry conflicts template message has been rendered despite having no active geometry conflict
    * happened in case of existing geometry conflicts related to an as deleted marked entry
2022-09-29 10:29:33 +02:00
0bff9e0018 Merge pull request 'API Celery parcel calculation fix' (#209) from map_client_update into master
Reviewed-on: SGD-Nord/konova#209
2022-09-28 12:28:08 +02:00
0e9f0ba53e API Celery parcel calculation fix
* fixes bug where API stored data would not have parcels be calculated correctly
2022-09-28 12:24:06 +02:00
9ee0bddde9 Merge pull request 'Bugfix Parcel calculation' (#206) from map_client_update into master
Reviewed-on: SGD-Nord/konova#206
2022-09-16 12:13:24 +02:00
66a2387791 Bugfix Parcel calculation
* fixes a bug where neighbouring parcels would be detected using Intersection operation as well
2022-09-16 12:09:25 +02:00
79 changed files with 1267 additions and 585 deletions

View File

@@ -17,6 +17,7 @@ from compensation.models import Compensation, Payment, EcoAccountDeduction, EcoA
from intervention.models import Intervention from intervention.models import Intervention
from konova.models import Geometry from konova.models import Geometry
from konova.sub_settings.django_settings import BASE_DIR, DEFAULT_DATE_FORMAT from konova.sub_settings.django_settings import BASE_DIR, DEFAULT_DATE_FORMAT
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
class TimespanReport: class TimespanReport:
@@ -103,9 +104,9 @@ class TimespanReport:
"iterable": self.evaluated_laws, "iterable": self.evaluated_laws,
"attrs": [ "attrs": [
"short_name", "short_name",
"num",
"num_checked", "num_checked",
"num_recorded", "num_recorded",
"num",
] ]
}, },
"i_laws_checked": self.law_sum_checked, "i_laws_checked": self.law_sum_checked,
@@ -333,7 +334,7 @@ class TimespanReport:
return Geometry.objects.filter( return Geometry.objects.filter(
id__in=ids id__in=ids
).annotate( ).annotate(
geom_cast=Cast("geom", MultiPolygonField()) geom_cast=Cast("geom", MultiPolygonField(srid=DEFAULT_SRID_RLP))
).annotate( ).annotate(
num=NumGeometries("geom_cast") num=NumGeometries("geom_cast")
).aggregate( ).aggregate(

View File

@@ -1,5 +1,5 @@
{ {
"eco_account": "CHANGE_BEFORE_RUN!!!", "eco_account": "CHANGE_BEFORE_RUN!!!",
"surface": 500.0, "surface": 500.50,
"intervention": "CHANGE_BEFORE_RUN!!!" "intervention": "CHANGE_BEFORE_RUN!!!"
} }

View File

@@ -1,5 +1,5 @@
{ {
"eco_account": "CHANGE_BEFORE_RUN!!!", "eco_account": "CHANGE_BEFORE_RUN!!!",
"surface": 523400.0, "surface": 523400.50,
"intervention": "CHANGE_BEFORE_RUN!!!" "intervention": "CHANGE_BEFORE_RUN!!!"
} }

View File

@@ -136,8 +136,6 @@ class AbstractModelAPISerializer:
geometry = geos.fromstr(geojson) geometry = geos.fromstr(geojson)
if geometry.srid != DEFAULT_SRID_RLP: if geometry.srid != DEFAULT_SRID_RLP:
geometry.transform(DEFAULT_SRID_RLP) geometry.transform(DEFAULT_SRID_RLP)
if geometry.empty:
geometry = None
return geometry return geometry
def _get_obj_from_db(self, id, user): def _get_obj_from_db(self, id, user):

View File

@@ -11,7 +11,7 @@ from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, Abs
from compensation.models import Compensation from compensation.models import Compensation
from intervention.models import Intervention from intervention.models import Intervention
from konova.models import Geometry from konova.models import Geometry
from konova.tasks import celery_update_parcels from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts
from konova.utils.message_templates import DATA_UNSHARED from konova.utils.message_templates import DATA_UNSHARED
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@@ -64,6 +64,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
obj = Compensation() obj = Compensation()
created = create_action created = create_action
obj.created = created obj.created = created
obj.modified = created
obj.geometry = geometry obj.geometry = geometry
return obj return obj
@@ -125,11 +126,12 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states) obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states)
obj = self._set_deadlines(obj, properties["deadlines"]) obj = self._set_deadlines(obj, properties["deadlines"])
obj.log.add(obj.created) obj.log.add(obj.created)
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)
celery_check_for_geometry_conflicts.delay(obj.geometry.id)
return obj.id return obj.id
def update_model_from_json(self, id, json_model, user): def update_model_from_json(self, id, json_model, user):
""" Updates an entry for the model based on the contents of json_model """ Updates an entry for the model based on the contents of json_model
@@ -165,8 +167,8 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states) obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states)
obj = self._set_deadlines(obj, properties["deadlines"]) obj = self._set_deadlines(obj, properties["deadlines"])
obj.log.add(update_action) obj.log.add(update_action)
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)
return obj.id return obj.id

View File

@@ -13,7 +13,7 @@ from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_
from compensation.models import EcoAccount from compensation.models import EcoAccount
from intervention.models import Legal, Responsibility, Handler from intervention.models import Legal, Responsibility, Handler
from konova.models import Geometry from konova.models import Geometry
from konova.tasks import celery_update_parcels from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@@ -103,6 +103,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
obj.legal = Legal() obj.legal = Legal()
created = create_action created = create_action
obj.created = created obj.created = created
obj.modified = created
obj.geometry = geometry obj.geometry = geometry
return obj return obj
@@ -146,12 +147,13 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states) obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states)
obj = self._set_deadlines(obj, properties["deadlines"]) obj = self._set_deadlines(obj, properties["deadlines"])
obj.log.add(obj.created) obj.log.add(obj.created)
obj.users.add(user) obj.users.add(user)
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)
celery_check_for_geometry_conflicts.delay(obj.geometry.id)
return obj.id return obj.id
def update_model_from_json(self, id, json_model, user): def update_model_from_json(self, id, json_model, user):
""" Updates an entry for the model based on the contents of json_model """ Updates an entry for the model based on the contents of json_model
@@ -190,8 +192,8 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states) obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states)
obj = self._set_deadlines(obj, properties["deadlines"]) obj = self._set_deadlines(obj, properties["deadlines"])
obj.log.add(update_action) obj.log.add(update_action)
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)
return obj.id return obj.id

View File

@@ -13,7 +13,7 @@ from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_
from ema.models import Ema from ema.models import Ema
from intervention.models import Responsibility, Handler from intervention.models import Responsibility, Handler
from konova.models import Geometry from konova.models import Geometry
from konova.tasks import celery_update_parcels from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@@ -85,6 +85,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
) )
created = create_action created = create_action
obj.created = created obj.created = created
obj.modified = created
obj.geometry = geometry obj.geometry = geometry
return obj return obj
@@ -118,12 +119,13 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states) obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states)
obj = self._set_deadlines(obj, properties["deadlines"]) obj = self._set_deadlines(obj, properties["deadlines"])
obj.log.add(obj.created) obj.log.add(obj.created)
obj.users.add(user) obj.users.add(user)
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)
celery_check_for_geometry_conflicts.delay(obj.geometry.id)
return obj.id return obj.id
def update_model_from_json(self, id, json_model, user): def update_model_from_json(self, id, json_model, user):
""" Updates an entry for the model based on the contents of json_model """ Updates an entry for the model based on the contents of json_model
@@ -159,8 +161,8 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states) obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states)
obj = self._set_deadlines(obj, properties["deadlines"]) obj = self._set_deadlines(obj, properties["deadlines"])
obj.log.add(update_action) obj.log.add(update_action)
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)
return obj.id return obj.id

View File

@@ -13,7 +13,7 @@ from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, \
from compensation.models import Payment from compensation.models import Payment
from intervention.models import Intervention, Responsibility, Legal, Handler from intervention.models import Intervention, Responsibility, Legal, Handler
from konova.models import Geometry from konova.models import Geometry
from konova.tasks import celery_update_parcels from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@@ -76,6 +76,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
created = create_action created = create_action
obj.legal = legal obj.legal = legal
obj.created = created obj.created = created
obj.modified = created
obj.geometry = geometry obj.geometry = geometry
obj.responsible = resp obj.responsible = resp
return obj return obj
@@ -161,12 +162,13 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
obj.legal.save() obj.legal.save()
obj.save() obj.save()
obj.users.add(user) obj.users.add(user)
obj.log.add(obj.created) obj.log.add(obj.created)
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)
celery_check_for_geometry_conflicts.delay(obj.geometry.id)
return obj.id return obj.id
def update_model_from_json(self, id, json_model, user): def update_model_from_json(self, id, json_model, user):
""" Updates an entry for the model based on the contents of json_model """ Updates an entry for the model based on the contents of json_model
@@ -198,8 +200,8 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
obj.legal.save() obj.legal.save()
obj.save() obj.save()
obj.mark_as_edited(user, edit_comment="API update") obj.mark_as_edited(user, edit_comment="API update")
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)
return obj.id return obj.id

View File

@@ -9,6 +9,7 @@ Created on: 24.01.22
import json import json
from django.contrib.gis.geos import MultiPolygon from django.contrib.gis.geos import MultiPolygon
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import QuerySet from django.db.models import QuerySet
from api.utils.serializer.serializer import AbstractModelAPISerializer from api.utils.serializer.serializer import AbstractModelAPISerializer
@@ -80,10 +81,14 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer):
json_str = str(json_str) json_str = str(json_str)
if len(json_str) == 0: if len(json_str) == 0:
return None return None
code = KonovaCode.objects.get( try:
atom_id=json_str, code = KonovaCode.objects.get(
code_lists__in=[code_list_identifier] atom_id=json_str,
) code_lists__in=[code_list_identifier]
)
except ObjectDoesNotExist as e:
msg = f"{e.args[0]} ({json_str} not found in official list {code_list_identifier})"
raise ObjectDoesNotExist(msg)
return code return code
def _created_on_to_json(self, entry): def _created_on_to_json(self, entry):
@@ -387,7 +392,8 @@ class AbstractCompensationAPISerializerV1Mixin:
self._konova_code_from_json(e, CODELIST_COMPENSATION_ACTION_DETAIL_ID) for e in entry["action_details"] self._konova_code_from_json(e, CODELIST_COMPENSATION_ACTION_DETAIL_ID) for e in entry["action_details"]
] ]
amount = float(entry["amount"]) amount = float(entry["amount"])
unit = entry["unit"] # Mapping of old "qm" into "m²"
unit = UnitChoices.m2.value if entry["unit"] == "qm" else entry["unit"]
comment = entry["comment"] comment = entry["comment"]
# Check on validity # Check on validity

View File

@@ -81,13 +81,15 @@ class EcoAccountAdmin(AbstractCompensationAdmin):
] ]
filter_horizontal = [ filter_horizontal = [
"users" "users",
"teams",
] ]
def get_fields(self, request, obj=None): def get_fields(self, request, obj=None):
return super().get_fields(request, obj) + [ return super().get_fields(request, obj) + [
"deductable_surface", "deductable_surface",
"users" "users",
"teams",
] ]

View File

@@ -129,12 +129,11 @@ class NewCompensationForm(AbstractCompensationForm,
self.initialize_form_field("identifier", identifier) self.initialize_form_field("identifier", identifier)
self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:new-id") self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:new-id")
def __create_comp(self, user, geom_form) -> Compensation: def __create_comp(self, user):
""" Creates the compensation from form data """ Creates the compensation from form data
Args: Args:
user (User): The performing user user (User): The performing user
geom_form (SimpleGeomForm): The geometry form
Returns: Returns:
comp (Compensation): The compensation object comp (Compensation): The compensation object
@@ -150,8 +149,6 @@ class NewCompensationForm(AbstractCompensationForm,
# Create log entry # Create log entry
action = UserActionLogEntry.get_created_action(user) action = UserActionLogEntry.get_created_action(user)
# Process the geometry form
geometry = geom_form.save(action)
# Finally create main object # Finally create main object
comp = Compensation.objects.create( comp = Compensation.objects.create(
@@ -159,21 +156,27 @@ class NewCompensationForm(AbstractCompensationForm,
title=title, title=title,
intervention=intervention, intervention=intervention,
created=action, created=action,
modified=action,
is_cef=is_cef, is_cef=is_cef,
is_coherence_keeping=is_coherence_keeping, is_coherence_keeping=is_coherence_keeping,
is_pik=is_pik, is_pik=is_pik,
geometry=geometry,
comment=comment, comment=comment,
) )
# Add the log entry to the main objects log list # Add the log entry to the main objects log list
comp.log.add(action) comp.log.add(action)
return comp return comp, action
def save(self, user: User, geom_form: SimpleGeomForm): def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic(): with transaction.atomic():
comp = self.__create_comp(user, geom_form) comp, action = self.__create_comp(user)
comp.intervention.mark_as_edited(user, edit_comment=COMPENSATION_ADDED_TEMPLATE.format(comp.identifier)) comp.intervention.mark_as_edited(user, edit_comment=COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
# Process the geometry form
geometry = geom_form.save(action)
comp.geometry = geometry
comp.save()
return comp return comp
@@ -205,6 +208,9 @@ class EditCompensationForm(NewCompensationForm):
def save(self, user: User, geom_form: SimpleGeomForm): def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic(): with transaction.atomic():
# Create log entry
action = UserActionLogEntry.get_edited_action(user)
# Fetch data from cleaned POST values # Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None) identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None) title = self.cleaned_data.get("title", None)
@@ -214,17 +220,9 @@ class EditCompensationForm(NewCompensationForm):
is_pik = self.cleaned_data.get("is_pik", None) is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None) comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.get_edited_action(user)
# Process the geometry form
geometry = geom_form.save(action)
# Finally create main object
self.instance.identifier = identifier self.instance.identifier = identifier
self.instance.title = title self.instance.title = title
self.instance.intervention = intervention self.instance.intervention = intervention
self.instance.geometry = geometry
self.instance.is_cef = is_cef self.instance.is_cef = is_cef
self.instance.is_coherence_keeping = is_coherence_keeping self.instance.is_coherence_keeping = is_coherence_keeping
self.instance.comment = comment self.instance.comment = comment
@@ -233,6 +231,11 @@ class EditCompensationForm(NewCompensationForm):
self.instance.save() self.instance.save()
self.instance.log.add(action) self.instance.log.add(action)
intervention.mark_as_edited(user, self.request, EDITED_GENERAL_DATA) intervention.mark_as_edited(user, self.request, EDITED_GENERAL_DATA)
return self.instance
# Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!)
geometry = geom_form.save(action)
self.instance.geometry = geometry
self.instance.save()
return self.instance

View File

@@ -14,6 +14,7 @@ from compensation.forms.mixins import CompensationResponsibleFormMixin, PikCompe
from compensation.models import EcoAccount from compensation.models import EcoAccount
from intervention.models import Handler, Responsibility, Legal from intervention.models import Handler, Responsibility, Legal
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm
from user.models import User, UserActionLogEntry from user.models import User, UserActionLogEntry
@@ -93,8 +94,6 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
# Create log entry # Create log entry
action = UserActionLogEntry.get_created_action(user) action = UserActionLogEntry.get_created_action(user)
# Process the geometry form
geometry = geom_form.save(action)
handler = Handler.objects.create( handler = Handler.objects.create(
type=handler_type, type=handler_type,
@@ -118,7 +117,7 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
responsible=responsible, responsible=responsible,
deductable_surface=surface, deductable_surface=surface,
created=action, created=action,
geometry=geometry, modified=action,
comment=comment, comment=comment,
is_pik=is_pik, is_pik=is_pik,
legal=legal legal=legal
@@ -127,6 +126,12 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
# Add the log entry to the main objects log list # Add the log entry to the main objects log list
acc.log.add(action) acc.log.add(action)
# Process the geometry form
geometry = geom_form.save(action)
acc.geometry = geometry
acc.save()
acc.update_deductable_rest()
return acc return acc
@@ -182,9 +187,6 @@ class EditEcoAccountForm(NewEcoAccountForm):
# Create log entry # Create log entry
action = UserActionLogEntry.get_edited_action(user) action = UserActionLogEntry.get_edited_action(user)
# Process the geometry form
geometry = geom_form.save(action)
# Update responsible data # Update responsible data
self.instance.responsible.handler.type = handler_type self.instance.responsible.handler.type = handler_type
self.instance.responsible.handler.detail = handler_detail self.instance.responsible.handler.detail = handler_detail
@@ -201,7 +203,6 @@ class EditEcoAccountForm(NewEcoAccountForm):
self.instance.identifier = identifier self.instance.identifier = identifier
self.instance.title = title self.instance.title = title
self.instance.deductable_surface = surface self.instance.deductable_surface = surface
self.instance.geometry = geometry
self.instance.comment = comment self.instance.comment = comment
self.instance.is_pik = is_pik self.instance.is_pik = is_pik
self.instance.modified = action self.instance.modified = action
@@ -209,4 +210,23 @@ class EditEcoAccountForm(NewEcoAccountForm):
# Add the log entry to the main objects log list # Add the log entry to the main objects log list
self.instance.log.add(action) self.instance.log.add(action)
# Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!)
geometry = geom_form.save(action)
self.instance.geometry = geometry
self.instance.save()
self.instance.update_deductable_rest()
return self.instance return self.instance
class RemoveEcoAccountModalForm(RemoveModalForm):
def is_valid(self):
super_valid = super().is_valid()
has_deductions = self.instance.deductions.exists()
if has_deductions:
self.add_error(
"confirm",
_("The account can not be removed, since there are still deductions.")
)
return super_valid and not has_deductions

View File

@@ -0,0 +1,36 @@
# Generated by Django 3.1.3 on 2022-10-11 11:39
from django.db import migrations, models
from django.db.models import Sum
def fill_deductable_rest(apps, schema_editor):
EcoAccount = apps.get_model("compensation", "EcoAccount")
accs = EcoAccount.objects.all()
for acc in accs:
deductions = acc.deductions.filter(
intervention__deleted=None,
)
deductions_surfaces = deductions.aggregate(Sum("surface"))["surface__sum"] or 0
available_surfaces = acc.deductable_surface or deductions_surfaces
rest = available_surfaces - deductions_surfaces
acc.deductable_rest = rest
acc.save()
class Migration(migrations.Migration):
dependencies = [
('compensation', '0010_auto_20220815_1030'),
]
operations = [
migrations.AddField(
model_name='ecoaccount',
name='deductable_rest',
field=models.FloatField(blank=True, default=0, help_text='Amount of deductable rest', null=True),
),
migrations.RunPython(fill_deductable_rest)
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 3.1.3 on 2022-11-16 12:22
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0006_auto_20220815_0759'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('compensation', '0011_ecoaccount_deductable_rest'),
]
operations = [
migrations.AlterField(
model_name='ecoaccount',
name='teams',
field=models.ManyToManyField(blank=True, help_text='Teams having access (data shared with)', to='user.Team'),
),
migrations.AlterField(
model_name='ecoaccount',
name='users',
field=models.ManyToManyField(blank=True, help_text='Users having access (data shared with)', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,35 @@
# Generated by Django 3.1.3 on 2022-11-17 07:19
from django.db import migrations
from compensation.models import UnitChoices
def harmonize_action_units(apps, schema_editor):
"""
CompensationAction units (based on UnitChoices) can be mixed up at this point where
* qm represents m² and
* m2 represents m²
We drop qm in support of m2
"""
CompensationAction = apps.get_model("compensation", "CompensationAction")
actions = CompensationAction.objects.filter(
unit="qm"
)
for action in actions:
action.unit = UnitChoices.m2
action.save()
class Migration(migrations.Migration):
dependencies = [
('compensation', '0012_auto_20221116_1322'),
]
operations = [
migrations.RunPython(harmonize_action_units),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.3 on 2022-11-18 15:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('compensation', '0013_auto_20221117_0819'),
]
operations = [
migrations.AlterField(
model_name='compensationaction',
name='unit',
field=models.CharField(blank=True, choices=[('cm', 'cm'), ('m', 'm'), ('m2', ''), ('m3', ''), ('km', 'km'), ('ha', 'ha'), ('pcs', 'Pieces')], max_length=100, null=True),
),
]

View File

@@ -19,8 +19,9 @@ class UnitChoices(models.TextChoices):
""" """
cm = "cm", _("cm") cm = "cm", _("cm")
m = "m", _("m") m = "m", _("m")
m2 = "m2", _("")
m3 = "m3", _("")
km = "km", _("km") km = "km", _("km")
qm = "qm", _("")
ha = "ha", _("ha") ha = "ha", _("ha")
st = "pcs", _("Pieces") # pieces st = "pcs", _("Pieces") # pieces

View File

@@ -35,6 +35,12 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
help_text="Amount of deductable surface - can be lower than the total surface due to deduction limitations", help_text="Amount of deductable surface - can be lower than the total surface due to deduction limitations",
default=0, default=0,
) )
deductable_rest = models.FloatField(
blank=True,
null=True,
help_text="Amount of deductable rest",
default=0,
)
legal = models.OneToOneField( legal = models.OneToOneField(
"intervention.Legal", "intervention.Legal",
@@ -100,28 +106,29 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
""" """
return self.after_states.all().aggregate(Sum("surface"))["surface__sum"] or 0 return self.after_states.all().aggregate(Sum("surface"))["surface__sum"] or 0
def get_available_rest(self) -> (float, float): def __calculate_deductable_rest(self):
""" Calculates available rest surface of the eco account """ Calculates available rest surface of the eco account
Args: Args:
Returns: Returns:
ret_val_total (float): Total amount ret_val_total (float): Total amount
ret_val_relative (float): Amount as percentage (0-100)
""" """
deductions = self.deductions.filter( deductions = self.deductions.filter(
intervention__deleted=None, intervention__deleted=None,
) )
deductions_surfaces = deductions.aggregate(Sum("surface"))["surface__sum"] or 0 deductions_surfaces = deductions.aggregate(Sum("surface"))["surface__sum"] or 0
available_surfaces = self.deductable_surface or deductions_surfaces ## no division by zero
ret_val_total = available_surfaces - deductions_surfaces
if available_surfaces > 0: available_surface = self.deductable_surface
ret_val_relative = int((ret_val_total / available_surfaces) * 100) if available_surface is None:
# Fallback!
available_surface = deductions_surfaces
else: else:
ret_val_relative = 0 available_surface = float(available_surface)
return ret_val_total, ret_val_relative ret_val = available_surface - deductions_surfaces
return ret_val
def quality_check(self) -> EcoAccountQualityChecker: def quality_check(self) -> EcoAccountQualityChecker:
""" Quality check """ Quality check
@@ -181,6 +188,29 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
for team_id in shared_teams: for team_id in shared_teams:
celery_send_mail_deduction_changed_team.delay(self.identifier, self.title, team_id, data_change) celery_send_mail_deduction_changed_team.delay(self.identifier, self.title, team_id, data_change)
def update_deductable_rest(self):
"""
Updates deductable_rest, which holds the amount of rest surface for this account.
Returns:
"""
self.deductable_rest = self.__calculate_deductable_rest()
self.save()
def get_deductable_rest_relative(self):
"""
Returns deductable_rest relative to deductable_surface mapped to [0,100]
Returns:
"""
try:
ret_val = int((self.deductable_rest / (self.deductable_surface or 0)) * 100)
except ZeroDivisionError:
ret_val = 0
return ret_val
class EcoAccountDocument(AbstractDocument): class EcoAccountDocument(AbstractDocument):
""" """
@@ -272,3 +302,8 @@ class EcoAccountDeduction(BaseResource):
self.intervention.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED) self.intervention.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED)
self.account.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED) self.account.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED)
super().delete(*args, **kwargs) super().delete(*args, **kwargs)
self.account.update_deductable_rest()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self.account.update_deductable_rest()

View File

@@ -14,11 +14,11 @@ from django.utils.translation import gettext_lazy as _
from compensation.filters.compensation import CompensationTableFilter from compensation.filters.compensation import CompensationTableFilter
from compensation.models import Compensation from compensation.models import Compensation
from konova.utils.message_templates import DATA_IS_UNCHECKED, DATA_CHECKED_ON_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE from konova.utils.message_templates import DATA_IS_UNCHECKED, DATA_CHECKED_ON_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE
from konova.utils.tables import BaseTable, TableRenderMixin from konova.utils.tables import BaseTable, TableRenderMixin, TableOrderMixin
import django_tables2 as tables import django_tables2 as tables
class CompensationTable(BaseTable, TableRenderMixin): class CompensationTable(BaseTable, TableRenderMixin, TableOrderMixin):
id = tables.Column( id = tables.Column(
verbose_name=_("Identifier"), verbose_name=_("Identifier"),
orderable=True, orderable=True,
@@ -31,7 +31,7 @@ class CompensationTable(BaseTable, TableRenderMixin):
) )
d = tables.Column( d = tables.Column(
verbose_name=_("Parcel gmrkng"), verbose_name=_("Parcel gmrkng"),
orderable=True, orderable=False,
accessor="geometry", accessor="geometry",
) )
c = tables.Column( c = tables.Column(
@@ -126,28 +126,6 @@ class CompensationTable(BaseTable, TableRenderMixin):
) )
return format_html(html) return format_html(html)
def render_d(self, value, record: Compensation):
""" Renders the parcel district column for a compensation
Args:
value (str): The geometry
record (Compensation): The compensation record
Returns:
"""
parcels = value.get_underlying_parcels().values_list(
"parcel_group__name",
flat=True
).distinct()
html = render_to_string(
"table/gmrkng_col.html",
{
"entries": parcels
}
)
return html
def render_r(self, value, record: Compensation): def render_r(self, value, record: Compensation):
""" Renders the registered column for a compensation """ Renders the registered column for a compensation
@@ -170,20 +148,3 @@ class CompensationTable(BaseTable, TableRenderMixin):
) )
return format_html(html) return format_html(html)
def render_e(self, value, record: Compensation):
""" Renders the editable column for a compensation
Args:
value (str): The identifier value
record (Compensation): The compensation record
Returns:
"""
has_access = record.is_shared_with(self.user)
html = self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),
icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit",
)
return format_html(html)

View File

@@ -13,12 +13,12 @@ from django.utils.translation import gettext_lazy as _
from compensation.filters.eco_account import EcoAccountTableFilter from compensation.filters.eco_account import EcoAccountTableFilter
from compensation.models import EcoAccount from compensation.models import EcoAccount
from konova.utils.tables import TableRenderMixin, BaseTable from konova.utils.tables import TableRenderMixin, BaseTable, TableOrderMixin
import django_tables2 as tables import django_tables2 as tables
class EcoAccountTable(BaseTable, TableRenderMixin): class EcoAccountTable(BaseTable, TableRenderMixin, TableOrderMixin):
id = tables.Column( id = tables.Column(
verbose_name=_("Identifier"), verbose_name=_("Identifier"),
orderable=True, orderable=True,
@@ -31,13 +31,13 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
) )
d = tables.Column( d = tables.Column(
verbose_name=_("Parcel gmrkng"), verbose_name=_("Parcel gmrkng"),
orderable=True, orderable=False,
accessor="geometry", accessor="geometry",
) )
av = tables.Column( av = tables.Column(
verbose_name=_("Available"), verbose_name=_("Available"),
orderable=True, orderable=True,
empty_values=[], accessor="deductable_rest",
attrs={ attrs={
"th": { "th": {
"class": "w-20", "class": "w-20",
@@ -100,38 +100,19 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
""" Renders the available column for an eco account """ Renders the available column for an eco account
Args: Args:
value (str): The identifier value value (float): The deductable_rest
record (EcoAccount): The eco account record record (EcoAccount): The eco account record
Returns: Returns:
""" """
value_total, value_relative = record.get_available_rest() try:
value_relative = record.get_deductable_rest_relative()
except ZeroDivisionError:
value_relative = 0
html = render_to_string("konova/widgets/progressbar.html", {"value": value_relative}) html = render_to_string("konova/widgets/progressbar.html", {"value": value_relative})
return format_html(html) return format_html(html)
def render_d(self, value, record):
""" Renders the parcel district column for a compensation
Args:
value (str): The geometry
record (Compensation): The compensation record
Returns:
"""
parcels = value.get_underlying_parcels().values_list(
"parcel_group__name",
flat=True
).distinct()
html = render_to_string(
"table/gmrkng_col.html",
{
"entries": parcels
}
)
return html
def render_r(self, value, record: EcoAccount): def render_r(self, value, record: EcoAccount):
""" Renders the recorded column for an eco account """ Renders the recorded column for an eco account
@@ -153,23 +134,3 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
icn_filled=checked, icn_filled=checked,
) )
return format_html(html) return format_html(html)
def render_e(self, value, record: EcoAccount):
""" Renders the editable column for an eco account
Args:
value (str): The identifier value
record (EcoAccount): The eco account record
Returns:
"""
html = ""
# Do not use value in here, since value does use unprefetched 'users' manager, where record has already
# prefetched users data
has_access = record.is_shared_with(self.user)
html += self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),
icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit",
)
return format_html(html)

View File

@@ -74,6 +74,10 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %}
<div class="alert alert-danger mb-0">
{% trans 'Missing' %}
</div>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@@ -68,6 +68,10 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %}
<div class="alert alert-danger mb-0">
{% trans 'Missing' %}
</div>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@@ -68,6 +68,10 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %}
<div class="alert alert-danger mb-0">
{% trans 'Missing' %}
</div>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@@ -124,7 +124,7 @@
{% endfor %} {% endfor %}
<hr> <hr>
{% if has_access %} {% if has_access %}
{% for user in obj.users.all %} {% for user in obj.intervention.shared_users %}
{% include 'user/includes/contact_modal_button.html' %} {% include 'user/includes/contact_modal_button.html' %}
{% endfor %} {% endfor %}
{% else %} {% else %}

View File

@@ -73,6 +73,10 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %}
<div class="alert alert-danger mb-0">
{% trans 'Missing' %}
</div>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@@ -68,6 +68,10 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %}
<div class="alert alert-danger mb-0">
{% trans 'Missing' %}
</div>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@@ -68,6 +68,10 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %}
<div class="alert alert-danger mb-0">
{% trans 'Missing' %}
</div>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@@ -71,6 +71,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
self.assertEqual(new_compensation.title, test_title) self.assertEqual(new_compensation.title, test_title)
self.assert_equal_geometries(new_compensation.geometry.geom, test_geom) self.assert_equal_geometries(new_compensation.geometry.geom, test_geom)
self.assertEqual(new_compensation.log.count(), 1) self.assertEqual(new_compensation.log.count(), 1)
self.assertEqual(new_compensation.created, new_compensation.modified)
# Expect logs to be set # Expect logs to be set
self.assertEqual(pre_creation_intervention_log_count + 1, self.intervention.log.count()) self.assertEqual(pre_creation_intervention_log_count + 1, self.intervention.log.count())

View File

@@ -47,7 +47,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
"identifier": test_id, "identifier": test_id,
"title": test_title, "title": test_title,
"geom": geom_json, "geom": geom_json,
"deductable_surface": test_deductable_surface, "surface": test_deductable_surface,
"conservation_office": test_conservation_office.id "conservation_office": test_conservation_office.id
} }
self.client_user.post(new_url, post_data) self.client_user.post(new_url, post_data)
@@ -61,8 +61,11 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.assertEqual(acc.identifier, test_id) self.assertEqual(acc.identifier, test_id)
self.assertEqual(acc.title, test_title) self.assertEqual(acc.title, test_title)
self.assertEqual(acc.deductable_surface, test_deductable_surface)
self.assertEqual(acc.deductable_rest, test_deductable_surface)
self.assert_equal_geometries(acc.geometry.geom, test_geom) self.assert_equal_geometries(acc.geometry.geom, test_geom)
self.assertEqual(acc.log.count(), 1) self.assertEqual(acc.log.count(), 1)
self.assertEqual(acc.created, acc.modified)
# Expect logs to be set # Expect logs to be set
self.assertEqual(acc.log.count(), 1) self.assertEqual(acc.log.count(), 1)
@@ -84,7 +87,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
new_comment = self.create_dummy_string() new_comment = self.create_dummy_string()
new_geometry = MultiPolygon(srid=4326) # Create an empty geometry new_geometry = MultiPolygon(srid=4326) # Create an empty geometry
test_conservation_office = self.get_conservation_office_code() test_conservation_office = self.get_conservation_office_code()
test_deductable_surface = 10005 test_deductable_surface = self.eco_account.deductable_surface + 100
check_on_elements = { check_on_elements = {
self.eco_account.title: new_title, self.eco_account.title: new_title,
@@ -110,6 +113,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.eco_account.title: new_title, self.eco_account.title: new_title,
self.eco_account.identifier: new_identifier, self.eco_account.identifier: new_identifier,
self.eco_account.deductable_surface: test_deductable_surface, self.eco_account.deductable_surface: test_deductable_surface,
self.eco_account.deductable_rest: test_deductable_surface,
self.eco_account.comment: new_comment, self.eco_account.comment: new_comment,
} }
@@ -185,7 +189,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
# Prepare data for deduction creation # Prepare data for deduction creation
deduct_url = reverse("compensation:acc:new-deduction", args=(self.eco_account.id,)) deduct_url = reverse("compensation:acc:new-deduction", args=(self.eco_account.id,))
test_surface = 10.00 test_surface = 10.50
post_data = { post_data = {
"surface": test_surface, "surface": test_surface,
"account": self.eco_account.id, "account": self.eco_account.id,
@@ -194,17 +198,17 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
# Perform request --> expect to fail # Perform request --> expect to fail
self.client_user.post(deduct_url, post_data) self.client_user.post(deduct_url, post_data)
# Expect that no deduction has been created # Expect that no deduction has been created since the eco account is not recorded, yet
self.assertEqual(0, self.eco_account.deductions.count()) self.assertEqual(0, self.eco_account.deductions.count())
self.assertEqual(0, self.intervention.deductions.count()) self.assertEqual(0, self.intervention.deductions.count())
self.assertEqual(pre_deduction_acc_log_count, 0) self.assertEqual(pre_deduction_acc_log_count, 0)
self.assertEqual(pre_deduction_int_log_count, 0) self.assertEqual(pre_deduction_int_log_count, 0)
# Now mock the eco account as it would be recorded (with invalid data) # Now mock the eco account as it would be recorded (with invalid data)
# Make sure the deductible surface is high enough for the request # Make sure the deductible surface is valid for the request
self.eco_account.set_recorded(self.superuser) self.eco_account.set_recorded(self.superuser)
self.eco_account.refresh_from_db() self.eco_account.refresh_from_db()
self.eco_account.deductable_surface = test_surface + 1.00 self.eco_account.deductable_surface = test_surface + 1.0
self.eco_account.save() self.eco_account.save()
self.assertIsNotNone(self.eco_account.recorded) self.assertIsNotNone(self.eco_account.recorded)
self.assertGreater(self.eco_account.deductable_surface, test_surface) self.assertGreater(self.eco_account.deductable_surface, test_surface)
@@ -216,10 +220,12 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.client_user.post(deduct_url, post_data) self.client_user.post(deduct_url, post_data)
# Expect that the deduction has been created # Expect that the deduction has been created
self.eco_account.refresh_from_db()
self.assertEqual(1, self.eco_account.deductions.count()) self.assertEqual(1, self.eco_account.deductions.count())
self.assertEqual(1, self.intervention.deductions.count()) self.assertEqual(1, self.intervention.deductions.count())
deduction = self.eco_account.deductions.first() deduction = self.eco_account.deductions.first()
self.assertEqual(deduction.surface, test_surface) self.assertEqual(deduction.surface, test_surface)
self.assertEqual(self.eco_account.deductable_rest, self.eco_account.deductable_surface - deduction.surface)
self.assertEqual(deduction.account, self.eco_account) self.assertEqual(deduction.account, self.eco_account)
self.assertEqual(deduction.intervention, self.intervention) self.assertEqual(deduction.intervention, self.intervention)
@@ -230,7 +236,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.assertTrue(self.intervention.log.first().action == UserAction.EDITED) self.assertTrue(self.intervention.log.first().action == UserAction.EDITED)
def test_edit_deduction(self): def test_edit_deduction(self):
test_surface = self.eco_account.get_available_rest()[0] test_surface = self.eco_account.deductable_rest
self.eco_account.set_recorded(self.superuser) self.eco_account.set_recorded(self.superuser)
self.intervention.share_with_user(self.superuser) self.intervention.share_with_user(self.superuser)
self.eco_account.refresh_from_db() self.eco_account.refresh_from_db()
@@ -239,7 +245,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
deduction = EcoAccountDeduction.objects.create( deduction = EcoAccountDeduction.objects.create(
intervention=self.intervention, intervention=self.intervention,
account=self.eco_account, account=self.eco_account,
surface=0 surface=1.10
) )
self.assertEqual(1, self.intervention.deductions.count()) self.assertEqual(1, self.intervention.deductions.count())
self.assertEqual(1, self.eco_account.deductions.count()) self.assertEqual(1, self.eco_account.deductions.count())
@@ -262,6 +268,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.eco_account.refresh_from_db() self.eco_account.refresh_from_db()
deduction.refresh_from_db() deduction.refresh_from_db()
self.assertEqual(self.eco_account.deductable_rest, self.eco_account.deductable_surface - deduction.surface)
self.assertEqual(num_deductions_intervention, self.intervention.deductions.count()) self.assertEqual(num_deductions_intervention, self.intervention.deductions.count())
self.assertEqual(num_deductions_account, self.eco_account.deductions.count()) self.assertEqual(num_deductions_account, self.eco_account.deductions.count())
self.assertEqual(deduction.surface, test_surface) self.assertEqual(deduction.surface, test_surface)
@@ -275,6 +282,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
def test_remove_deduction(self): def test_remove_deduction(self):
intervention = self.deduction.intervention intervention = self.deduction.intervention
account = self.deduction.account account = self.deduction.account
deducted_surface = self.deduction.surface
# Prepare url and form data to be posted # Prepare url and form data to be posted
new_url = reverse("compensation:acc:remove-deduction", args=(account.id, self.deduction.id)) new_url = reverse("compensation:acc:remove-deduction", args=(account.id, self.deduction.id))
@@ -287,6 +295,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
pre_edit_intervention_log_count = intervention.log.count() pre_edit_intervention_log_count = intervention.log.count()
pre_edit_account_log_count = account.log.count() pre_edit_account_log_count = account.log.count()
pre_edit_account_rest = account.deductable_rest
num_deductions_intervention = intervention.deductions.count() num_deductions_intervention = intervention.deductions.count()
num_deductions_account = account.deductions.count() num_deductions_account = account.deductions.count()
@@ -297,6 +306,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.assertEqual(num_deductions_intervention - 1, intervention.deductions.count()) self.assertEqual(num_deductions_intervention - 1, intervention.deductions.count())
self.assertEqual(num_deductions_account - 1, account.deductions.count()) self.assertEqual(num_deductions_account - 1, account.deductions.count())
self.assertEqual(account.deductable_rest, pre_edit_account_rest + deducted_surface)
# Expect logs to be set # Expect logs to be set
self.assertEqual(pre_edit_intervention_log_count + 1, intervention.log.count()) self.assertEqual(pre_edit_intervention_log_count + 1, intervention.log.count())

View File

@@ -46,6 +46,8 @@ def index_view(request: HttpRequest):
compensations = Compensation.objects.filter( compensations = Compensation.objects.filter(
deleted=None, # only show those which are not deleted individually deleted=None, # only show those which are not deleted individually
intervention__deleted=None, # and don't show the ones whose intervention has been deleted intervention__deleted=None, # and don't show the ones whose intervention has been deleted
).order_by(
"-modified__timestamp"
) )
table = CompensationTable( table = CompensationTable(
request=request, request=request,

View File

@@ -13,13 +13,12 @@ from django.shortcuts import get_object_or_404, redirect, render
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 compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm, RemoveEcoAccountModalForm
from compensation.models import EcoAccount from compensation.models import EcoAccount
from compensation.tables.eco_account import EcoAccountTable from compensation.tables.eco_account import EcoAccountTable
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm
from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \ from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \
@@ -42,6 +41,8 @@ def index_view(request: HttpRequest):
template = "generic_index.html" template = "generic_index.html"
eco_accounts = EcoAccount.objects.filter( eco_accounts = EcoAccount.objects.filter(
deleted=None, deleted=None,
).order_by(
"-modified__timestamp"
) )
table = EcoAccountTable( table = EcoAccountTable(
request=request, request=request,
@@ -200,7 +201,8 @@ def detail_view(request: HttpRequest, id: str):
sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0 sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0
diff_states = abs(sum_before_states - sum_after_states) diff_states = abs(sum_before_states - sum_after_states)
# Calculate rest of available surface for deductions # Calculate rest of available surface for deductions
available_total, available_relative = acc.get_available_rest() available_total = acc.deductable_rest
available_relative = acc.get_deductable_rest_relative()
# Prefetch related data to decrease the amount of db connections # Prefetch related data to decrease the amount of db connections
deductions = acc.deductions.filter( deductions = acc.deductions.filter(
@@ -259,7 +261,7 @@ def remove_view(request: HttpRequest, id: str):
messages.info(request, CANCEL_ACC_RECORDED_OR_DEDUCTED) messages.info(request, CANCEL_ACC_RECORDED_OR_DEDUCTED)
return redirect("compensation:acc:detail", id=id) return redirect("compensation:acc:detail", id=id)
form = RemoveModalForm(request.POST or None, instance=acc, request=request) form = RemoveEcoAccountModalForm(request.POST or None, instance=acc, request=request)
return form.process_request( return form.process_request(
request=request, request=request,
msg_success=_("Eco-account removed"), msg_success=_("Eco-account removed"),

View File

@@ -6,12 +6,14 @@ from ema.models import Ema
class EmaAdmin(AbstractCompensationAdmin): class EmaAdmin(AbstractCompensationAdmin):
filter_horizontal = [ filter_horizontal = [
"users" "users",
"teams",
] ]
def get_fields(self, request, obj=None): def get_fields(self, request, obj=None):
return super().get_fields(request, obj) + [ return super().get_fields(request, obj) + [
"users" "users",
"teams",
] ]
admin.site.register(Ema, EmaAdmin) admin.site.register(Ema, EmaAdmin)

View File

@@ -64,8 +64,6 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin, Pik
# Create log entry # Create log entry
action = UserActionLogEntry.get_created_action(user) action = UserActionLogEntry.get_created_action(user)
# Process the geometry form
geometry = geom_form.save(action)
handler = Handler.objects.create( handler = Handler.objects.create(
type=handler_type, type=handler_type,
@@ -83,7 +81,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin, Pik
title=title, title=title,
responsible=responsible, responsible=responsible,
created=action, created=action,
geometry=geometry, modified=action,
comment=comment, comment=comment,
is_pik=is_pik, is_pik=is_pik,
) )
@@ -93,6 +91,11 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin, Pik
# Add the log entry to the main objects log list # Add the log entry to the main objects log list
acc.log.add(action) acc.log.add(action)
# Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!)
geometry = geom_form.save(action)
acc.geometry = geometry
acc.save()
return acc return acc
@@ -141,8 +144,6 @@ class EditEmaForm(NewEmaForm):
# Create log entry # Create log entry
action = UserActionLogEntry.get_edited_action(user) action = UserActionLogEntry.get_edited_action(user)
# Process the geometry form
geometry = geom_form.save(action)
# Update responsible data # Update responsible data
self.instance.responsible.handler.type = handler_type self.instance.responsible.handler.type = handler_type
@@ -155,7 +156,6 @@ class EditEmaForm(NewEmaForm):
# Update main oject data # Update main oject data
self.instance.identifier = identifier self.instance.identifier = identifier
self.instance.title = title self.instance.title = title
self.instance.geometry = geometry
self.instance.comment = comment self.instance.comment = comment
self.instance.is_pik = is_pik self.instance.is_pik = is_pik
self.instance.modified = action self.instance.modified = action
@@ -163,6 +163,11 @@ class EditEmaForm(NewEmaForm):
# Add the log entry to the main objects log list # Add the log entry to the main objects log list
self.instance.log.add(action) self.instance.log.add(action)
# Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!)
geometry = geom_form.save(action)
self.instance.geometry = geometry
self.instance.save()
return self.instance return self.instance

View File

@@ -0,0 +1,26 @@
# Generated by Django 3.1.3 on 2022-11-16 12:22
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0006_auto_20220815_0759'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('ema', '0007_auto_20220815_1030'),
]
operations = [
migrations.AlterField(
model_name='ema',
name='teams',
field=models.ManyToManyField(blank=True, help_text='Teams having access (data shared with)', to='user.Team'),
),
migrations.AlterField(
model_name='ema',
name='users',
field=models.ManyToManyField(blank=True, help_text='Users having access (data shared with)', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -6,21 +6,18 @@ Created on: 19.08.21
""" """
from django.http import HttpRequest from django.http import HttpRequest
from django.template.loader import render_to_string
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.timezone import localtime
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.urls import reverse from django.urls import reverse
import django_tables2 as tables import django_tables2 as tables
from konova.sub_settings.django_settings import DEFAULT_DATE_TIME_FORMAT from konova.utils.tables import BaseTable, TableRenderMixin, TableOrderMixin
from konova.utils.tables import BaseTable, TableRenderMixin
from ema.filters import EmaTableFilter from ema.filters import EmaTableFilter
from ema.models import Ema from ema.models import Ema
class EmaTable(BaseTable, TableRenderMixin): class EmaTable(BaseTable, TableRenderMixin, TableOrderMixin):
""" """
Since EMA and compensation are basically the same, we can reuse CompensationTableFilter and extend the EMA filter Since EMA and compensation are basically the same, we can reuse CompensationTableFilter and extend the EMA filter
in the future by inheriting. in the future by inheriting.
@@ -37,7 +34,7 @@ class EmaTable(BaseTable, TableRenderMixin):
) )
d = tables.Column( d = tables.Column(
verbose_name=_("Parcel gmrkng"), verbose_name=_("Parcel gmrkng"),
orderable=True, orderable=False,
accessor="geometry", accessor="geometry",
) )
r = tables.Column( r = tables.Column(
@@ -93,28 +90,6 @@ class EmaTable(BaseTable, TableRenderMixin):
) )
return format_html(html) return format_html(html)
def render_d(self, value, record: Ema):
""" Renders the parcel district column for a ema
Args:
value (str): The geometry
record (Ema): The ema record
Returns:
"""
parcels = value.get_underlying_parcels().values_list(
"parcel_group__name",
flat=True
).distinct()
html = render_to_string(
"table/gmrkng_col.html",
{
"entries": parcels
}
)
return html
def render_r(self, value, record: Ema): def render_r(self, value, record: Ema):
""" Renders the registered column for a EMA """ Renders the registered column for a EMA
@@ -136,22 +111,3 @@ class EmaTable(BaseTable, TableRenderMixin):
icn_filled=recorded, icn_filled=recorded,
) )
return format_html(html) return format_html(html)
def render_e(self, value, record: Ema):
""" Renders the editable column for a EMA
Args:
value (str): The identifier value
record (Ema): The EMA record
Returns:
"""
html = ""
has_access = record.is_shared_with(self.user)
html += self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),
icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit",
)
return format_html(html)

View File

@@ -71,6 +71,10 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %}
<div class="alert alert-danger mb-0">
{% trans 'Missing' %}
</div>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@@ -66,6 +66,10 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %}
<div class="alert alert-danger mb-0">
{% trans 'Missing' %}
</div>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@@ -66,6 +66,10 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %}
<div class="alert alert-danger mb-0">
{% trans 'Missing' %}
</div>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@@ -62,6 +62,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
self.assertEqual(ema.title, test_title) self.assertEqual(ema.title, test_title)
self.assert_equal_geometries(ema.geometry.geom, test_geom) self.assert_equal_geometries(ema.geometry.geom, test_geom)
self.assertEqual(ema.log.count(), 1) self.assertEqual(ema.log.count(), 1)
self.assertEqual(ema.created, ema.modified)
# Expect logs to be set # Expect logs to be set
self.assertEqual(ema.log.count(), 1) self.assertEqual(ema.log.count(), 1)

View File

@@ -40,8 +40,9 @@ def index_view(request: HttpRequest):
emas = Ema.objects.filter( emas = Ema.objects.filter(
deleted=None, deleted=None,
).order_by( ).order_by(
"-modified" "-modified__timestamp"
) )
table = EmaTable( table = EmaTable(
request, request,
queryset=emas queryset=emas

View File

@@ -14,7 +14,8 @@ class InterventionAdmin(BaseObjectAdmin):
] ]
filter_horizontal = [ filter_horizontal = [
"users" "users",
"teams",
] ]
def get_fields(self, request, obj=None): def get_fields(self, request, obj=None):
@@ -25,6 +26,7 @@ class InterventionAdmin(BaseObjectAdmin):
"checked", "checked",
"recorded", "recorded",
"users", "users",
"teams",
"geometry", "geometry",
] ]

View File

@@ -263,9 +263,6 @@ class NewInterventionForm(BaseForm):
handler=handler, handler=handler,
) )
# Process the geometry form
geometry = geom_form.save(action)
# Finally create main object, holding the other objects # Finally create main object, holding the other objects
intervention = Intervention.objects.create( intervention = Intervention.objects.create(
identifier=identifier, identifier=identifier,
@@ -273,7 +270,7 @@ class NewInterventionForm(BaseForm):
responsible=responsibility_data, responsible=responsibility_data,
legal=legal_data, legal=legal_data,
created=action, created=action,
geometry=geometry, modified=action,
comment=comment, comment=comment,
) )
@@ -282,6 +279,12 @@ class NewInterventionForm(BaseForm):
# Add the performing user as the first user having access to the data # Add the performing user as the first user having access to the data
intervention.share_with_user(user) intervention.share_with_user(user)
# Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!)
geometry = geom_form.save(action)
intervention.geometry = geometry
intervention.save()
return intervention return intervention
@@ -370,9 +373,6 @@ class EditInterventionForm(NewInterventionForm):
user_action = self.instance.mark_as_edited(user, edit_comment=EDITED_GENERAL_DATA) user_action = self.instance.mark_as_edited(user, edit_comment=EDITED_GENERAL_DATA)
geometry = geom_form.save(user_action)
self.instance.geometry = geometry
self.instance.log.add(user_action) self.instance.log.add(user_action)
self.instance.identifier = identifier self.instance.identifier = identifier
@@ -381,5 +381,10 @@ class EditInterventionForm(NewInterventionForm):
self.instance.modified = user_action self.instance.modified = user_action
self.instance.save() self.instance.save()
# Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!)
geometry = geom_form.save(user_action)
self.instance.geometry = geometry
self.instance.save()
return self.instance return self.instance

View File

@@ -163,6 +163,10 @@ class NewEcoAccountDeductionModalForm(BaseModalForm):
self.cleaned_data["account"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED) self.cleaned_data["account"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED)
return deduction return deduction
def check_for_recorded_instance(self):
# Ignore super() implementation
return
class EditEcoAccountDeductionModalForm(NewEcoAccountDeductionModalForm): class EditEcoAccountDeductionModalForm(NewEcoAccountDeductionModalForm):
deduction = None deduction = None
@@ -231,6 +235,16 @@ class EditEcoAccountDeductionModalForm(NewEcoAccountDeductionModalForm):
old_account.send_notification_mail_on_deduction_change(data_changes) old_account.send_notification_mail_on_deduction_change(data_changes)
return deduction return deduction
def check_for_recorded_instance(self):
"""
Extension to super class base method
Returns:
"""
if self.deduction.intervention.is_recorded:
self.block_form()
class RemoveEcoAccountDeductionModalForm(RemoveModalForm): class RemoveEcoAccountDeductionModalForm(RemoveModalForm):
""" Removing modal form for EcoAccountDeduction """ Removing modal form for EcoAccountDeduction
@@ -249,4 +263,8 @@ class RemoveEcoAccountDeductionModalForm(RemoveModalForm):
with transaction.atomic(): with transaction.atomic():
self.deduction.intervention.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED) self.deduction.intervention.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED)
self.deduction.account.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED) self.deduction.account.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED)
self.deduction.delete() self.deduction.delete()
def check_for_recorded_instance(self):
if self.deduction.intervention.is_recorded:
self.block_form()

View File

@@ -10,4 +10,23 @@ from konova.forms.modals import NewDocumentModalForm
class NewInterventionDocumentModalForm(NewDocumentModalForm): class NewInterventionDocumentModalForm(NewDocumentModalForm):
document_model = InterventionDocument document_model = InterventionDocument
def save(self, *args, **kwargs):
""" Extension of regular NewDocumentModalForm
Checks whether payments exist on the intervention and sends the data to EGON
Args:
*args ():
**kwargs ():
Returns:
"""
doc = super().save(*args, **kwargs)
if self.instance.payments.exists():
self.instance.send_data_to_egon()
return doc

View File

@@ -0,0 +1,26 @@
# Generated by Django 3.1.3 on 2022-11-16 12:22
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0006_auto_20220815_0759'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('intervention', '0007_auto_20220815_1030'),
]
operations = [
migrations.AlterField(
model_name='intervention',
name='teams',
field=models.ManyToManyField(blank=True, help_text='Teams having access (data shared with)', to='user.Team'),
),
migrations.AlterField(
model_name='intervention',
name='users',
field=models.ManyToManyField(blank=True, help_text='Users having access (data shared with)', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -14,11 +14,11 @@ from django.utils.translation import gettext_lazy as _
from intervention.filters import InterventionTableFilter from intervention.filters import InterventionTableFilter
from intervention.models import Intervention from intervention.models import Intervention
from konova.utils.message_templates import DATA_CHECKED_ON_TEMPLATE, DATA_IS_UNCHECKED, DATA_CHECKED_PREVIOUSLY_TEMPLATE from konova.utils.message_templates import DATA_CHECKED_ON_TEMPLATE, DATA_IS_UNCHECKED, DATA_CHECKED_PREVIOUSLY_TEMPLATE
from konova.utils.tables import BaseTable, TableRenderMixin from konova.utils.tables import BaseTable, TableRenderMixin, TableOrderMixin
import django_tables2 as tables import django_tables2 as tables
class InterventionTable(BaseTable, TableRenderMixin): class InterventionTable(BaseTable, TableRenderMixin, TableOrderMixin):
id = tables.Column( id = tables.Column(
verbose_name=_("Identifier"), verbose_name=_("Identifier"),
orderable=True, orderable=True,
@@ -31,7 +31,7 @@ class InterventionTable(BaseTable, TableRenderMixin):
) )
d = tables.Column( d = tables.Column(
verbose_name=_("Parcel gmrkng"), verbose_name=_("Parcel gmrkng"),
orderable=True, orderable=False,
accessor="geometry", accessor="geometry",
) )
c = tables.Column( c = tables.Column(
@@ -124,28 +124,6 @@ class InterventionTable(BaseTable, TableRenderMixin):
) )
return format_html(html) return format_html(html)
def render_d(self, value, record: Intervention):
""" Renders the parcel district column for an intervention
Args:
value (str): The intervention geometry
record (Intervention): The intervention record
Returns:
"""
parcels = value.get_underlying_parcels().values_list(
"parcel_group__name",
flat=True
).distinct()
html = render_to_string(
"table/gmrkng_col.html",
{
"entries": parcels
}
)
return html
def render_r(self, value, record: Intervention): def render_r(self, value, record: Intervention):
""" Renders the recorded column for an intervention """ Renders the recorded column for an intervention
@@ -168,22 +146,3 @@ class InterventionTable(BaseTable, TableRenderMixin):
) )
return format_html(html) return format_html(html)
def render_e(self, value, record: Intervention):
""" Renders the editable column for an intervention
Args:
value (str): The identifier value
record (Intervention): The intervention record
Returns:
"""
html = ""
has_access = record.is_shared_with(self.user)
html += self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),
icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit",
)
return format_html(html)

View File

@@ -20,6 +20,11 @@
</div> </div>
</div> </div>
</div> </div>
{% if has_payment_without_document %}
<div class="alert alert-danger mb-0">
{% trans 'You entered a payment. Please upload the legal document which defines the payment`s amount.' %}
</div>
{% endif %}
<div class="card-body scroll-300 p-2"> <div class="card-body scroll-300 p-2">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>

View File

@@ -11,7 +11,7 @@ from django.core.exceptions import ObjectDoesNotExist
from django.urls import reverse from django.urls import reverse
from compensation.models import Payment, EcoAccountDeduction from compensation.models import Payment, EcoAccountDeduction
from intervention.models import Intervention from intervention.models import Intervention, InterventionDocument
from konova.settings import ETS_GROUP, ZB_GROUP from konova.settings import ETS_GROUP, ZB_GROUP
from konova.tests.test_views import BaseWorkflowTestCase from konova.tests.test_views import BaseWorkflowTestCase
from user.models import UserActionLogEntry, UserAction from user.models import UserActionLogEntry, UserAction
@@ -78,6 +78,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
self.assertEqual(1, obj.log.count()) self.assertEqual(1, obj.log.count())
self.assertEqual(obj.log.first().action, UserAction.CREATED) self.assertEqual(obj.log.first().action, UserAction.CREATED)
self.assertEqual(obj.log.first().user, self.superuser) self.assertEqual(obj.log.first().user, self.superuser)
self.assertEqual(obj.created, obj.modified)
except ObjectDoesNotExist: except ObjectDoesNotExist:
# Fail if there is no such object # Fail if there is no such object
self.fail() self.fail()
@@ -153,13 +154,16 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
payment = Payment.objects.create(amount=10.00, due_on=None, comment="No due date because test") payment = Payment.objects.create(amount=10.00, due_on=None, comment="No due date because test")
self.intervention.payments.add(payment) self.intervention.payments.add(payment)
# Since there is a payment, we need to add a dummy document (mocking a legal document for payment info)
document = self.create_dummy_document(InterventionDocument, self.intervention)
# Run request again # Run request again
self.client_user.post(check_url, post_data) self.client_user.post(check_url, post_data)
# Update intervention from db # Update intervention from db
self.intervention.refresh_from_db() self.intervention.refresh_from_db()
# We expect the intervention to be checked now and contains the proper data # We expect the intervention to be checked now and contain the proper data
# Attention: We check the timestamp only on the date, not the time, since the microseconds delay would result # Attention: We check the timestamp only on the date, not the time, since the microseconds delay would result
# in an unwanted assertion error # in an unwanted assertion error
checked = self.intervention.checked checked = self.intervention.checked
@@ -209,6 +213,9 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
payment = Payment.objects.create(amount=10.00, due_on=None, comment="No due date because test") payment = Payment.objects.create(amount=10.00, due_on=None, comment="No due date because test")
self.intervention.payments.add(payment) self.intervention.payments.add(payment)
# Since there is a payment, we need to add a dummy document (mocking a legal document for payment info)
document = self.create_dummy_document(InterventionDocument, self.intervention)
# Run request again # Run request again
self.client_user.post(record_url, post_data) self.client_user.post(record_url, post_data)

View File

@@ -11,6 +11,7 @@ import json
import pika import pika
import xmltodict import xmltodict
from django.db.models import Sum from django.db.models import Sum
from django.utils import formats
from intervention.settings import EGON_RABBITMQ_HOST, EGON_RABBITMQ_USER, EGON_RABBITMQ_PW, EGON_RABBITMQ_PORT from intervention.settings import EGON_RABBITMQ_HOST, EGON_RABBITMQ_USER, EGON_RABBITMQ_PW, EGON_RABBITMQ_PORT
from konova.sub_settings.django_settings import DEFAULT_DATE_FORMAT from konova.sub_settings.django_settings import DEFAULT_DATE_FORMAT
@@ -92,6 +93,9 @@ class EgonGmlBuilder:
)["summed"] )["summed"]
return all_payments return all_payments
def _float_to_localized_string(self, value: float):
return formats.number_format(value, use_l10n=True, decimal_pos=2)
def _gen_kompensationsArt(self) -> (str, int): def _gen_kompensationsArt(self) -> (str, int):
comp_type = "Ersatzzahlung" comp_type = "Ersatzzahlung"
comp_type_code = 774898901 comp_type_code = 774898901
@@ -191,7 +195,7 @@ class EgonGmlBuilder:
"@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/1053/{reg_office.atom_id if reg_office else None}", "@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/1053/{reg_office.atom_id if reg_office else None}",
"#text": reg_office.long_name if reg_office else None "#text": reg_office.long_name if reg_office else None
}, },
"oneo:ersatzzahlung": self._sum_all_payments(), "oneo:ersatzzahlung": self._float_to_localized_string(self._sum_all_payments()),
"oneo:kompensationsart": { "oneo:kompensationsart": {
"@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/88140/{comp_type_code}", "@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/88140/{comp_type_code}",
"#text": comp_type "#text": comp_type

View File

@@ -21,8 +21,21 @@ class InterventionQualityChecker(AbstractQualityChecker):
self._check_legal_data() self._check_legal_data()
self._check_compensations() self._check_compensations()
self._check_geometry() self._check_geometry()
self._check_payment_documents()
self.valid = len(self.messages) == 0 self.valid = len(self.messages) == 0
def _check_payment_documents(self):
""" Checks existence of documents in case of payments
There should be at least one legal document which defines the payment's total amount.
Returns:
"""
has_payment_without_document = self.obj.payments.exists() and not self.obj.get_documents()[1].exists()
if has_payment_without_document:
self._add_missing_attr_name(_("Documents"))
def _check_responsible_data(self): def _check_responsible_data(self):
""" Checks data quality of related Responsibility """ Checks data quality of related Responsibility

View File

@@ -45,6 +45,8 @@ def index_view(request: HttpRequest):
deleted=None, # not deleted deleted=None, # not deleted
).select_related( ).select_related(
"legal" "legal"
).order_by(
"-modified__timestamp"
) )
table = InterventionTable( table = InterventionTable(
request=request, request=request,
@@ -157,6 +159,8 @@ def detail_view(request: HttpRequest, id: str):
if last_checked: if last_checked:
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user) last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user)
has_payment_without_document = intervention.payments.exists() and not intervention.get_documents()[1].exists()
context = { context = {
"obj": intervention, "obj": intervention,
"last_checked": last_checked, "last_checked": last_checked,
@@ -168,6 +172,7 @@ def detail_view(request: HttpRequest, id: str):
"is_zb_member": in_group(_user, ZB_GROUP), "is_zb_member": in_group(_user, ZB_GROUP),
"is_ets_member": in_group(_user, ETS_GROUP), "is_ets_member": in_group(_user, ETS_GROUP),
"LANIS_LINK": intervention.get_LANIS_link(), "LANIS_LINK": intervention.get_LANIS_link(),
"has_payment_without_document": has_payment_without_document,
TAB_TITLE_IDENTIFIER: f"{intervention.identifier} - {intervention.title}", TAB_TITLE_IDENTIFIER: f"{intervention.identifier} - {intervention.title}",
} }

View File

@@ -46,7 +46,7 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
), ),
) )
# Parcel # Parcel
p = django_filters.CharFilter( p = django_filters.NumberFilter(
method="filter_parcel", method="filter_parcel",
label=_(""), label=_(""),
label_suffix=_(""), label_suffix=_(""),
@@ -59,7 +59,7 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
), ),
) )
# Parcel counter # Parcel counter
pc = django_filters.CharFilter( pc = django_filters.NumberFilter(
method="filter_parcel_counter", method="filter_parcel_counter",
label=_(""), label=_(""),
label_suffix=_(""), label_suffix=_(""),
@@ -73,7 +73,7 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
) )
# Parcel counter # Parcel counter
pn = django_filters.CharFilter( pn = django_filters.NumberFilter(
method="filter_parcel_number", method="filter_parcel_number",
label=_(""), label=_(""),
label_suffix=_(""), label_suffix=_(""),
@@ -165,7 +165,6 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
Returns: Returns:
""" """
value = value.replace("-", "")
queryset = self._filter_parcel_reference( queryset = self._filter_parcel_reference(
queryset, queryset,
Q(flr=value), Q(flr=value),
@@ -183,7 +182,6 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
Returns: Returns:
""" """
value = value.replace("-", "")
queryset = self._filter_parcel_reference( queryset = self._filter_parcel_reference(
queryset, queryset,
Q(flrstck_zhlr=value) Q(flrstck_zhlr=value)
@@ -201,7 +199,6 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
Returns: Returns:
""" """
value = value.replace("-", "")
queryset = self._filter_parcel_reference( queryset = self._filter_parcel_reference(
queryset, queryset,
Q(flrstck_nnr=value), Q(flrstck_nnr=value),

View File

@@ -134,24 +134,21 @@ class BaseForm(forms.Form):
Returns: Returns:
""" """
from intervention.forms.modals.deduction import NewEcoAccountDeductionModalForm, EditEcoAccountDeductionModalForm, \
RemoveEcoAccountDeductionModalForm
from konova.forms.modals.resubmission_form import ResubmissionModalForm
is_none = self.instance is None is_none = self.instance is None
is_other_data_type = not isinstance(self.instance, BaseObject) is_other_data_type = not isinstance(self.instance, BaseObject)
is_deduction_form_from_account = isinstance(
self,
(
NewEcoAccountDeductionModalForm,
ResubmissionModalForm,
EditEcoAccountDeductionModalForm,
RemoveEcoAccountDeductionModalForm,
)
) and isinstance(self.instance, EcoAccount)
if is_none or is_other_data_type or is_deduction_form_from_account: if is_none or is_other_data_type:
# Do nothing # Do nothing
return return
if self.instance.is_recorded: if self.instance.is_recorded:
self.template = "form/recorded_no_edit.html" self.block_form()
def block_form(self):
"""
Overwrites template, providing no actions
Returns:
"""
self.template = "form/recorded_no_edit.html"

View File

@@ -10,11 +10,12 @@ import json
from django.contrib.gis import gdal from django.contrib.gis import gdal
from django.contrib.gis.forms import MultiPolygonField from django.contrib.gis.forms import MultiPolygonField
from django.contrib.gis.geos import MultiPolygon, Polygon from django.contrib.gis.geos import MultiPolygon, Polygon
from django.contrib.gis.geos.prototypes.io import WKTWriter
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from konova.forms.base_form import BaseForm from konova.forms.base_form import BaseForm
from konova.models import Geometry from konova.models import Geometry
from konova.tasks import celery_update_parcels from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@@ -62,6 +63,7 @@ class SimpleGeomForm(BaseForm):
geom = self.data["geom"] geom = self.data["geom"]
if geom is None or len(geom) == 0: if geom is None or len(geom) == 0:
# empty geometry is a valid geometry # empty geometry is a valid geometry
self.cleaned_data["geom"] = MultiPolygon(srid=DEFAULT_SRID_RLP).ewkt
return is_valid return is_valid
geom = json.loads(geom) geom = json.loads(geom)
@@ -74,9 +76,26 @@ class SimpleGeomForm(BaseForm):
# this case) # this case)
features = [] features = []
features_json = geom.get("features", []) features_json = geom.get("features", [])
accepted_ogr_types = [
"Polygon",
"Polygon25D",
"MultiPolygon",
"MultiPolygon25D",
]
for feature in features_json: for feature in features_json:
g = gdal.OGRGeometry(json.dumps(feature.get("geometry", feature)), srs=DEFAULT_SRID_RLP) feature_geom = feature.get("geometry", feature)
if g.geom_type not in ["Polygon", "MultiPolygon"]: if feature_geom is None:
# Fallback for rare cases where a feature does not contain any geometry
continue
feature_geom = json.dumps(feature_geom)
g = gdal.OGRGeometry(feature_geom, srs=DEFAULT_SRID_RLP)
flatten_geometry = g.coord_dim > 2
if flatten_geometry:
g = self.__flatten_geom_to_2D(g)
if g.geom_type not in accepted_ogr_types:
self.add_error("geom", _("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 is_valid = False
return is_valid return is_valid
@@ -88,6 +107,8 @@ class SimpleGeomForm(BaseForm):
return is_valid return is_valid
features.append(polygon) features.append(polygon)
# Unionize all geometry features into one new MultiPolygon
form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP) form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
for feature in features: for feature in features:
form_geom = form_geom.union(feature) form_geom = form_geom.union(feature)
@@ -128,6 +149,17 @@ class SimpleGeomForm(BaseForm):
geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)), geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)),
created=action, created=action,
) )
# Start the parcel update procedure in a background process # Start parcel update and geometry conflict checking procedure in a background process
celery_update_parcels.delay(geometry.id) celery_update_parcels.delay(geometry.id)
return geometry celery_check_for_geometry_conflicts.delay(geometry.id)
return geometry
def __flatten_geom_to_2D(self, geom):
"""
Enforces a given OGRGeometry from higher dimensions into 2D
"""
wkt_w = WKTWriter(dim=2)
g_wkt = wkt_w.write(geom.geos).decode("utf-8")
geom = gdal.OGRGeometry(g_wkt)
return geom

View File

@@ -83,3 +83,6 @@ class ResubmissionModalForm(BaseModalForm):
self.instance.resubmissions.add(self.resubmission) self.instance.resubmissions.add(self.resubmission)
return self.resubmission return self.resubmission
def check_for_recorded_instance(self):
# Ignore logic in super() implementation
return

View File

@@ -0,0 +1,137 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 26.10.22
"""
import zipfile
from io import BytesIO
from django.core.mail import EmailMessage
from django.utils import timezone
from django.utils.datetime_safe import datetime
from analysis.utils.excel.excel import TempExcelFile
from analysis.utils.report import TimespanReport
from codelist.models import KonovaCode
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID
from konova.management.commands.setup import BaseKonovaCommand
from konova.sub_settings.django_settings import DEFAULT_FROM_EMAIL, ADMINS
class Command(BaseKonovaCommand):
help = "Generates reports for conservation offices"
__from_key = "from"
__to_key = "to"
__for_key = "for"
from_date = None
to_date = None
offices = None
def add_arguments(self, parser):
try:
parser.add_argument(f"--{self.__from_key}", type=str, nargs="+")
parser.add_argument(f"--{self.__to_key}", type=str, nargs="+")
parser.add_argument(f"--{self.__for_key}", type=str, nargs="*")
except ValueError as e:
self._write_error(f"Argument error: {e}")
exit(-1)
def handle(self, *args, **options):
self.__check_arguments(options)
in_memory_zipped_reports = self.generate_reports_in_memory_zipped()
self.send_mail_to_admins(in_memory_zipped_reports)
def generate_reports_in_memory_zipped(self):
"""
Generates in-memory reports and zips into in-memory zip file.
Returns:
"""
memory_zip = BytesIO()
with zipfile.ZipFile(memory_zip, "w", compression=zipfile.ZIP_DEFLATED) as zf:
for office in self.offices:
self._write_warning(f" Process report for {office.long_name}...")
report = TimespanReport(office.id, self.from_date, self.to_date)
excel_file = TempExcelFile(report.excel_template_path, report.excel_map)
zf.writestr(zinfo_or_arcname=f"{office.long_name}_{self.from_date.date()}_{self.to_date.date()}.xlsx", data=excel_file.stream)
self._write_success(f"Reports generated for {self.offices.count()} offices and zipped.")
return memory_zip.getvalue()
def __check_arguments(self, options):
"""
Checks given parameters for validity
Args:
options ():
Returns:
"""
_from_value = options.get(self.__from_key, [None])[0]
_to_value = options.get(self.__to_key, [None])[0]
_for_value = options.get(self.__for_key, [])
# Check ISO dates
try:
_from_date = timezone.make_aware(datetime.fromisoformat(_from_value))
_to_date = timezone.make_aware(datetime.fromisoformat(_to_value))
except Exception as e:
self._write_warning(f"One of the dates is not in ISO format (YYYY-MM-DD). {e}")
exit(-1)
# Check conservation office IDs
_filter = {
"is_archived": False,
"is_leaf": True,
"code_lists__in": [CODELIST_CONSERVATION_OFFICE_ID],
}
offices = KonovaCode.objects.filter(**_filter)
if _for_value is not None and len(_for_value) != 0:
# Specifc offices requested
offices = offices.filter(short_name__in=_for_value)
all_requested_offices_exist = offices.count() == len(_for_value)
if not all_requested_offices_exist:
offices_short_name = set(offices.values_list("short_name", flat=True))
missing_ids = []
for val in _for_value:
if val not in offices_short_name:
missing_ids.append(val)
self._write_warning(
f"Unknown offices: {missing_ids}"
)
exit(-1)
self.offices = offices
self.from_date = _from_date
self.to_date = _to_date
def send_mail_to_admins(self, attachment):
office_names = [office.long_name for office in self.offices]
admin_mails = [admin[1] for admin in ADMINS]
date_from_date = self.from_date.date()
date_to_date = self.to_date.date()
mail = EmailMessage(
subject="Konova reports generated",
body=f"Zipped reports attached from {date_from_date} to {date_to_date} for {office_names}.",
from_email=DEFAULT_FROM_EMAIL,
to=admin_mails,
attachments=[
(
f"reports_{date_from_date}_{date_to_date}.zip",
attachment,
"application/zip"
)
]
)
success = mail.send()
if success == 1:
self._write_success(f"Mails with zip as attachment sent to {admin_mails}.")
else:
self._write_error(f"Something went wrong during mail sending. Returned sending code was '{success}'")

View File

@@ -8,13 +8,11 @@ Created on: 15.11.21
import json import json
from django.contrib.gis.db.models import MultiPolygonField from django.contrib.gis.db.models import MultiPolygonField
from django.contrib.gis.geos import Polygon
from django.db import models, transaction from django.db import models, transaction
from django.utils import timezone from django.utils import timezone
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
from konova.tasks import celery_check_for_geometry_conflicts
from konova.utils.wfs.spatial import ParcelWFSFetcher from konova.utils.wfs.spatial import ParcelWFSFetcher
@@ -29,7 +27,18 @@ class Geometry(BaseResource):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs) super().save(*args, **kwargs)
celery_check_for_geometry_conflicts.delay(self.id)
@property
def geom_small_buffered(self):
"""
Returns a smaller buffered version of the geometry.
Can be used to shrink the geometry used for intersection purposes to avoid intersection detection on
neighbouring geometries.
Returns:
"""
return self.geom.buffer(-0.001)
def check_for_conflicts(self): def check_for_conflicts(self):
""" Checks for new geometry overlaps """ Checks for new geometry overlaps
@@ -44,9 +53,8 @@ class Geometry(BaseResource):
return None return None
self.recheck_existing_conflicts() self.recheck_existing_conflicts()
overlapping_geoms = Geometry.objects.filter( overlapping_geoms = Geometry.objects.filter(
geom__intersects=self.geom, geom__intersects=self.geom_small_buffered,
).exclude( ).exclude(
id=self.id id=self.id
).distinct() ).distinct()
@@ -68,14 +76,14 @@ class Geometry(BaseResource):
""" """
all_conflicts_as_conflicting = self.conflicts_geometries.all() all_conflicts_as_conflicting = self.conflicts_geometries.all()
still_conflicting_conflicts = all_conflicts_as_conflicting.filter( still_conflicting_conflicts = all_conflicts_as_conflicting.filter(
affected_geometry__geom__intersects=self.geom affected_geometry__geom__intersects=self.geom_small_buffered
) )
resolved_conflicts = all_conflicts_as_conflicting.exclude(id__in=still_conflicting_conflicts) resolved_conflicts = all_conflicts_as_conflicting.exclude(id__in=still_conflicting_conflicts)
resolved_conflicts.delete() resolved_conflicts.delete()
all_conflicted_by_conflicts = self.conflicted_by_geometries.all() all_conflicted_by_conflicts = self.conflicted_by_geometries.all()
still_conflicting_conflicts = all_conflicted_by_conflicts.filter( still_conflicting_conflicts = all_conflicted_by_conflicts.filter(
conflicting_geometry__geom__intersects=self.geom conflicting_geometry__geom__intersects=self.geom_small_buffered
) )
resolved_conflicts = all_conflicted_by_conflicts.exclude(id__in=still_conflicting_conflicts) resolved_conflicts = all_conflicted_by_conflicts.exclude(id__in=still_conflicting_conflicts)
resolved_conflicts.delete() resolved_conflicts.delete()
@@ -108,6 +116,11 @@ class Geometry(BaseResource):
""" """
from konova.models import Parcel, District, ParcelIntersection, Municipal, ParcelGroup from konova.models import Parcel, District, ParcelIntersection, Municipal, ParcelGroup
if self.geom.empty:
# Nothing to do
return
parcel_fetcher = ParcelWFSFetcher( parcel_fetcher = ParcelWFSFetcher(
geometry_id=self.id, geometry_id=self.id,
) )
@@ -213,16 +226,22 @@ class Geometry(BaseResource):
geojson (dict): The FeatureCollection json (as dict) geojson (dict): The FeatureCollection json (as dict)
""" """
geom = self.geom geom = self.geom
geom.transform(ct=srid) if geom.srid != srid:
geom.transform(ct=srid)
polygons = []
for coords in geom.coords:
p = Polygon(coords[0], srid=geom.srid)
polygons.append(p)
geojson = { geojson = {
"type": "FeatureCollection", "type": "FeatureCollection",
"crs": {
"type": "name",
"properties": {
"name": f"urn:ogc:def:crs:EPSG::{geom.srid}"
}
},
"features": [ "features": [
json.loads(x.geojson) for x in polygons {
"type": "Feature",
"geometry": json.loads(geom.json),
}
] ]
} }
return geojson return geojson

View File

@@ -434,8 +434,16 @@ class CheckableObjectMixin(models.Model):
class ShareableObjectMixin(models.Model): class ShareableObjectMixin(models.Model):
# Users having access on this object # Users having access on this object
users = models.ManyToManyField("user.User", help_text="Users having access (data shared with)") users = models.ManyToManyField(
teams = models.ManyToManyField("user.Team", help_text="Teams having access (data shared with)") "user.User",
help_text="Users having access (data shared with)",
blank=True
)
teams = models.ManyToManyField(
"user.Team",
help_text="Teams having access (data shared with)",
blank=True
)
access_token = models.CharField( access_token = models.CharField(
max_length=255, max_length=255,
null=True, null=True,
@@ -698,18 +706,16 @@ class GeoReferencedMixin(models.Model):
return request return request
instance_objs = [] instance_objs = []
add_message = False
conflicts = self.geometry.conflicts_geometries.all() conflicts = self.geometry.conflicts_geometries.all()
for conflict in conflicts: for conflict in conflicts:
instance_objs += conflict.affected_geometry.get_data_objects() instance_objs += conflict.affected_geometry.get_data_objects()
add_message = True
conflicts = self.geometry.conflicted_by_geometries.all() conflicts = self.geometry.conflicted_by_geometries.all()
for conflict in conflicts: for conflict in conflicts:
instance_objs += conflict.conflicting_geometry.get_data_objects() instance_objs += conflict.conflicting_geometry.get_data_objects()
add_message = True
add_message = len(instance_objs) > 0
if add_message: if add_message:
instance_identifiers = [x.identifier for x in instance_objs] instance_identifiers = [x.identifier for x in instance_objs]
instance_identifiers = ", ".join(instance_identifiers) instance_identifiers = ", ".join(instance_identifiers)

View File

@@ -10,5 +10,5 @@ BASE_TITLE_SHORT = "KSP"
BASE_TITLE = "KSP - Kompensationsverzeichnis Service Portal" BASE_TITLE = "KSP - Kompensationsverzeichnis Service Portal"
BASE_FRONTEND_TITLE = "Kompensationsverzeichnis Service Portal" BASE_FRONTEND_TITLE = "Kompensationsverzeichnis Service Portal"
TAB_TITLE_IDENTIFIER = "tab_title" TAB_TITLE_IDENTIFIER = "tab_title"
HELP_LINK = "https://dienste.naturschutz.rlp.de/doku/doku.php?id=ksp:start" HELP_LINK = "https://dienste.naturschutz.rlp.de/doku/doku.php?id=ksp2:start"
IMPRESSUM_LINK = "https://naturschutz.rlp.de/index.php?q=impressum" IMPRESSUM_LINK = "https://naturschutz.rlp.de/index.php?q=impressum"

View File

@@ -15,7 +15,7 @@ DEFAULT_SRID_RLP = 25832
# Needed to redirect to LANIS # Needed to redirect to LANIS
## Values to be inserted are [zoom_level, x_coord, y_coord] ## Values to be inserted are [zoom_level, x_coord, y_coord]
LANIS_LINK_TEMPLATE = "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/index.php?lang=de&zl={}&x={}&y={}&bl=tk_rlp_tms_grau&bo=1&lo=0.8,0.8,0.8,0.6,0.8,0.8,0.8,0.8,0.8&layers=eiv_f,eiv_l,eiv_p,kom_f,kom_l,kom_p,oek_f,ema_f,mae&service=kartendienste_naturschutz" LANIS_LINK_TEMPLATE = "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/index.php?lang=de&zl={}&x={}&y={}&bl=tk_rlp_tms_grau&bo=1&lo=0.8,0.8,0.8,0.6,0.8,0.8,0.8,0.8,0.8&layers=eiv_recorded,eiv_unrecorded,kom_recorded,kom_unrecorded,oek_recorded,oek_unrecorded,ema_recorded,ema_unrecorded,mae&service=kartendienste_naturschutz"
## This look up table (LUT) defines different zoom levels on the size of the calculate area of a geometry. ## This look up table (LUT) defines different zoom levels on the size of the calculate area of a geometry.
LANIS_ZOOM_LUT = { LANIS_ZOOM_LUT = {
1000000000: 6, 1000000000: 6,

View File

@@ -1,7 +1,8 @@
{% load i18n l10n %} {% load i18n l10n fontawesome_5 %}
<div class="table-container w-100 scroll-300"> <div class="table-container w-100 scroll-300">
{% if parcels|length == 0 %} {% if parcels|length == 0 %}
<article class="alert alert-info"> <article class="alert alert-info">
{% fa5_icon 'search-location' %}
{% trans 'Parcels can not be calculated, since no geometry is given.' %} {% trans 'Parcels can not be calculated, since no geometry is given.' %}
</article> </article>
{% else %} {% else %}

View File

@@ -2,17 +2,32 @@
<div class="col-sm-12"> <div class="col-sm-12">
<div class="card"> <div class="card">
<div class="card-header rlp-r"> <div class="card-header rlp-r">
<h5> <div class="row">
{% fa5_icon 'search-location' %} <div class="col-6">
{% trans 'Spatial reference' %} <h5>
</h5> {% fa5_icon 'search-location' %}
{% trans 'Spatial reference' %}
</h5>
</div>
<div class="col-6 text-right">
<h5>
{{ obj.geometry.geom.area|floatformat:2 }} m²
</h5>
</div>
</div>
</div> </div>
<div class="card-body"> <div class="card-body">
{% if geom_form.instance.geometry %}
<div hx-trigger="load, every 5s" hx-get="{% url 'geometry-parcels' geom_form.instance.geometry.id %}"> <div hx-trigger="load, every 5s" hx-get="{% url 'geometry-parcels' geom_form.instance.geometry.id %}">
<div class="row justify-content-center"> <div class="row justify-content-center">
<span class="spinner-border rlp-r-inv" role="status"></span> <span class="spinner-border rlp-r-inv" role="status"></span>
</div> </div>
</div> </div>
{% else %}
<div class="alert alert-danger">
{% translate 'No geometry entry found on database. Please contact an admin!' %}
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -115,6 +115,19 @@ class BaseTestCase(TestCase):
""" """
return f"{prefix}{generate_random_string(3, True)}" return f"{prefix}{generate_random_string(3, True)}"
def create_dummy_document(self, DocumentModel, instance):
""" Creates a document db entry which can be used for tests
"""
doc = DocumentModel.objects.create(
title="TEST_doc",
comment="",
file=None,
date_of_creation="1970-01-01",
instance=instance,
)
return doc
def create_dummy_intervention(self): def create_dummy_intervention(self):
""" Creates an intervention which can be used for tests """ Creates an intervention which can be used for tests
@@ -187,6 +200,7 @@ class BaseTestCase(TestCase):
eco_account = EcoAccount.objects.create( eco_account = EcoAccount.objects.create(
identifier="TEST", identifier="TEST",
title="Test_title", title="Test_title",
deductable_surface=500,
legal=lega_data, legal=lega_data,
responsible=responsible_data, responsible=responsible_data,
created=action, created=action,

View File

@@ -92,11 +92,14 @@ class Mailer:
msg msg
) )
def send_mail_shared_access_given_team(self, obj_identifier, obj_title, team): def send_mail_shared_access_given_team(self, obj_identifier, obj_title, team, users_to_notify):
""" Send a mail if a team just got access to the object """ Send a mail if a team just got access to the object
Args: Args:
obj_identifier (str): The object identifier obj_identifier (str): The object identifier
obj_title (str): Title of the main object
team (Team): Team to be notified
users_to_notify (QueryDict): Contains the team users which should be notified
Returns: Returns:
@@ -108,18 +111,21 @@ class Mailer:
"EMAIL_REPLY_TO": EMAIL_REPLY_TO, "EMAIL_REPLY_TO": EMAIL_REPLY_TO,
} }
msg = render_to_string("email/sharing/shared_access_given_team.html", context) msg = render_to_string("email/sharing/shared_access_given_team.html", context)
user_mail_address = team.users.values_list("email", flat=True) user_mail_address = users_to_notify.values_list("email", flat=True)
self.send( self.send(
user_mail_address, user_mail_address,
_("{} - Shared access given").format(obj_identifier), _("{} - Shared access given").format(obj_identifier),
msg msg
) )
def send_mail_shared_access_removed_team(self, obj_identifier, obj_title, team): def send_mail_shared_access_removed_team(self, obj_identifier, obj_title, team, users_to_notify):
""" Send a mail if a team just lost access to the object """ Send a mail if a team just lost access to the object
Args: Args:
obj_identifier (str): The object identifier obj_identifier (str): The object identifier
obj_title (str): Title of the main object
team (Team): Team to be notified
users_to_notify (QueryDict): Contains the team users which should be notified
Returns: Returns:
@@ -131,18 +137,21 @@ class Mailer:
"EMAIL_REPLY_TO": EMAIL_REPLY_TO, "EMAIL_REPLY_TO": EMAIL_REPLY_TO,
} }
msg = render_to_string("email/sharing/shared_access_removed_team.html", context) msg = render_to_string("email/sharing/shared_access_removed_team.html", context)
user_mail_address = team.users.values_list("email", flat=True) user_mail_address = users_to_notify.values_list("email", flat=True)
self.send( self.send(
user_mail_address, user_mail_address,
_("{} - Shared access removed").format(obj_identifier), _("{} - Shared access removed").format(obj_identifier),
msg msg
) )
def send_mail_shared_data_unrecorded_team(self, obj_identifier, obj_title, team): def send_mail_shared_data_unrecorded_team(self, obj_identifier, obj_title, team, users_to_notify):
""" Send a mail if data has just been unrecorded """ Send a mail if data has just been unrecorded
Args: Args:
obj_identifier (str): The object identifier obj_identifier (str): The object identifier
obj_title (str): Title of the main object
team (Team): Team to be notified
users_to_notify (QueryDict): Contains the team users which should be notified
Returns: Returns:
@@ -154,18 +163,21 @@ class Mailer:
"EMAIL_REPLY_TO": EMAIL_REPLY_TO, "EMAIL_REPLY_TO": EMAIL_REPLY_TO,
} }
msg = render_to_string("email/recording/shared_data_unrecorded_team.html", context) msg = render_to_string("email/recording/shared_data_unrecorded_team.html", context)
user_mail_address = team.users.values_list("email", flat=True) user_mail_address = users_to_notify.values_list("email", flat=True)
self.send( self.send(
user_mail_address, user_mail_address,
_("{} - Shared data unrecorded").format(obj_identifier), _("{} - Shared data unrecorded").format(obj_identifier),
msg msg
) )
def send_mail_shared_data_recorded_team(self, obj_identifier, obj_title, team): def send_mail_shared_data_recorded_team(self, obj_identifier, obj_title, team, users_to_notify):
""" Send a mail if data has just been recorded """ Send a mail if data has just been recorded
Args: Args:
obj_identifier (str): The object identifier obj_identifier (str): The object identifier
obj_title (str): Title of the main object
team (Team): Team to be notified
users_to_notify (QueryDict): Contains the team users which should be notified
Returns: Returns:
@@ -177,18 +189,21 @@ class Mailer:
"EMAIL_REPLY_TO": EMAIL_REPLY_TO, "EMAIL_REPLY_TO": EMAIL_REPLY_TO,
} }
msg = render_to_string("email/recording/shared_data_recorded_team.html", context) msg = render_to_string("email/recording/shared_data_recorded_team.html", context)
user_mail_address = team.users.values_list("email", flat=True) user_mail_address = users_to_notify.values_list("email", flat=True)
self.send( self.send(
user_mail_address, user_mail_address,
_("{} - Shared data recorded").format(obj_identifier), _("{} - Shared data recorded").format(obj_identifier),
msg msg
) )
def send_mail_shared_data_checked_team(self, obj_identifier, obj_title, team): def send_mail_shared_data_checked_team(self, obj_identifier, obj_title, team, users_to_notify):
""" Send a mail if data has just been checked """ Send a mail if data has just been checked
Args: Args:
obj_identifier (str): The object identifier obj_identifier (str): The object identifier
obj_title (str): Title of the main object
team (Team): Team to be notified
users_to_notify (QueryDict): Contains the team users which should be notified
Returns: Returns:
@@ -200,14 +215,14 @@ class Mailer:
"EMAIL_REPLY_TO": EMAIL_REPLY_TO, "EMAIL_REPLY_TO": EMAIL_REPLY_TO,
} }
msg = render_to_string("email/checking/shared_data_checked_team.html", context) msg = render_to_string("email/checking/shared_data_checked_team.html", context)
user_mail_address = team.users.values_list("email", flat=True) user_mail_address = users_to_notify.values_list("email", flat=True)
self.send( self.send(
user_mail_address, user_mail_address,
_("{} - Shared data checked").format(obj_identifier), _("{} - Shared data checked").format(obj_identifier),
msg msg
) )
def send_mail_deduction_changed_team(self, obj_identifier, obj_title, team, data_changes): def send_mail_deduction_changed_team(self, obj_identifier, obj_title, team, data_changes, users_to_notify):
""" Send a mail if deduction has been changed """ Send a mail if deduction has been changed
Args: Args:
@@ -215,7 +230,7 @@ class Mailer:
obj_title (str): Title of the main object obj_title (str): Title of the main object
team (Team): Team to be notified team (Team): Team to be notified
data_changes (dict): Contains the old|new changes of the deduction changes data_changes (dict): Contains the old|new changes of the deduction changes
users_to_notify (QueryDict): Contains the team users which should be notified
Returns: Returns:
""" """
@@ -227,14 +242,14 @@ class Mailer:
"data_changes": data_changes, "data_changes": data_changes,
} }
msg = render_to_string("email/other/deduction_changed_team.html", context) msg = render_to_string("email/other/deduction_changed_team.html", context)
user_mail_address = team.users.values_list("email", flat=True) user_mail_address = users_to_notify.values_list("email", flat=True)
self.send( self.send(
user_mail_address, user_mail_address,
_("{} - Deduction changed").format(obj_identifier), _("{} - Deduction changed").format(obj_identifier),
msg msg
) )
def send_mail_shared_data_deleted_team(self, obj_identifier, obj_title, team): def send_mail_shared_data_deleted_team(self, obj_identifier, obj_title, team, users_to_notify):
""" Send a mail if data has just been deleted """ Send a mail if data has just been deleted
Args: Args:
@@ -250,7 +265,7 @@ class Mailer:
"EMAIL_REPLY_TO": EMAIL_REPLY_TO, "EMAIL_REPLY_TO": EMAIL_REPLY_TO,
} }
msg = render_to_string("email/deleting/shared_data_deleted_team.html", context) msg = render_to_string("email/deleting/shared_data_deleted_team.html", context)
user_mail_address = team.users.values_list("email", flat=True) user_mail_address = users_to_notify.values_list("email", flat=True)
self.send( self.send(
user_mail_address, user_mail_address,
_("{} - Shared data deleted").format(obj_identifier), _("{} - Shared data deleted").format(obj_identifier),

View File

@@ -7,11 +7,14 @@ Created on: 25.11.20
""" """
from django.core.paginator import PageNotAnInteger, EmptyPage from django.core.paginator import PageNotAnInteger, EmptyPage
from django.db.models import F
from django.http import HttpRequest from django.http import HttpRequest
from django.template.loader import render_to_string
from django.utils.html import format_html from django.utils.html import format_html
import django_tables2 as tables import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from konova.models import BaseObject from konova.models import BaseObject, GeoReferencedMixin, ShareableObjectMixin
from konova.settings import PAGE_SIZE_DEFAULT, PAGE_PARAM, RESULTS_PER_PAGE_PARAM, PAGE_SIZE_OPTIONS from konova.settings import PAGE_SIZE_DEFAULT, PAGE_PARAM, RESULTS_PER_PAGE_PARAM, PAGE_SIZE_OPTIONS
@@ -173,4 +176,56 @@ class TableRenderMixin:
max_length = 75 max_length = 75
if len(value) > max_length: if len(value) > max_length:
value = f"{value[:max_length]}..." value = f"{value[:max_length]}..."
return value return value
def render_d(self, value, record: GeoReferencedMixin):
""" Renders the parcel district column
Args:
value (str): The intervention geometry
record (GeoReferencedMixin): The record
Returns:
"""
parcels = value.get_underlying_parcels().values_list(
"parcel_group__name",
flat=True
).distinct()
html = render_to_string(
"table/gmrkng_col.html",
{
"entries": parcels,
"geometry": record.geometry
}
)
return html
def render_e(self, value, record: ShareableObjectMixin):
""" Renders the editable column
Args:
value (str): The identifier value
record (ShareableObjectMixin): The record
Returns:
"""
html = ""
has_access = record.is_shared_with(self.user)
html += self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),
icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit",
)
return format_html(html)
class TableOrderMixin:
"""
Holds different order_by methods for general purposes
"""
def order_lm(self, queryset, is_asc):
queryset = queryset.order_by(F('modified__timestamp').desc(nulls_last=True))
return (queryset, is_asc)

View File

@@ -11,10 +11,11 @@ from json import JSONDecodeError
from time import sleep from time import sleep
import requests import requests
from django.contrib.gis.db.models.functions import AsGML, Transform, MakeValid from django.contrib.gis.db.models.functions import AsGML, MakeValid
from django.db.models import Func, F
from requests.auth import HTTPDigestAuth from requests.auth import HTTPDigestAuth
from konova.settings import DEFAULT_SRID_RLP, PARCEL_WFS_USER, PARCEL_WFS_PW, PROXIES from konova.settings import PARCEL_WFS_USER, PARCEL_WFS_PW, PROXIES
class AbstractWFSFetcher: class AbstractWFSFetcher:
@@ -72,33 +73,34 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
self.geometry_property_name = geometry_property_name self.geometry_property_name = geometry_property_name
def _create_spatial_filter(self, def _create_spatial_filter(self,
geometry_operation: str, geometry_operation: str):
filter_srid: str = None):
""" Creates a xml spatial filter according to the WFS filter specification """ Creates a xml spatial filter according to the WFS filter specification
The geometry needs to be shrinked by a very small factor (-0.01) before a GML can be created for intersection
checking. Otherwise perfect parcel outline placement on top of a neighbouring parcel would result in an
intersection hit, despite the fact they do not truly intersect just because their vertices match.
Args: Args:
geometry_operation (str): One of the WFS supported spatial filter operations (according to capabilities) geometry_operation (str): One of the WFS supported spatial filter operations (according to capabilities)
filter_srid (str): Used to transform the geometry into the spatial reference system identified by this srid
Returns: Returns:
spatial_filter (str): The spatial filter element spatial_filter (str): The spatial filter element
""" """
from konova.models import Geometry from konova.models import Geometry
if filter_srid is None:
filter_srid = DEFAULT_SRID_RLP geom = Geometry.objects.filter(
geom_gml = Geometry.objects.filter(
id=self.geometry_id id=self.geometry_id
).annotate( ).annotate(
transformed=Transform(srid=filter_srid, expression="geom") smaller=Func(F('geom'), -0.001, function="ST_Buffer") # same as geometry.geom_small_buffered but for QuerySet
).annotate( ).annotate(
gml=AsGML(MakeValid('transformed')) gml=AsGML(MakeValid('smaller'))
).first().gml ).first()
geom_gml = geom.gml
spatial_filter = f"<Filter><{geometry_operation}><PropertyName>{self.geometry_property_name}</PropertyName>{geom_gml}</{geometry_operation}></Filter>" spatial_filter = f"<Filter><{geometry_operation}><PropertyName>{self.geometry_property_name}</PropertyName>{geom_gml}</{geometry_operation}></Filter>"
return spatial_filter return spatial_filter
def _create_post_data(self, def _create_post_data(self,
geometry_operation: str, geometry_operation: str,
filter_srid: str = None,
typenames: str = None, typenames: str = None,
start_index: int = 0, start_index: int = 0,
): ):
@@ -106,15 +108,13 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
Args: Args:
geometry_operation (str): One of the WFS supported spatial filter operations (according to capabilities) geometry_operation (str): One of the WFS supported spatial filter operations (according to capabilities)
filter_srid (str): Used to transform the geometry into the spatial reference system identified by this srid
Returns: Returns:
_filter (str): A proper xml WFS filter _filter (str): A proper xml WFS filter
""" """
start_index = str(start_index) start_index = str(start_index)
spatial_filter = self._create_spatial_filter( spatial_filter = self._create_spatial_filter(
geometry_operation, geometry_operation
filter_srid
) )
_filter = f'<wfs:GetFeature service="WFS" version="{self.version}" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:fes="http://www.opengis.net/fes/2.0" xmlns:myns="http://www.someserver.com/myns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0.0/wfs.xsd" count="{self.count}" startindex="{start_index}" outputFormat="application/json; subtype=geojson"><wfs:Query typeNames="{typenames}">{spatial_filter}</wfs:Query></wfs:GetFeature>' _filter = f'<wfs:GetFeature service="WFS" version="{self.version}" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:fes="http://www.opengis.net/fes/2.0" xmlns:myns="http://www.someserver.com/myns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0.0/wfs.xsd" count="{self.count}" startindex="{start_index}" outputFormat="application/json; subtype=geojson"><wfs:Query typeNames="{typenames}">{spatial_filter}</wfs:Query></wfs:GetFeature>'
return _filter return _filter
@@ -144,7 +144,6 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
while start_index is not None: while start_index is not None:
post_body = self._create_post_data( post_body = self._create_post_data(
spatial_operator, spatial_operator,
filter_srid,
typenames, typenames,
start_index start_index
) )

View File

@@ -32,16 +32,16 @@ def get_geom_parcels(request: HttpRequest, id: str):
parcels = geom.get_underlying_parcels() parcels = geom.get_underlying_parcels()
geos_geom = geom.geom geos_geom = geom.geom
parcels_are_currently_calculated = geos_geom is not None and geos_geom.area > 0 and len(parcels) == 0 geometry_exists = not geos_geom.empty
parcels_are_currently_calculated = geometry_exists and geos_geom.area > 0 and len(parcels) == 0
parcels_available = len(parcels) > 0 parcels_available = len(parcels) > 0
no_geometry_given = geos_geom is None
if parcels_are_currently_calculated: if parcels_are_currently_calculated:
# Parcels are being calculated right now. Change the status code, so polling stays active for fetching # Parcels are being calculated right now. Change the status code, so polling stays active for fetching
# resutls after the calculation # resutls after the calculation
status_code = 200 status_code = 200
if parcels_available or no_geometry_given: if parcels_available or not geometry_exists:
parcels = parcels.order_by("-municipal", "flr", "flrstck_zhlr", "flrstck_nnr") parcels = parcels.order_by("-municipal", "flr", "flrstck_zhlr", "flrstck_nnr")
municipals = parcels.order_by("municipal").distinct("municipal").values("municipal__id") municipals = parcels.order_by("municipal").distinct("municipal").values("municipal__id")
municipals = Municipal.objects.filter(id__in=municipals) municipals = Municipal.objects.filter(id__in=municipals)

View File

@@ -6,6 +6,7 @@ Created on: 19.08.22
""" """
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db.models import Q
from django.http import HttpRequest from django.http import HttpRequest
from django.shortcuts import render from django.shortcuts import render
from django.utils import timezone from django.utils import timezone
@@ -50,22 +51,22 @@ def home_view(request: HttpRequest):
) )
# Then fetch only user related ones # Then fetch only user related ones
user_interventions = interventions.filter( user_interventions = interventions.filter(
users__in=[user] Q(users__in=[user]) | Q(teams__in=user.shared_teams)
) ).distinct()
# Repeat for other objects # Repeat for other objects
comps = Compensation.objects.filter( comps = Compensation.objects.filter(
deleted=None, deleted=None,
) )
user_comps = comps.filter( user_comps = comps.filter(
intervention__users__in=[user] Q(intervention__users__in=[user]) | Q(intervention__teams__in=user.shared_teams)
) ).distinct()
eco_accs = EcoAccount.objects.filter( eco_accs = EcoAccount.objects.filter(
deleted=None, deleted=None,
) )
user_ecco_accs = eco_accs.filter( user_ecco_accs = eco_accs.filter(
users__in=[user] Q(users__in=[user]) | Q(teams__in=user.shared_teams)
) ).distinct()
additional_context = { additional_context = {
"msgs": msgs, "msgs": msgs,

Binary file not shown.

View File

@@ -43,7 +43,7 @@ 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: 2022-08-25 10:57+0200\n" "POT-Creation-Date: 2022-11-16 13:36+0100\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"
@@ -204,7 +204,7 @@ msgstr "Geprüft"
#: compensation/templates/compensation/detail/compensation/view.html:93 #: compensation/templates/compensation/detail/compensation/view.html:93
#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:31 #: compensation/templates/compensation/detail/eco_account/includes/deductions.html:31
#: compensation/templates/compensation/detail/eco_account/view.html:45 #: compensation/templates/compensation/detail/eco_account/view.html:45
#: ema/tables.py:44 ema/templates/ema/detail/view.html:35 #: ema/tables.py:41 ema/templates/ema/detail/view.html:35
#: intervention/tables.py:44 #: intervention/tables.py:44
#: intervention/templates/intervention/detail/view.html:87 #: intervention/templates/intervention/detail/view.html:87
#: user/models/user_action.py:22 #: user/models/user_action.py:22
@@ -357,7 +357,7 @@ msgid "Show only unrecorded"
msgstr "Nur unverzeichnete anzeigen" msgstr "Nur unverzeichnete anzeigen"
#: compensation/forms/compensation.py:30 compensation/tables/compensation.py:23 #: compensation/forms/compensation.py:30 compensation/tables/compensation.py:23
#: compensation/tables/eco_account.py:23 ema/tables.py:29 #: compensation/tables/eco_account.py:23 ema/tables.py:26
#: intervention/forms/intervention.py:29 intervention/tables.py:23 #: intervention/forms/intervention.py:29 intervention/tables.py:23
#: intervention/templates/intervention/detail/includes/compensations.html:30 #: intervention/templates/intervention/detail/includes/compensations.html:30
msgid "Identifier" msgid "Identifier"
@@ -376,12 +376,12 @@ msgstr "Automatisch generiert"
#: compensation/templates/compensation/detail/eco_account/view.html:32 #: compensation/templates/compensation/detail/eco_account/view.html:32
#: compensation/templates/compensation/report/compensation/report.html:12 #: compensation/templates/compensation/report/compensation/report.html:12
#: compensation/templates/compensation/report/eco_account/report.html:12 #: compensation/templates/compensation/report/eco_account/report.html:12
#: ema/tables.py:34 ema/templates/ema/detail/includes/documents.html:28 #: ema/tables.py:31 ema/templates/ema/detail/includes/documents.html:28
#: ema/templates/ema/detail/view.html:31 #: ema/templates/ema/detail/view.html:31
#: ema/templates/ema/report/report.html:12 #: ema/templates/ema/report/report.html:12
#: intervention/forms/intervention.py:41 intervention/tables.py:28 #: intervention/forms/intervention.py:41 intervention/tables.py:28
#: intervention/templates/intervention/detail/includes/compensations.html:33 #: intervention/templates/intervention/detail/includes/compensations.html:33
#: intervention/templates/intervention/detail/includes/documents.html:28 #: intervention/templates/intervention/detail/includes/documents.html:33
#: intervention/templates/intervention/detail/view.html:31 #: intervention/templates/intervention/detail/view.html:31
#: intervention/templates/intervention/report/report.html:12 #: intervention/templates/intervention/report/report.html:12
#: konova/forms/modals/document_form.py:24 #: konova/forms/modals/document_form.py:24
@@ -411,7 +411,7 @@ msgstr "Kompensation XY; Flur ABC"
#: ema/templates/ema/detail/includes/documents.html:34 #: ema/templates/ema/detail/includes/documents.html:34
#: intervention/forms/intervention.py:199 #: intervention/forms/intervention.py:199
#: intervention/forms/modals/revocation.py:45 #: intervention/forms/modals/revocation.py:45
#: intervention/templates/intervention/detail/includes/documents.html:34 #: intervention/templates/intervention/detail/includes/documents.html:39
#: intervention/templates/intervention/detail/includes/payments.html:34 #: intervention/templates/intervention/detail/includes/payments.html:34
#: intervention/templates/intervention/detail/includes/revocation.html:38 #: intervention/templates/intervention/detail/includes/revocation.html:38
#: konova/forms/modals/document_form.py:59 #: konova/forms/modals/document_form.py:59
@@ -446,37 +446,43 @@ msgstr "Neue Kompensation"
msgid "Edit compensation" msgid "Edit compensation"
msgstr "Bearbeite Kompensation" msgstr "Bearbeite Kompensation"
#: compensation/forms/eco_account.py:29 compensation/utils/quality.py:95 #: compensation/forms/eco_account.py:30 compensation/utils/quality.py:95
msgid "Available Surface" msgid "Available Surface"
msgstr "Verfügbare Fläche" msgstr "Verfügbare Fläche"
#: compensation/forms/eco_account.py:32 #: compensation/forms/eco_account.py:33
msgid "The amount that can be used for deductions" msgid "The amount that can be used for deductions"
msgstr "Die für Abbuchungen zur Verfügung stehende Menge" msgstr "Die für Abbuchungen zur Verfügung stehende Menge"
#: compensation/forms/eco_account.py:41 #: compensation/forms/eco_account.py:42
#: compensation/templates/compensation/detail/eco_account/view.html:67 #: compensation/templates/compensation/detail/eco_account/view.html:67
#: compensation/utils/quality.py:83 #: compensation/utils/quality.py:83
msgid "Agreement date" msgid "Agreement date"
msgstr "Vereinbarungsdatum" msgstr "Vereinbarungsdatum"
#: compensation/forms/eco_account.py:43 #: compensation/forms/eco_account.py:44
msgid "When did the parties agree on this?" msgid "When did the parties agree on this?"
msgstr "Wann wurde dieses Ökokonto offiziell vereinbart?" msgstr "Wann wurde dieses Ökokonto offiziell vereinbart?"
#: compensation/forms/eco_account.py:69 #: compensation/forms/eco_account.py:70
#: compensation/views/eco_account/eco_account.py:94 #: compensation/views/eco_account/eco_account.py:94
msgid "New Eco-Account" msgid "New Eco-Account"
msgstr "Neues Ökokonto" msgstr "Neues Ökokonto"
#: compensation/forms/eco_account.py:78 #: compensation/forms/eco_account.py:79
msgid "Eco-Account XY; Location ABC" msgid "Eco-Account XY; Location ABC"
msgstr "Ökokonto XY; Flur ABC" msgstr "Ökokonto XY; Flur ABC"
#: compensation/forms/eco_account.py:140 #: compensation/forms/eco_account.py:143
msgid "Edit Eco-Account" msgid "Edit Eco-Account"
msgstr "Ökokonto bearbeiten" msgstr "Ökokonto bearbeiten"
#: compensation/forms/eco_account.py:228
msgid "The account can not be removed, since there are still deductions."
msgstr ""
"Das Ökokonto kann nicht entfernt werden, da hierzu noch Abbuchungen "
"vorliegen."
#: compensation/forms/mixins.py:37 #: compensation/forms/mixins.py:37
#: compensation/templates/compensation/detail/eco_account/view.html:63 #: compensation/templates/compensation/detail/eco_account/view.html:63
#: compensation/templates/compensation/report/eco_account/report.html:20 #: compensation/templates/compensation/report/eco_account/report.html:20
@@ -734,14 +740,14 @@ msgstr ""
msgid "Pieces" msgid "Pieces"
msgstr "Stück" msgstr "Stück"
#: compensation/models/eco_account.py:56 #: compensation/models/eco_account.py:62
msgid "" msgid ""
"Deductable surface can not be larger than existing surfaces in after states" "Deductable surface can not be larger than existing surfaces in after states"
msgstr "" msgstr ""
"Die abbuchbare Fläche darf die Gesamtfläche der Zielzustände nicht " "Die abbuchbare Fläche darf die Gesamtfläche der Zielzustände nicht "
"überschreiten" "überschreiten"
#: compensation/models/eco_account.py:63 #: compensation/models/eco_account.py:69
msgid "" msgid ""
"Deductable surface can not be smaller than the sum of already existing " "Deductable surface can not be smaller than the sum of already existing "
"deductions. Please contact the responsible users for the deductions!" "deductions. Please contact the responsible users for the deductions!"
@@ -750,54 +756,42 @@ msgstr ""
"wollen. Kontaktieren Sie die für die Abbuchungen verantwortlichen Nutzer!" "wollen. Kontaktieren Sie die für die Abbuchungen verantwortlichen Nutzer!"
#: compensation/tables/compensation.py:33 compensation/tables/eco_account.py:33 #: compensation/tables/compensation.py:33 compensation/tables/eco_account.py:33
#: ema/tables.py:39 intervention/tables.py:33 #: ema/tables.py:36 intervention/tables.py:33
#: konova/filters/mixins/geo_reference.py:42 #: konova/filters/mixins/geo_reference.py:42
msgid "Parcel gmrkng" msgid "Parcel gmrkng"
msgstr "Gemarkung" msgstr "Gemarkung"
#: compensation/tables/compensation.py:50 compensation/tables/eco_account.py:54 #: compensation/tables/compensation.py:50 compensation/tables/eco_account.py:54
#: ema/tables.py:50 intervention/tables.py:50 #: ema/tables.py:47 intervention/tables.py:50
msgid "Editable" msgid "Editable"
msgstr "Freigegeben" msgstr "Freigegeben"
#: compensation/tables/compensation.py:56 compensation/tables/eco_account.py:60 #: compensation/tables/compensation.py:56 compensation/tables/eco_account.py:60
#: ema/tables.py:56 intervention/tables.py:56 #: ema/tables.py:53 intervention/tables.py:56
msgid "Last edit" msgid "Last edit"
msgstr "Zuletzt bearbeitet" msgstr "Zuletzt bearbeitet"
#: compensation/tables/compensation.py:87 compensation/tables/eco_account.py:92 #: compensation/tables/compensation.py:87 compensation/tables/eco_account.py:92
#: ema/tables.py:89 intervention/tables.py:87 #: ema/tables.py:86 intervention/tables.py:87
msgid "Open {}" msgid "Open {}"
msgstr "Öffne {}" msgstr "Öffne {}"
#: compensation/tables/compensation.py:163 #: compensation/tables/compensation.py:141
#: compensation/templates/compensation/detail/compensation/view.html:96 #: compensation/templates/compensation/detail/compensation/view.html:96
#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:58 #: compensation/templates/compensation/detail/eco_account/includes/deductions.html:58
#: compensation/templates/compensation/detail/eco_account/view.html:48 #: compensation/templates/compensation/detail/eco_account/view.html:48
#: ema/tables.py:130 ema/templates/ema/detail/view.html:38 #: ema/tables.py:105 ema/templates/ema/detail/view.html:38
#: intervention/tables.py:161 #: intervention/tables.py:139
#: intervention/templates/intervention/detail/view.html:90 #: intervention/templates/intervention/detail/view.html:90
msgid "Not recorded yet" msgid "Not recorded yet"
msgstr "Noch nicht verzeichnet" msgstr "Noch nicht verzeichnet"
#: compensation/tables/compensation.py:166 #: compensation/tables/compensation.py:144
#: compensation/tables/eco_account.py:150 ema/tables.py:133 #: compensation/tables/eco_account.py:131 ema/tables.py:108
#: intervention/tables.py:164 #: intervention/tables.py:142
msgid "Recorded on {} by {}" msgid "Recorded on {} by {}"
msgstr "Am {} von {} verzeichnet worden" msgstr "Am {} von {} verzeichnet worden"
#: compensation/tables/compensation.py:186
#: compensation/tables/eco_account.py:172 ema/tables.py:154
#: intervention/tables.py:185
msgid "Full access granted"
msgstr "Für Sie freigegeben - Datensatz kann bearbeitet werden"
#: compensation/tables/compensation.py:186
#: compensation/tables/eco_account.py:172 ema/tables.py:154
#: intervention/tables.py:185
msgid "Access not granted"
msgstr "Nicht freigegeben - Datensatz nur lesbar"
#: compensation/tables/eco_account.py:38 #: compensation/tables/eco_account.py:38
#: compensation/templates/compensation/detail/eco_account/view.html:36 #: compensation/templates/compensation/detail/eco_account/view.html:36
#: konova/templates/konova/widgets/progressbar.html:3 #: konova/templates/konova/widgets/progressbar.html:3
@@ -808,7 +802,7 @@ msgstr "Verfügbar"
msgid "Eco Accounts" msgid "Eco Accounts"
msgstr "Ökokonten" msgstr "Ökokonten"
#: compensation/tables/eco_account.py:147 #: compensation/tables/eco_account.py:128
msgid "Not recorded yet. Can not be used for deductions, yet." msgid "Not recorded yet. Can not be used for deductions, yet."
msgstr "" msgstr ""
"Noch nicht verzeichnet. Kann noch nicht für Abbuchungen genutzt werden." "Noch nicht verzeichnet. Kann noch nicht für Abbuchungen genutzt werden."
@@ -857,7 +851,7 @@ msgstr "Menge"
#: ema/templates/ema/detail/includes/states-before.html:40 #: ema/templates/ema/detail/includes/states-before.html:40
#: intervention/templates/intervention/detail/includes/compensations.html:38 #: intervention/templates/intervention/detail/includes/compensations.html:38
#: intervention/templates/intervention/detail/includes/deductions.html:39 #: intervention/templates/intervention/detail/includes/deductions.html:39
#: intervention/templates/intervention/detail/includes/documents.html:39 #: intervention/templates/intervention/detail/includes/documents.html:44
#: intervention/templates/intervention/detail/includes/payments.html:39 #: intervention/templates/intervention/detail/includes/payments.html:39
#: intervention/templates/intervention/detail/includes/revocation.html:43 #: intervention/templates/intervention/detail/includes/revocation.html:43
#: templates/log.html:10 user/templates/user/team/index.html:33 #: templates/log.html:10 user/templates/user/team/index.html:33
@@ -876,6 +870,34 @@ msgstr "Keine Zusatzmerkmale"
msgid "Remove action" msgid "Remove action"
msgstr "Maßnahme entfernen" msgstr "Maßnahme entfernen"
#: compensation/templates/compensation/detail/compensation/includes/actions.html:79
#: compensation/templates/compensation/detail/compensation/includes/states-after.html:73
#: compensation/templates/compensation/detail/compensation/includes/states-before.html:73
#: compensation/templates/compensation/detail/eco_account/includes/actions.html:78
#: compensation/templates/compensation/detail/eco_account/includes/states-after.html:73
#: compensation/templates/compensation/detail/eco_account/includes/states-before.html:73
#: compensation/templates/compensation/detail/eco_account/view.html:58
#: compensation/templates/compensation/detail/eco_account/view.html:62
#: compensation/templates/compensation/detail/eco_account/view.html:66
#: compensation/templates/compensation/detail/eco_account/view.html:70
#: ema/templates/ema/detail/includes/actions.html:76
#: ema/templates/ema/detail/includes/states-after.html:71
#: ema/templates/ema/detail/includes/states-before.html:71
#: ema/templates/ema/detail/view.html:48 ema/templates/ema/detail/view.html:52
#: ema/templates/ema/detail/view.html:56
#: intervention/templates/intervention/detail/view.html:30
#: intervention/templates/intervention/detail/view.html:34
#: intervention/templates/intervention/detail/view.html:38
#: intervention/templates/intervention/detail/view.html:47
#: intervention/templates/intervention/detail/view.html:51
#: intervention/templates/intervention/detail/view.html:55
#: intervention/templates/intervention/detail/view.html:59
#: intervention/templates/intervention/detail/view.html:63
#: intervention/templates/intervention/detail/view.html:100
#: intervention/templates/intervention/detail/view.html:104
msgid "Missing"
msgstr "Fehlend"
#: compensation/templates/compensation/detail/compensation/includes/controls.html:5 #: compensation/templates/compensation/detail/compensation/includes/controls.html:5
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:5 #: compensation/templates/compensation/detail/eco_account/includes/controls.html:5
#: ema/templates/ema/detail/includes/controls.html:5 #: ema/templates/ema/detail/includes/controls.html:5
@@ -963,7 +985,7 @@ msgstr "Neues Dokument hinzufügen"
#: compensation/templates/compensation/detail/compensation/includes/documents.html:31 #: compensation/templates/compensation/detail/compensation/includes/documents.html:31
#: compensation/templates/compensation/detail/eco_account/includes/documents.html:31 #: compensation/templates/compensation/detail/eco_account/includes/documents.html:31
#: ema/templates/ema/detail/includes/documents.html:31 #: ema/templates/ema/detail/includes/documents.html:31
#: intervention/templates/intervention/detail/includes/documents.html:31 #: intervention/templates/intervention/detail/includes/documents.html:36
#: konova/forms/modals/document_form.py:34 #: konova/forms/modals/document_form.py:34
msgid "Created on" msgid "Created on"
msgstr "Erstellt" msgstr "Erstellt"
@@ -971,7 +993,7 @@ msgstr "Erstellt"
#: compensation/templates/compensation/detail/compensation/includes/documents.html:63 #: compensation/templates/compensation/detail/compensation/includes/documents.html:63
#: compensation/templates/compensation/detail/eco_account/includes/documents.html:61 #: compensation/templates/compensation/detail/eco_account/includes/documents.html:61
#: ema/templates/ema/detail/includes/documents.html:61 #: ema/templates/ema/detail/includes/documents.html:61
#: intervention/templates/intervention/detail/includes/documents.html:65 #: intervention/templates/intervention/detail/includes/documents.html:70
#: konova/forms/modals/document_form.py:139 #: konova/forms/modals/document_form.py:139
msgid "Edit document" msgid "Edit document"
msgstr "Dokument bearbeiten" msgstr "Dokument bearbeiten"
@@ -979,7 +1001,7 @@ msgstr "Dokument bearbeiten"
#: compensation/templates/compensation/detail/compensation/includes/documents.html:66 #: compensation/templates/compensation/detail/compensation/includes/documents.html:66
#: compensation/templates/compensation/detail/eco_account/includes/documents.html:64 #: compensation/templates/compensation/detail/eco_account/includes/documents.html:64
#: ema/templates/ema/detail/includes/documents.html:64 #: ema/templates/ema/detail/includes/documents.html:64
#: intervention/templates/intervention/detail/includes/documents.html:68 #: intervention/templates/intervention/detail/includes/documents.html:73
msgid "Remove document" msgid "Remove document"
msgstr "Dokument löschen" msgstr "Dokument löschen"
@@ -1186,7 +1208,7 @@ msgid "Recorded on"
msgstr "Verzeichnet am" msgstr "Verzeichnet am"
#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:65 #: compensation/templates/compensation/detail/eco_account/includes/deductions.html:65
#: intervention/forms/modals/deduction.py:173 #: intervention/forms/modals/deduction.py:177
#: intervention/templates/intervention/detail/includes/deductions.html:60 #: intervention/templates/intervention/detail/includes/deductions.html:60
msgid "Edit Deduction" msgid "Edit Deduction"
msgstr "Abbuchung bearbeiten" msgstr "Abbuchung bearbeiten"
@@ -1200,25 +1222,6 @@ msgstr "Abbuchung entfernen"
msgid "No surface deductable" msgid "No surface deductable"
msgstr "Keine Flächenmenge für Abbuchungen eingegeben. Bitte bearbeiten." msgstr "Keine Flächenmenge für Abbuchungen eingegeben. Bitte bearbeiten."
#: compensation/templates/compensation/detail/eco_account/view.html:58
#: compensation/templates/compensation/detail/eco_account/view.html:62
#: compensation/templates/compensation/detail/eco_account/view.html:66
#: compensation/templates/compensation/detail/eco_account/view.html:70
#: ema/templates/ema/detail/view.html:48 ema/templates/ema/detail/view.html:52
#: ema/templates/ema/detail/view.html:56
#: intervention/templates/intervention/detail/view.html:30
#: intervention/templates/intervention/detail/view.html:34
#: intervention/templates/intervention/detail/view.html:38
#: intervention/templates/intervention/detail/view.html:47
#: intervention/templates/intervention/detail/view.html:51
#: intervention/templates/intervention/detail/view.html:55
#: intervention/templates/intervention/detail/view.html:59
#: intervention/templates/intervention/detail/view.html:63
#: intervention/templates/intervention/detail/view.html:100
#: intervention/templates/intervention/detail/view.html:104
msgid "Missing"
msgstr "fehlt"
#: compensation/templates/compensation/detail/eco_account/view.html:71 #: compensation/templates/compensation/detail/eco_account/view.html:71
#: ema/templates/ema/detail/view.html:57 #: ema/templates/ema/detail/view.html:57
msgid "Action handler" msgid "Action handler"
@@ -1274,8 +1277,8 @@ msgid "Compensation {} edited"
msgstr "Kompensation {} bearbeitet" msgstr "Kompensation {} bearbeitet"
#: compensation/views/compensation/compensation.py:185 #: compensation/views/compensation/compensation.py:185
#: compensation/views/eco_account/eco_account.py:159 ema/views/ema.py:211 #: compensation/views/eco_account/eco_account.py:159 ema/views/ema.py:210
#: intervention/views/intervention.py:225 #: intervention/views/intervention.py:228
msgid "Edit {}" msgid "Edit {}"
msgstr "Bearbeite {}" msgstr "Bearbeite {}"
@@ -1297,11 +1300,11 @@ msgstr "Ökokonto {} hinzugefügt"
msgid "Eco-Account {} edited" msgid "Eco-Account {} edited"
msgstr "Ökokonto {} bearbeitet" msgstr "Ökokonto {} bearbeitet"
#: compensation/views/eco_account/eco_account.py:263 #: compensation/views/eco_account/eco_account.py:265
msgid "Eco-account removed" msgid "Eco-account removed"
msgstr "Ökokonto entfernt" msgstr "Ökokonto entfernt"
#: ema/forms.py:42 ema/views/ema.py:94 #: ema/forms.py:42 ema/views/ema.py:93
msgid "New EMA" msgid "New EMA"
msgstr "Neue EMA hinzufügen" msgstr "Neue EMA hinzufügen"
@@ -1309,11 +1312,11 @@ msgstr "Neue EMA hinzufügen"
msgid "Edit EMA" msgid "Edit EMA"
msgstr "Bearbeite EMA" msgstr "Bearbeite EMA"
#: ema/tables.py:65 templates/navbars/navbar.html:43 #: ema/tables.py:62 templates/navbars/navbar.html:43
msgid "Payment funded compensations" msgid "Payment funded compensations"
msgstr "Ersatzzahlungsmaßnahmen (EMA)" msgstr "Ersatzzahlungsmaßnahmen (EMA)"
#: ema/tables.py:66 #: ema/tables.py:63
msgid "EMA explanation" msgid "EMA explanation"
msgstr "" msgstr ""
"EMA sind Kompensationen, die durch Ersatzzahlungen finanziert wurden. " "EMA sind Kompensationen, die durch Ersatzzahlungen finanziert wurden. "
@@ -1321,7 +1324,7 @@ msgstr ""
"Maßnahmen aus Ersatzzahlungen, die nach 2015 rechtskräftig wurden, werden " "Maßnahmen aus Ersatzzahlungen, die nach 2015 rechtskräftig wurden, werden "
"durch die Stiftung Natur und Umwelt verwaltet." "durch die Stiftung Natur und Umwelt verwaltet."
#: ema/tables.py:89 templates/navbars/navbar.html:43 #: ema/tables.py:86 templates/navbars/navbar.html:43
msgid "EMA" msgid "EMA"
msgstr "" msgstr ""
@@ -1329,15 +1332,15 @@ msgstr ""
msgid "Payment funded compensation" msgid "Payment funded compensation"
msgstr "Ersatzzahlungsmaßnahme" msgstr "Ersatzzahlungsmaßnahme"
#: ema/views/ema.py:51 #: ema/views/ema.py:50
msgid "EMAs - Overview" msgid "EMAs - Overview"
msgstr "EMAs - Übersicht" msgstr "EMAs - Übersicht"
#: ema/views/ema.py:84 #: ema/views/ema.py:83
msgid "EMA {} added" msgid "EMA {} added"
msgstr "EMA {} hinzugefügt" msgstr "EMA {} hinzugefügt"
#: ema/views/ema.py:201 #: ema/views/ema.py:200
msgid "EMA {} edited" msgid "EMA {} edited"
msgstr "EMA {} bearbeitet" msgstr "EMA {} bearbeitet"
@@ -1551,6 +1554,13 @@ msgstr "Ökokonto gelöscht! Abbuchung ungültig!"
msgid "Eco-account not recorded! Deduction invalid!" msgid "Eco-account not recorded! Deduction invalid!"
msgstr "Ökokonto nicht verzeichnet! Abbuchung ungültig!" msgstr "Ökokonto nicht verzeichnet! Abbuchung ungültig!"
#: intervention/templates/intervention/detail/includes/documents.html:25
msgid ""
"You entered a payment. Please upload the legal document which defines the "
"payment`s amount."
msgstr ""
"Sie haben Ersatzzahlungen angegeben. Laden Sie bitte den Zahlungsbescheid als Dokument hoch."
#: intervention/templates/intervention/detail/includes/payments.html:8 #: intervention/templates/intervention/detail/includes/payments.html:8
#: intervention/templates/intervention/report/report.html:69 #: intervention/templates/intervention/report/report.html:69
msgid "Payments" msgid "Payments"
@@ -1635,23 +1645,23 @@ msgstr "Eingriffe - Übersicht"
msgid "Intervention {} added" msgid "Intervention {} added"
msgstr "Eingriff {} hinzugefügt" msgstr "Eingriff {} hinzugefügt"
#: intervention/views/intervention.py:213 #: intervention/views/intervention.py:216
msgid "Intervention {} edited" msgid "Intervention {} edited"
msgstr "Eingriff {} bearbeitet" msgstr "Eingriff {} bearbeitet"
#: intervention/views/intervention.py:249 #: intervention/views/intervention.py:253
msgid "{} removed" msgid "{} removed"
msgstr "{} entfernt" msgstr "{} entfernt"
#: konova/decorators.py:33 #: konova/decorators.py:30
msgid "You need to be staff to perform this action!" msgid "You need to be staff to perform this action!"
msgstr "Hierfür müssen Sie Mitarbeiter sein!" msgstr "Hierfür müssen Sie Mitarbeiter sein!"
#: konova/decorators.py:48 #: konova/decorators.py:45
msgid "You need to be administrator to perform this action!" msgid "You need to be administrator to perform this action!"
msgstr "Hierfür müssen Sie Administrator sein!" msgstr "Hierfür müssen Sie Administrator sein!"
#: konova/decorators.py:66 #: konova/decorators.py:63
msgid "" msgid ""
"+++ Attention: You are not part of any group. You won't be able to create, " "+++ Attention: You are not part of any group. You won't be able to create, "
"edit or do anything. Please contact an administrator. +++" "edit or do anything. Please contact an administrator. +++"
@@ -1673,7 +1683,7 @@ msgid "Search for file number"
msgstr "Nach Aktenzeichen suchen" msgstr "Nach Aktenzeichen suchen"
#: konova/filters/mixins/geo_reference.py:29 #: konova/filters/mixins/geo_reference.py:29
#: konova/templates/konova/includes/parcels/parcel_table_frame.html:18 #: konova/templates/konova/includes/parcels/parcel_table_frame.html:19
msgid "District" msgid "District"
msgstr "Kreis" msgstr "Kreis"
@@ -1686,7 +1696,7 @@ msgid "Search for parcel gmrkng"
msgstr "Nach Gemarkung suchen" msgstr "Nach Gemarkung suchen"
#: konova/filters/mixins/geo_reference.py:55 #: konova/filters/mixins/geo_reference.py:55
#: konova/templates/konova/includes/parcels/parcel_table_frame.html:39 #: konova/templates/konova/includes/parcels/parcel_table_frame.html:40
msgid "Parcel" msgid "Parcel"
msgstr "Flur" msgstr "Flur"
@@ -1695,7 +1705,7 @@ msgid "Search for parcel"
msgstr "Nach Flur suchen" msgstr "Nach Flur suchen"
#: konova/filters/mixins/geo_reference.py:68 #: konova/filters/mixins/geo_reference.py:68
#: konova/templates/konova/includes/parcels/parcel_table_frame.html:40 #: konova/templates/konova/includes/parcels/parcel_table_frame.html:41
msgid "Parcel counter" msgid "Parcel counter"
msgstr "Flurstückzähler" msgstr "Flurstückzähler"
@@ -1704,7 +1714,7 @@ msgid "Search for parcel counter"
msgstr "Nach Flurstückzähler suchen" msgstr "Nach Flurstückzähler suchen"
#: konova/filters/mixins/geo_reference.py:82 #: konova/filters/mixins/geo_reference.py:82
#: konova/templates/konova/includes/parcels/parcel_table_frame.html:41 #: konova/templates/konova/includes/parcels/parcel_table_frame.html:42
msgid "Parcel number" msgid "Parcel number"
msgstr "Flurstücknenner" msgstr "Flurstücknenner"
@@ -1860,37 +1870,37 @@ msgstr ""
msgid "English" msgid "English"
msgstr "" msgstr ""
#: konova/templates/konova/includes/parcels/parcel_table_frame.html:5 #: konova/templates/konova/includes/parcels/parcel_table_frame.html:6
msgid "Parcels can not be calculated, since no geometry is given." msgid "Parcels can not be calculated, since no geometry is given."
msgstr "" msgstr ""
"Flurstücke können nicht berechnet werden, da keine Geometrie eingegeben " "Flurstücke können nicht berechnet werden, da keine Geometrie eingegeben "
"wurde." "wurde."
#: konova/templates/konova/includes/parcels/parcel_table_frame.html:11 #: konova/templates/konova/includes/parcels/parcel_table_frame.html:12
msgid "Parcels found" msgid "Parcels found"
msgstr "Flurstücke" msgstr "Flurstücke"
#: konova/templates/konova/includes/parcels/parcel_table_frame.html:16 #: konova/templates/konova/includes/parcels/parcel_table_frame.html:17
msgid "Municipal" msgid "Municipal"
msgstr "Gemeinde" msgstr "Gemeinde"
#: konova/templates/konova/includes/parcels/parcel_table_frame.html:17 #: konova/templates/konova/includes/parcels/parcel_table_frame.html:18
msgid "Municipal key" msgid "Municipal key"
msgstr "Gemeindeschlüssel" msgstr "Gemeindeschlüssel"
#: konova/templates/konova/includes/parcels/parcel_table_frame.html:19 #: konova/templates/konova/includes/parcels/parcel_table_frame.html:20
msgid "District key" msgid "District key"
msgstr "Kreisschlüssel" msgstr "Kreisschlüssel"
#: konova/templates/konova/includes/parcels/parcel_table_frame.html:37 #: konova/templates/konova/includes/parcels/parcel_table_frame.html:38
msgid "Parcel group" msgid "Parcel group"
msgstr "Gemarkung" msgstr "Gemarkung"
#: konova/templates/konova/includes/parcels/parcel_table_frame.html:38 #: konova/templates/konova/includes/parcels/parcel_table_frame.html:39
msgid "Parcel group key" msgid "Parcel group key"
msgstr "Gemarkungsschlüssel" msgstr "Gemarkungsschlüssel"
#: konova/templates/konova/includes/parcels/parcels.html:7 #: konova/templates/konova/includes/parcels/parcels.html:9
msgid "Spatial reference" msgid "Spatial reference"
msgstr "Raumreferenz" msgstr "Raumreferenz"
@@ -1938,39 +1948,39 @@ msgstr "In Zwischenablage kopiert"
msgid "Search" msgid "Search"
msgstr "Suchen" msgstr "Suchen"
#: konova/utils/mailer.py:68 konova/utils/mailer.py:137 #: konova/utils/mailer.py:68 konova/utils/mailer.py:143
msgid "{} - Shared access removed" msgid "{} - Shared access removed"
msgstr "{} - Zugriff entzogen" msgstr "{} - Zugriff entzogen"
#: konova/utils/mailer.py:91 konova/utils/mailer.py:114 #: konova/utils/mailer.py:91 konova/utils/mailer.py:117
msgid "{} - Shared access given" msgid "{} - Shared access given"
msgstr "{} - Zugriff freigegeben" msgstr "{} - Zugriff freigegeben"
#: konova/utils/mailer.py:160 konova/utils/mailer.py:302 #: konova/utils/mailer.py:169 konova/utils/mailer.py:317
msgid "{} - Shared data unrecorded" msgid "{} - Shared data unrecorded"
msgstr "{} - Freigegebene Daten entzeichnet" msgstr "{} - Freigegebene Daten entzeichnet"
#: konova/utils/mailer.py:183 konova/utils/mailer.py:279 #: konova/utils/mailer.py:195 konova/utils/mailer.py:294
msgid "{} - Shared data recorded" msgid "{} - Shared data recorded"
msgstr "{} - Freigegebene Daten verzeichnet" msgstr "{} - Freigegebene Daten verzeichnet"
#: konova/utils/mailer.py:206 konova/utils/mailer.py:348 #: konova/utils/mailer.py:221 konova/utils/mailer.py:363
msgid "{} - Shared data checked" msgid "{} - Shared data checked"
msgstr "{} - Freigegebene Daten geprüft" msgstr "{} - Freigegebene Daten geprüft"
#: konova/utils/mailer.py:233 konova/utils/mailer.py:376 #: konova/utils/mailer.py:248 konova/utils/mailer.py:391
msgid "{} - Deduction changed" msgid "{} - Deduction changed"
msgstr "{} - Abbuchung geändert" msgstr "{} - Abbuchung geändert"
#: konova/utils/mailer.py:256 konova/utils/mailer.py:325 #: konova/utils/mailer.py:271 konova/utils/mailer.py:340
msgid "{} - Shared data deleted" msgid "{} - Shared data deleted"
msgstr "{} - Freigegebene Daten gelöscht" msgstr "{} - Freigegebene Daten gelöscht"
#: konova/utils/mailer.py:397 templates/email/api/verify_token.html:4 #: konova/utils/mailer.py:412 templates/email/api/verify_token.html:4
msgid "Request for new API token" msgid "Request for new API token"
msgstr "Anfrage für neuen API Token" msgstr "Anfrage für neuen API Token"
#: konova/utils/mailer.py:420 #: konova/utils/mailer.py:435
msgid "Resubmission - {}" msgid "Resubmission - {}"
msgstr "Wiedervorlage - {}" msgstr "Wiedervorlage - {}"
@@ -2210,9 +2220,17 @@ msgstr "{} wurde erfolgreich vom Nutzer {} geprüft! {}"
#: konova/utils/quality.py:32 #: konova/utils/quality.py:32
msgid "missing" msgid "missing"
msgstr "fehlt" msgstr "fehlend"
#: konova/views/home.py:78 templates/navbars/navbar.html:16 #: konova/utils/tables.py:218
msgid "Full access granted"
msgstr "Für Sie freigegeben - Datensatz kann bearbeitet werden"
#: konova/utils/tables.py:218
msgid "Access not granted"
msgstr "Nicht freigegeben - Datensatz nur lesbar"
#: konova/views/home.py:79 templates/navbars/navbar.html:16
msgid "Home" msgid "Home"
msgstr "Home" msgstr "Home"
@@ -2289,8 +2307,8 @@ msgid "Server Error"
msgstr "" msgstr ""
#: templates/500.html:10 #: templates/500.html:10
msgid "Something happened. We are working on it!" msgid "Something happened. Admins have been informed. We are working on it!"
msgstr "Irgendetwas ist passiert. Wir arbeiten daran!" msgstr "Irgendetwas ist passiert. Die Administratoren wurden informiert. Wir arbeiten daran!"
#: templates/email/api/verify_token.html:7 #: templates/email/api/verify_token.html:7
msgid "Hello support" msgid "Hello support"
@@ -2657,13 +2675,15 @@ msgstr ""
msgid "User" msgid "User"
msgstr "Nutzer" msgstr "Nutzer"
#: templates/map/geom_form.html:9 #: templates/map/geom_form.html:11 templates/table/gmrkng_col.html:4
msgid "No geometry added, yet." msgid "No geometry added, yet."
msgstr "Keine Geometrie vorhanden" msgstr "Keine Geometrie vorhanden"
#: templates/modal/modal_session_timed_out.html:3 #: templates/modal/modal_session_timed_out.html:3
msgid "Your session has timed out. Please reload the page to login." msgid "Your session has timed out. Please reload the page to login."
msgstr "Ihre Sitzung ist aufgrund von Inaktivität abgelaufen. Laden Sie die Seite erneut, um sich wieder einzuloggen." msgstr ""
"Ihre Sitzung ist aufgrund von Inaktivität abgelaufen. Laden Sie die Seite "
"erneut, um sich wieder einzuloggen."
#: templates/navbars/navbar.html:4 #: templates/navbars/navbar.html:4
msgid "Kompensationsverzeichnis Service Portal" msgid "Kompensationsverzeichnis Service Portal"
@@ -2710,7 +2730,7 @@ msgstr ""
"vorbei. \n" "vorbei. \n"
" " " "
#: templates/table/gmrkng_col.html:6 #: templates/table/gmrkng_col.html:12
msgid "" msgid ""
"If the geometry is not empty, the parcels are currently recalculated. Please " "If the geometry is not empty, the parcels are currently recalculated. Please "
"refresh this page in a few moments." "refresh this page in a few moments."
@@ -2882,35 +2902,27 @@ msgstr ""
" " " "
#: user/templates/user/index.html:42 #: user/templates/user/index.html:42
msgid "Change default configuration for your KSP map"
msgstr "Karteneinstellungen ändern"
#: user/templates/user/index.html:45
msgid "Map settings"
msgstr "Karte"
#: user/templates/user/index.html:50
msgid "Change notification configurations" msgid "Change notification configurations"
msgstr "Benachrichtigungseinstellungen ändern" msgstr "Benachrichtigungseinstellungen ändern"
#: user/templates/user/index.html:53 #: user/templates/user/index.html:45
msgid "Notification settings" msgid "Notification settings"
msgstr "Benachrichtigungen" msgstr "Benachrichtigungen"
#: user/templates/user/index.html:58 #: user/templates/user/index.html:50
msgid "Manage teams" msgid "Manage teams"
msgstr "" msgstr ""
#: user/templates/user/index.html:61 user/templates/user/team/index.html:19 #: user/templates/user/index.html:53 user/templates/user/team/index.html:19
#: user/views.py:169 #: user/views.py:171
msgid "Teams" msgid "Teams"
msgstr "" msgstr ""
#: user/templates/user/index.html:66 #: user/templates/user/index.html:58
msgid "See or edit your API token" msgid "See or edit your API token"
msgstr "API token einsehen oder neu generieren" msgstr "API token einsehen oder neu generieren"
#: user/templates/user/index.html:69 #: user/templates/user/index.html:61
msgid "API" msgid "API"
msgstr "" msgstr ""
@@ -2974,23 +2986,23 @@ msgstr "Neuer Token generiert. Administratoren sind informiert."
msgid "User API token" msgid "User API token"
msgstr "API Nutzer Token" msgstr "API Nutzer Token"
#: user/views.py:180 #: user/views.py:183
msgid "New team added" msgid "New team added"
msgstr "Neues Team hinzugefügt" msgstr "Neues Team hinzugefügt"
#: user/views.py:194 #: user/views.py:198
msgid "Team edited" msgid "Team edited"
msgstr "Team bearbeitet" msgstr "Team bearbeitet"
#: user/views.py:208 #: user/views.py:213
msgid "Team removed" msgid "Team removed"
msgstr "Team gelöscht" msgstr "Team gelöscht"
#: user/views.py:222 #: user/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.py:229 #: user/views.py:235
msgid "Left Team" msgid "Left Team"
msgstr "Team verlassen" msgstr "Team verlassen"
@@ -4493,6 +4505,12 @@ msgstr ""
msgid "Unable to connect to qpid with SASL mechanism %s" msgid "Unable to connect to qpid with SASL mechanism %s"
msgstr "" msgstr ""
#~ msgid "Change default configuration for your KSP map"
#~ msgstr "Karteneinstellungen ändern"
#~ msgid "Map settings"
#~ msgstr "Karte"
#~ msgid "There are errors on this intervention:" #~ msgid "There are errors on this intervention:"
#~ msgstr "Es liegen Fehler in diesem Eingriff vor:" #~ msgstr "Es liegen Fehler in diesem Eingriff vor:"

View File

@@ -7,7 +7,7 @@
<h1 class="display-4">{% trans 'Server Error' %}</h1> <h1 class="display-4">{% trans 'Server Error' %}</h1>
<hr> <hr>
<p class="lead"> <p class="lead">
{% trans 'Something happened. We are working on it!' %} {% trans 'Something happened. Admins have been informed. We are working on it!' %}
</p> </p>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -36,11 +36,13 @@
{ "folder": 2, "type": "WMS", "title": "Verbandsgemeinden", "url": "http://geo5.service24.rlp.de/wms/verwaltungsgrenzen_rp.fcgi?", "name": "Verbandsgemeinden" }, { "folder": 2, "type": "WMS", "title": "Verbandsgemeinden", "url": "http://geo5.service24.rlp.de/wms/verwaltungsgrenzen_rp.fcgi?", "name": "Verbandsgemeinden" },
{ "folder": 2, "type": "WMS", "title": "Gemeinden", "url": "http://geo5.service24.rlp.de/wms/verwaltungsgrenzen_rp.fcgi?", "name": "Gemeinden" }, { "folder": 2, "type": "WMS", "title": "Gemeinden", "url": "http://geo5.service24.rlp.de/wms/verwaltungsgrenzen_rp.fcgi?", "name": "Gemeinden" },
{ "folder": 0, "type": "WMS", "title": "Webatlas farbig", "attribution": "LVermGeo", "url": "https://maps.service24.rlp.de/gisserver/services/RP/RP_WebAtlasRP/MapServer/WmsServer?", "name": "RP_WebAtlasRP", "active": true }, { "folder": 15, "type": "WMS", "title": "farbig", "attribution": "LVermGeo", "url": "https://maps.service24.rlp.de/gisserver/services/RP/RP_WebAtlasRP/MapServer/WmsServer?", "name": "RP_WebAtlasRP", "active": true },
{ "folder": 0, "type": "WMS", "title": "Webatlas grau", "attribution": "LVermGeo", "url": "https://maps.service24.rlp.de/gisserver/services/RP/RP_ETRS_Gt/MapServer/WmsServer?", "name": "0", "active": false }, { "folder": 15, "type": "WMS", "title": "grau", "attribution": "LVermGeo", "url": "https://maps.service24.rlp.de/gisserver/services/RP/RP_ETRS_Gt/MapServer/WmsServer?", "name": "0", "active": false },
{ "folder": 0, "type": "WMS", "title": "Luftbilder", "attribution": "LVermGeo", "url": "http://geo4.service24.rlp.de/wms/dop_basis.fcgi?", "name": "rp_dop", "active": false }, { "folder": 0, "type": "WMS", "title": "Luftbilder", "attribution": "LVermGeo", "url": "http://geo4.service24.rlp.de/wms/dop_basis.fcgi?", "name": "rp_dop", "active": false },
{ "folder": 0, "type": "WMS", "title": "TopPlusOpen", "attribution": "BKG", "url": "https://sgx.geodatenzentrum.de/wms_topplus_open?", "name": "web", "active": false }, { "folder": 14, "type": "WMS", "title": "farbig", "attribution": "BKG", "url": "https://sgx.geodatenzentrum.de/wms_basemapde?", "name": "de_basemapde_web_raster_farbe", "active": false },
{ "folder": 0, "type": "OSM", "title": "Open Street Map", "attribution": "OSM", "active": false } { "folder": 14, "type": "WMS", "title": "grau", "attribution": "BKG", "url": "https://sgx.geodatenzentrum.de/wms_basemapde?", "name": "de_basemapde_web_raster_grau", "active": false },
{ "folder": 13, "type": "WMS", "title": "farbig", "attribution": "LVermGeo", "url": "https://geo4.service24.rlp.de/wms/dtk5_rp.fcgi?", "name": "rp_dtk5", "active": false },
{ "folder": 13, "type": "WMS", "title": "grau", "attribution": "LVermGeo", "url": "https://geo4.service24.rlp.de/wms/dtk5_rp.fcgi?", "name": "rp_dtk5_grau", "active": false }
], ],
"folders": "folders":
@@ -57,7 +59,10 @@
{ "title": "MAE", "parent": 4 }, { "title": "MAE", "parent": 4 },
{ "title": "Schutzgebiete", "parent": 3 }, { "title": "Schutzgebiete", "parent": 3 },
{ "title": "Nationalparke", "parent": 10 }, { "title": "Nationalparke", "parent": 10 },
{ "title": "Naturräume", "parent": 10 } { "title": "Naturräume", "parent": 10 },
{ "title": "Topographisch (DTK5)", "parent": 0 },
{ "title": "BaseMap", "parent": 0 },
{ "title": "Webatlas", "parent": 0 }
], ],
"projections": "projections":

View File

@@ -394,16 +394,12 @@ netgis.MapOpenLayers.prototype.clearAll = function()
{ {
for ( var i = 0; i < this.layers.length; i++ ) for ( var i = 0; i < this.layers.length; i++ )
{ {
if(this.layers[i] === this.editLayer){
continue;
};
this.map.removeLayer( this.layers[ i ] ); this.map.removeLayer( this.layers[ i ] );
} }
this.layers = [this.editLayer]; this.layers = [];
this.snapFeatures.clear(); this.snapFeatures.clear();
this.snapFeatures.push(this.editLayer);
}; };
netgis.MapOpenLayers.prototype.onUpdateStyle = function( e ) netgis.MapOpenLayers.prototype.onUpdateStyle = function( e )
@@ -1116,10 +1112,15 @@ netgis.MapOpenLayers.prototype.updateEditOutput = function()
{ {
var features = this.editLayer.getSource().getFeatures(); var features = this.editLayer.getSource().getFeatures();
// Output var proj = this.client.config.map.projection;
var format = new ol.format.GeoJSON(); var format = new ol.format.GeoJSON();
//var output = format.writeFeatures( features ); var output = format.writeFeaturesObject( features, { dataProjection: proj, featureProjection: proj } );
var output = format.writeFeaturesObject( features );
output[ "crs" ] =
{
"type": "name",
"properties": { "name": "urn:ogc:def:crs:" + proj.replace( ":", "::" ) }
};
if ( ! this.editEventsSilent ) if ( ! this.editEventsSilent )
this.client.invoke( netgis.Events.EDIT_FEATURES_CHANGE, output ); this.client.invoke( netgis.Events.EDIT_FEATURES_CHANGE, output );
@@ -1139,15 +1140,9 @@ netgis.MapOpenLayers.prototype.updateEditLayerItem = function()
netgis.MapOpenLayers.prototype.onEditFeaturesLoaded = function( e ) netgis.MapOpenLayers.prototype.onEditFeaturesLoaded = function( e )
{ {
var json = e; var json = e;
var format = new ol.format.GeoJSON(); var self = this;
var features = format.readFeatures( json ); window.setTimeout( function() { self.createLayerGeoJSON( "Import", json ); }, 10 );
this.editLayer.getSource().addFeatures( features );
//this.snapFeatures.push( e.feature );
if ( features.length > 0 )
this.view.fit( this.editLayer.getSource().getExtent(), { padding: [ 40, 40, 40, 40 ] } );
}; };
netgis.MapOpenLayers.prototype.onDragEnter = function( e ) netgis.MapOpenLayers.prototype.onDragEnter = function( e )
@@ -1254,7 +1249,6 @@ netgis.MapOpenLayers.prototype.onImportShapefile = function( e )
netgis.MapOpenLayers.prototype.createLayerGeoJSON = function( title, data ) netgis.MapOpenLayers.prototype.createLayerGeoJSON = function( title, data )
{ {
//var format = new ol.format.GeoJSON( { dataProjection: "EPSG:4326", featureProjection: this.client.config.map.projection /*"EPSG:3857"*/ } );
var format = new ol.format.GeoJSON(); var format = new ol.format.GeoJSON();
var projection = format.readProjection( data ); var projection = format.readProjection( data );
var features = format.readFeatures( data, { featureProjection: this.client.config.map.projection } ); var features = format.readFeatures( data, { featureProjection: this.client.config.map.projection } );
@@ -1262,7 +1256,26 @@ netgis.MapOpenLayers.prototype.createLayerGeoJSON = function( title, data )
//NOTE: proj4.defs[ "EPSG:4326" ] //NOTE: proj4.defs[ "EPSG:4326" ]
//NOTE: netgis.util.foreach( proj4.defs, function( k,v ) { console.info( "DEF:", k, v ); } ) //NOTE: netgis.util.foreach( proj4.defs, function( k,v ) { console.info( "DEF:", k, v ); } )
//console.info( "Projection:", projection.getCode() ); var projcode = projection.getCode();
switch ( projcode )
{
case "EPSG:3857":
case "EPSG:4326":
case this.client.config.map.projection:
{
// Projection OK
//console.info( "Import Projection:", projcode );
break;
}
default:
{
// Projection Not Supported
console.warn( "Unsupported Import Projection:", projcode );
break;
}
}
this.addImportedFeatures( features ); this.addImportedFeatures( features );
}; };
@@ -1274,19 +1287,142 @@ netgis.MapOpenLayers.prototype.createLayerGML = function( title, data )
console.warn( "GML support is experimental!" ); console.warn( "GML support is experimental!" );
var format = new ol.format.WFS( /*{ srsName: "EPSG:4326", featureType: "ogr:RLP_OG_utf8_epsg4326" }*/ ); //var format = new ol.format.GML3( { srsName: "EPSG::25832", featureType: "Test", featureNS: "http://www.opengis.net/gml" } );
//var format = new ol.format.GML( { featureNS: "ogr" } );
//var format = new ol.format.WFS( /*{ srsName: "EPSG:4326", featureType: "ogr:RLP_OG_utf8_epsg4326" }*/ );
//var format = new ol.format.GML( { featureNS: "ogr", featureType: "ogr:RLP_OG_utf8_epsg4326" } ); //var format = new ol.format.GML( { featureNS: "ogr", featureType: "ogr:RLP_OG_utf8_epsg4326" } );
//var format = new ol.format.WFS(); //var format = new ol.format.WFS();
//var format = new ol.format.WFS( { featureNS: "ogr", featureType: "RLP_OG_utf8_epsg4326" } ); //var format = new ol.format.WFS( { featureNS: "ogr", featureType: "RLP_OG_utf8_epsg4326" } );
var projection = format.readProjection( data ); //var projection = format.readProjection( data );
//var features = format.readFeatures( data, { dataProjection: "EPSG:4326", featureProjection: "EPSG:3857" } ); //var features = format.readFeatures( data, { dataProjection: "EPSG:4326", featureProjection: "EPSG:3857" } );
var features = format.readFeatures( data, { featureProjection: this.client.config.map.projection } );
//var features = format.readFeatures( data, { dataProjection: this.client.config.map.projection, featureProjection: this.client.config.map.projection } );
console.info( "GML:", projection, features ); //console.info( "GML:", projection, features, features[ 0 ].getGeometry() );
var features = [];
var parser = new DOMParser();
var xml = parser.parseFromString( data, "text/xml" );
// Features
var featureMembers = xml.getElementsByTagName( "gml:featureMember" );
for ( var f = 0; f < featureMembers.length; f++ )
{
var props = {};
var node = featureMembers[ f ];
var child = node.children[ 0 ];
// Attributes
for ( var a = 0; a < child.attributes.length; a++ )
{
var attribute = child.attributes[ a ];
props[ attribute.nodeName ] = attribute.nodeValue;
}
for ( var c = 0; c < child.children.length; c++ )
{
var childNode = child.children[ c ];
if ( childNode.nodeName === "ogr:geometryProperty" ) continue;
var parts = childNode.nodeName.split( ":" );
var k = parts[ parts.length - 1 ];
var v = childNode.innerHTML;
props[ k ] = v;
}
// Geometry
var geomprop = child.getElementsByTagName( "ogr:geometryProperty" )[ 0 ];
//for ( var g = 0; g < geomprop.children.length; g++ )
{
var geom = geomprop.children[ 0 ];
var proj = geom.getAttribute( "srsName" );
if ( proj && proj !== "EPSG:4326" && proj !== this.client.config.map.projection )
console.warn( "Unsupported Import Projection:", proj );
switch ( geom.nodeName )
{
case "gml:Polygon":
{
props[ "geometry" ] = this.gmlParsePolygon( geom, proj );
break;
}
case "gml:MultiPolygon":
{
props[ "geometry" ] = this.gmlParseMultiPolygon( geom, proj );
break;
}
}
}
var feature = new ol.Feature( props );
features.push( feature );
}
this.addImportedFeatures( features ); this.addImportedFeatures( features );
}; };
netgis.MapOpenLayers.prototype.gmlParsePolygon = function( node, proj )
{
var rings = [];
var linearRings = node.getElementsByTagName( "gml:LinearRing" );
for ( var r = 0; r < linearRings.length; r++ )
{
var ring = linearRings[ r ];
var coords = ring.getElementsByTagName( "gml:coordinates" )[ 0 ].innerHTML;
rings.push( this.gmlParseCoordinates( coords, proj ) );
}
return new ol.geom.Polygon( rings );
};
netgis.MapOpenLayers.prototype.gmlParseMultiPolygon = function( node, proj )
{
var polygons = [];
var polygonMembers = node.getElementsByTagName( "gml:polygonMember" );
for ( var p = 0; p < polygonMembers.length; p++ )
{
var polygonMember = polygonMembers[ p ];
var polygonNode = polygonMember.getElementsByTagName( "gml:Polygon" )[ 0 ];
polygons.push( this.gmlParsePolygon( polygonNode, proj ) );
}
return new ol.geom.MultiPolygon( polygons );
};
netgis.MapOpenLayers.prototype.gmlParseCoordinates = function( s, proj )
{
var coords = s.split( " " );
for ( var c = 0; c < coords.length; c++ )
{
// Split
coords[ c ] = coords[ c ].split( "," );
// Parse
for ( var xy = 0; xy < coords[ c ].length; xy++ )
{
coords[ c ][ xy ] = Number.parseFloat( coords[ c ][ xy ] );
}
// Transform
if ( proj ) coords[ c ] = ol.proj.transform( coords[ c ], proj, this.client.config.map.projection );
}
return coords;
};
netgis.MapOpenLayers.prototype.createLayerShapefile = function( title, shapeData ) netgis.MapOpenLayers.prototype.createLayerShapefile = function( title, shapeData )
{ {
var self = this; var self = this;
@@ -1312,22 +1448,20 @@ netgis.MapOpenLayers.prototype.addImportedFeatures = function( features )
this.editLayer.getSource().addFeatures( features ); this.editLayer.getSource().addFeatures( features );
this.editEventsSilent = false; this.editEventsSilent = false;
this.updateEditOutput(); this.updateEditOutput();
/* // Zoom Imported Features
// Create New Layer
var id = this.importLayerID;
this.importLayerID += 1;
var layer = new ol.layer.Vector( { source: new ol.source.Vector( { features: features } ), zIndex: id } );
this.map.addLayer( layer );
this.layers[ id ] = layer;
this.addSnapLayer( layer );
if ( features.length > 0 ) if ( features.length > 0 )
this.view.fit( layer.getSource().getExtent(), {} ); {
var extent = features[ 0 ].getGeometry().getExtent();
this.client.invoke( netgis.Events.LAYER_CREATED, { id: id, title: title, checked: true, folder: "import" } );
*/ for ( var f = 1; f < features.length; f++ )
{
ol.extent.extend( extent, features[ f ].getGeometry().getExtent() );
}
var padding = 40;
this.view.fit( extent, { duration: 300, padding: [ padding, padding, padding, padding ] } );
}
}; };
netgis.MapOpenLayers.prototype.onImportWKT = function( e ) netgis.MapOpenLayers.prototype.onImportWKT = function( e )

View File

@@ -17,8 +17,6 @@
line-height: 12mm; line-height: 12mm;
font-size: 0mm; font-size: 0mm;
white-space: nowrap; white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
z-index: 1; z-index: 1;
-webkit-transform: none; -webkit-transform: none;
@@ -108,12 +106,8 @@
.netgis-toolbar .netgis-search-list .netgis-toolbar .netgis-search-list
{ {
position: fixed; position: absolute;
min-width: 68mm; /* 60mm + 4mm + 4mm ( input width + padding ) */ min-width: 68mm; /* 60mm + 4mm + 4mm ( input width + padding ) */
/*osition: absolute;
left: 0mm;
min-width: 100%;*/
/*height: 5.0em;*/
padding: 0mm; padding: 0mm;
margin: 0mm; margin: 0mm;
margin-left: -4mm; margin-left: -4mm;

View File

@@ -1,4 +1,4 @@
{% load i18n %} {% load i18n fontawesome_5 %}
{% comment %} {% comment %}
Encapsules the rendering and initializing of a geometry view component, e.g. used in the detail views. Encapsules the rendering and initializing of a geometry view component, e.g. used in the detail views.
@@ -6,7 +6,10 @@
{% if geom_form.empty %} {% if geom_form.empty %}
<div class="w-100"> <div class="w-100">
<div class="alert alert-info">{% trans 'No geometry added, yet.' %}</div> <div class="alert alert-info">
{% fa5_icon 'search-location' %}
{% trans 'No geometry added, yet.' %}
</div>
</div> </div>
{% endif %} {% endif %}

View File

@@ -1,9 +1,16 @@
{% load i18n fontawesome_5 %} {% load i18n fontawesome_5 %}
{% for entry in entries %} {% if geometry.geom is None or geometry.geom.empty %}
<span class="badge pill-badge rlp-r">{{entry}}</span> <span class="text-info" title="{% translate 'No geometry added, yet.' %}">
{% empty %} {% fa5_icon 'search-location' %}
<span class="text-info" title="{% trans 'If the geometry is not empty, the parcels are currently recalculated. Please refresh this page in a few moments.' %}"> {% fa5_icon 'question' %}
{% fa5_icon 'hourglass-half' %}
</span> </span>
{% endfor %} {% else %}
{% for entry in entries %}
<span class="badge pill-badge rlp-r">{{entry}}</span>
{% empty %}
<span class="text-info" title="{% trans 'If the geometry is not empty, the parcels are currently recalculated. Please refresh this page in a few moments.' %}">
{% fa5_icon 'hourglass-half' %}
</span>
{% endfor %}
{% endif %}

View File

@@ -17,15 +17,15 @@ class TeamAdminAutocomplete(Select2QuerySetView):
def get_queryset(self): def get_queryset(self):
if self.request.user.is_anonymous: if self.request.user.is_anonymous:
return User.objects.none() return User.objects.none()
qs = User.objects.filter( qs = User.objects.filter(
id__in=self.forwarded.get("members", []) id__in=self.forwarded.get("members", [])
).exclude( ).exclude(
id__in=self.forwarded.get("admins", []) id__in=self.forwarded.get("admins", [])
) )
if self.q: if self.q:
# Due to privacy concerns only a full username match will return the proper user entry
qs = qs.filter( qs = qs.filter(
name__icontains=self.q username__icontains=self.q
) )
qs = qs.order_by( qs = qs.order_by(
"username" "username"

View File

@@ -2,6 +2,7 @@ from django.db import models
from konova.models import UuidModel, DeletableObjectMixin from konova.models import UuidModel, DeletableObjectMixin
from konova.utils.mailer import Mailer from konova.utils.mailer import Mailer
from user.enums import UserNotificationEnum
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@@ -41,7 +42,10 @@ class Team(UuidModel, DeletableObjectMixin):
""" """
mailer = Mailer() mailer = Mailer()
mailer.send_mail_shared_access_given_team(obj_identifier, obj_title, self) users_to_notify = self.users.filter(
notifications__in=[UserNotificationEnum.NOTIFY_ON_SHARED_ACCESS_GAINED.value]
)
mailer.send_mail_shared_access_given_team(obj_identifier, obj_title, self, users_to_notify)
def send_mail_shared_access_removed(self, obj_identifier, obj_title): def send_mail_shared_access_removed(self, obj_identifier, obj_title):
""" Sends a mail to the team members in case of removed shared access """ Sends a mail to the team members in case of removed shared access
@@ -54,7 +58,10 @@ class Team(UuidModel, DeletableObjectMixin):
""" """
mailer = Mailer() mailer = Mailer()
mailer.send_mail_shared_access_removed_team(obj_identifier, obj_title, self) users_to_notify = self.users.filter(
notifications__in=[UserNotificationEnum.NOTIFY_ON_SHARED_ACCESS_REMOVED.value]
)
mailer.send_mail_shared_access_removed_team(obj_identifier, obj_title, self, users_to_notify)
def send_mail_shared_data_unrecorded(self, obj_identifier, obj_title): def send_mail_shared_data_unrecorded(self, obj_identifier, obj_title):
""" Sends a mail to the team members in case of unrecorded data """ Sends a mail to the team members in case of unrecorded data
@@ -67,7 +74,10 @@ class Team(UuidModel, DeletableObjectMixin):
""" """
mailer = Mailer() mailer = Mailer()
mailer.send_mail_shared_data_unrecorded_team(obj_identifier, obj_title, self) users_to_notify = self.users.filter(
notifications__in=[UserNotificationEnum.NOTIFY_ON_SHARED_DATA_RECORDED.value]
)
mailer.send_mail_shared_data_unrecorded_team(obj_identifier, obj_title, self, users_to_notify)
def send_mail_shared_data_recorded(self, obj_identifier, obj_title): def send_mail_shared_data_recorded(self, obj_identifier, obj_title):
""" Sends a mail to the team members in case of unrecorded data """ Sends a mail to the team members in case of unrecorded data
@@ -80,7 +90,10 @@ class Team(UuidModel, DeletableObjectMixin):
""" """
mailer = Mailer() mailer = Mailer()
mailer.send_mail_shared_data_recorded_team(obj_identifier, obj_title, self) users_to_notify = self.users.filter(
notifications__in=[UserNotificationEnum.NOTIFY_ON_SHARED_DATA_RECORDED.value]
)
mailer.send_mail_shared_data_recorded_team(obj_identifier, obj_title, self, users_to_notify)
def send_mail_shared_data_checked(self, obj_identifier, obj_title): def send_mail_shared_data_checked(self, obj_identifier, obj_title):
""" Sends a mail to the team members in case of checked data """ Sends a mail to the team members in case of checked data
@@ -93,7 +106,10 @@ class Team(UuidModel, DeletableObjectMixin):
""" """
mailer = Mailer() mailer = Mailer()
mailer.send_mail_shared_data_checked_team(obj_identifier, obj_title, self) users_to_notify = self.users.filter(
notifications__in=[UserNotificationEnum.NOTIFY_ON_SHARED_DATA_CHECKED.value]
)
mailer.send_mail_shared_data_checked_team(obj_identifier, obj_title, self, users_to_notify)
def send_mail_deduction_changed(self, obj_identifier, obj_title, data_changes): def send_mail_deduction_changed(self, obj_identifier, obj_title, data_changes):
""" Sends a mail to the team members in case of changed deduction values """ Sends a mail to the team members in case of changed deduction values
@@ -107,7 +123,10 @@ class Team(UuidModel, DeletableObjectMixin):
""" """
mailer = Mailer() mailer = Mailer()
mailer.send_mail_deduction_changed_team(obj_identifier, obj_title, self, data_changes) users_to_notify = self.users.filter(
notifications__in=[UserNotificationEnum.NOTIFY_ON_DEDUCTION_CHANGES.value]
)
mailer.send_mail_deduction_changed_team(obj_identifier, obj_title, self, data_changes, users_to_notify)
def send_mail_shared_data_deleted(self, obj_identifier, obj_title): def send_mail_shared_data_deleted(self, obj_identifier, obj_title):
""" Sends a mail to the team members in case of deleted data """ Sends a mail to the team members in case of deleted data
@@ -120,7 +139,10 @@ class Team(UuidModel, DeletableObjectMixin):
""" """
mailer = Mailer() mailer = Mailer()
mailer.send_mail_shared_data_deleted_team(obj_identifier, obj_title, self) users_to_notify = self.users.filter(
notifications__in=[UserNotificationEnum.NOTIFY_ON_SHARED_DATA_DELETED.value]
)
mailer.send_mail_shared_data_deleted_team(obj_identifier, obj_title, self, users_to_notify)
def remove_user(self, user): def remove_user(self, user):
""" Removes a user from the team """ Removes a user from the team

View File

@@ -38,14 +38,6 @@
</article> </article>
<hr> <hr>
<div class="col-sm"> <div class="col-sm">
<div class="row mb-2">
<a href="{% url 'user:index' %}" title="{% trans 'Change default configuration for your KSP map' %}">
<button class="btn btn-default">
{% fa5_icon 'layer-group' %}
<span>{% trans 'Map settings' %}</span>
</button>
</a>
</div>
<div class="row mb-2"> <div class="row mb-2">
<a href="{% url 'user:notifications' %}" title="{% trans 'Change notification configurations' %}"> <a href="{% url 'user:notifications' %}" title="{% trans 'Change notification configurations' %}">
<button class="btn btn-default"> <button class="btn btn-default">