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 konova.models import Geometry
from konova.sub_settings.django_settings import BASE_DIR, DEFAULT_DATE_FORMAT
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
class TimespanReport:
@@ -103,9 +104,9 @@ class TimespanReport:
"iterable": self.evaluated_laws,
"attrs": [
"short_name",
"num",
"num_checked",
"num_recorded",
"num",
]
},
"i_laws_checked": self.law_sum_checked,
@@ -333,7 +334,7 @@ class TimespanReport:
return Geometry.objects.filter(
id__in=ids
).annotate(
geom_cast=Cast("geom", MultiPolygonField())
geom_cast=Cast("geom", MultiPolygonField(srid=DEFAULT_SRID_RLP))
).annotate(
num=NumGeometries("geom_cast")
).aggregate(

View File

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

View File

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

View File

@@ -136,8 +136,6 @@ class AbstractModelAPISerializer:
geometry = geos.fromstr(geojson)
if geometry.srid != DEFAULT_SRID_RLP:
geometry.transform(DEFAULT_SRID_RLP)
if geometry.empty:
geometry = None
return geometry
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 intervention.models import Intervention
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 user.models import UserActionLogEntry
@@ -64,6 +64,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
obj = Compensation()
created = create_action
obj.created = created
obj.modified = created
obj.geometry = geometry
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_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):
""" 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_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 intervention.models import Legal, Responsibility, Handler
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
@@ -103,6 +103,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
obj.legal = Legal()
created = create_action
obj.created = created
obj.modified = created
obj.geometry = geometry
return obj
@@ -146,12 +147,13 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states)
obj = self._set_deadlines(obj, properties["deadlines"])
obj.log.add(obj.created)
obj.users.add(user)
obj.log.add(obj.created)
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):
""" 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_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 intervention.models import Responsibility, Handler
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
@@ -85,6 +85,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
)
created = create_action
obj.created = created
obj.modified = created
obj.geometry = geometry
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_deadlines(obj, properties["deadlines"])
obj.log.add(obj.created)
obj.users.add(user)
obj.log.add(obj.created)
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):
""" 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_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 intervention.models import Intervention, Responsibility, Legal, Handler
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
@@ -76,6 +76,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
created = create_action
obj.legal = legal
obj.created = created
obj.modified = created
obj.geometry = geometry
obj.responsible = resp
return obj
@@ -161,12 +162,13 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
obj.legal.save()
obj.save()
obj.users.add(user)
obj.log.add(obj.created)
obj.users.add(user)
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):
""" Updates an entry for the model based on the contents of json_model
@@ -198,8 +200,8 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
obj.legal.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
from django.contrib.gis.geos import MultiPolygon
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import QuerySet
from api.utils.serializer.serializer import AbstractModelAPISerializer
@@ -80,10 +81,14 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer):
json_str = str(json_str)
if len(json_str) == 0:
return None
code = KonovaCode.objects.get(
atom_id=json_str,
code_lists__in=[code_list_identifier]
)
try:
code = KonovaCode.objects.get(
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
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"]
]
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"]
# Check on validity

View File

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

View File

@@ -129,12 +129,11 @@ class NewCompensationForm(AbstractCompensationForm,
self.initialize_form_field("identifier", identifier)
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
Args:
user (User): The performing user
geom_form (SimpleGeomForm): The geometry form
Returns:
comp (Compensation): The compensation object
@@ -150,8 +149,6 @@ class NewCompensationForm(AbstractCompensationForm,
# Create log entry
action = UserActionLogEntry.get_created_action(user)
# Process the geometry form
geometry = geom_form.save(action)
# Finally create main object
comp = Compensation.objects.create(
@@ -159,21 +156,27 @@ class NewCompensationForm(AbstractCompensationForm,
title=title,
intervention=intervention,
created=action,
modified=action,
is_cef=is_cef,
is_coherence_keeping=is_coherence_keeping,
is_pik=is_pik,
geometry=geometry,
comment=comment,
)
# Add the log entry to the main objects log list
comp.log.add(action)
return comp
return comp, action
def save(self, user: User, geom_form: SimpleGeomForm):
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))
# Process the geometry form
geometry = geom_form.save(action)
comp.geometry = geometry
comp.save()
return comp
@@ -205,6 +208,9 @@ class EditCompensationForm(NewCompensationForm):
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Create log entry
action = UserActionLogEntry.get_edited_action(user)
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
@@ -214,17 +220,9 @@ class EditCompensationForm(NewCompensationForm):
is_pik = self.cleaned_data.get("is_pik", 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.title = title
self.instance.intervention = intervention
self.instance.geometry = geometry
self.instance.is_cef = is_cef
self.instance.is_coherence_keeping = is_coherence_keeping
self.instance.comment = comment
@@ -233,6 +231,11 @@ class EditCompensationForm(NewCompensationForm):
self.instance.save()
self.instance.log.add(action)
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 intervention.models import Handler, Responsibility, Legal
from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm
from user.models import User, UserActionLogEntry
@@ -93,8 +94,6 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
# Create log entry
action = UserActionLogEntry.get_created_action(user)
# Process the geometry form
geometry = geom_form.save(action)
handler = Handler.objects.create(
type=handler_type,
@@ -118,7 +117,7 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
responsible=responsible,
deductable_surface=surface,
created=action,
geometry=geometry,
modified=action,
comment=comment,
is_pik=is_pik,
legal=legal
@@ -127,6 +126,12 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
# Add the log entry to the main objects log list
acc.log.add(action)
# Process the geometry form
geometry = geom_form.save(action)
acc.geometry = geometry
acc.save()
acc.update_deductable_rest()
return acc
@@ -182,9 +187,6 @@ class EditEcoAccountForm(NewEcoAccountForm):
# Create log entry
action = UserActionLogEntry.get_edited_action(user)
# Process the geometry form
geometry = geom_form.save(action)
# Update responsible data
self.instance.responsible.handler.type = handler_type
self.instance.responsible.handler.detail = handler_detail
@@ -201,7 +203,6 @@ class EditEcoAccountForm(NewEcoAccountForm):
self.instance.identifier = identifier
self.instance.title = title
self.instance.deductable_surface = surface
self.instance.geometry = geometry
self.instance.comment = comment
self.instance.is_pik = is_pik
self.instance.modified = action
@@ -209,4 +210,23 @@ class EditEcoAccountForm(NewEcoAccountForm):
# Add the log entry to the main objects log list
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
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")
m = "m", _("m")
m2 = "m2", _("")
m3 = "m3", _("")
km = "km", _("km")
qm = "qm", _("")
ha = "ha", _("ha")
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",
default=0,
)
deductable_rest = models.FloatField(
blank=True,
null=True,
help_text="Amount of deductable rest",
default=0,
)
legal = models.OneToOneField(
"intervention.Legal",
@@ -100,28 +106,29 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
"""
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
Args:
Returns:
ret_val_total (float): Total amount
ret_val_relative (float): Amount as percentage (0-100)
"""
deductions = self.deductions.filter(
intervention__deleted=None,
)
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:
ret_val_relative = int((ret_val_total / available_surfaces) * 100)
available_surface = self.deductable_surface
if available_surface is None:
# Fallback!
available_surface = deductions_surfaces
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:
""" Quality check
@@ -181,6 +188,29 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
for team_id in shared_teams:
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):
"""
@@ -272,3 +302,8 @@ class EcoAccountDeduction(BaseResource):
self.intervention.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED)
self.account.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED)
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.models import Compensation
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
class CompensationTable(BaseTable, TableRenderMixin):
class CompensationTable(BaseTable, TableRenderMixin, TableOrderMixin):
id = tables.Column(
verbose_name=_("Identifier"),
orderable=True,
@@ -31,7 +31,7 @@ class CompensationTable(BaseTable, TableRenderMixin):
)
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=True,
orderable=False,
accessor="geometry",
)
c = tables.Column(
@@ -126,28 +126,6 @@ class CompensationTable(BaseTable, TableRenderMixin):
)
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):
""" Renders the registered column for a compensation
@@ -170,20 +148,3 @@ class CompensationTable(BaseTable, TableRenderMixin):
)
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.models import EcoAccount
from konova.utils.tables import TableRenderMixin, BaseTable
from konova.utils.tables import TableRenderMixin, BaseTable, TableOrderMixin
import django_tables2 as tables
class EcoAccountTable(BaseTable, TableRenderMixin):
class EcoAccountTable(BaseTable, TableRenderMixin, TableOrderMixin):
id = tables.Column(
verbose_name=_("Identifier"),
orderable=True,
@@ -31,13 +31,13 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
)
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=True,
orderable=False,
accessor="geometry",
)
av = tables.Column(
verbose_name=_("Available"),
orderable=True,
empty_values=[],
accessor="deductable_rest",
attrs={
"th": {
"class": "w-20",
@@ -100,38 +100,19 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
""" Renders the available column for an eco account
Args:
value (str): The identifier value
value (float): The deductable_rest
record (EcoAccount): The eco account record
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})
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):
""" Renders the recorded column for an eco account
@@ -153,23 +134,3 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
icn_filled=checked,
)
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 %}
</td>
</tr>
{% empty %}
<div class="alert alert-danger mb-0">
{% trans 'Missing' %}
</div>
{% endfor %}
</tbody>
</table>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,7 +47,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
"identifier": test_id,
"title": test_title,
"geom": geom_json,
"deductable_surface": test_deductable_surface,
"surface": test_deductable_surface,
"conservation_office": test_conservation_office.id
}
self.client_user.post(new_url, post_data)
@@ -61,8 +61,11 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.assertEqual(acc.identifier, test_id)
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.assertEqual(acc.log.count(), 1)
self.assertEqual(acc.created, acc.modified)
# Expect logs to be set
self.assertEqual(acc.log.count(), 1)
@@ -84,7 +87,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
new_comment = self.create_dummy_string()
new_geometry = MultiPolygon(srid=4326) # Create an empty geometry
test_conservation_office = self.get_conservation_office_code()
test_deductable_surface = 10005
test_deductable_surface = self.eco_account.deductable_surface + 100
check_on_elements = {
self.eco_account.title: new_title,
@@ -110,6 +113,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.eco_account.title: new_title,
self.eco_account.identifier: new_identifier,
self.eco_account.deductable_surface: test_deductable_surface,
self.eco_account.deductable_rest: test_deductable_surface,
self.eco_account.comment: new_comment,
}
@@ -185,7 +189,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
# Prepare data for deduction creation
deduct_url = reverse("compensation:acc:new-deduction", args=(self.eco_account.id,))
test_surface = 10.00
test_surface = 10.50
post_data = {
"surface": test_surface,
"account": self.eco_account.id,
@@ -194,17 +198,17 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
# Perform request --> expect to fail
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.intervention.deductions.count())
self.assertEqual(pre_deduction_acc_log_count, 0)
self.assertEqual(pre_deduction_int_log_count, 0)
# 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.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.assertIsNotNone(self.eco_account.recorded)
self.assertGreater(self.eco_account.deductable_surface, test_surface)
@@ -216,10 +220,12 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.client_user.post(deduct_url, post_data)
# 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.intervention.deductions.count())
deduction = self.eco_account.deductions.first()
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.intervention, self.intervention)
@@ -230,7 +236,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.assertTrue(self.intervention.log.first().action == UserAction.EDITED)
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.intervention.share_with_user(self.superuser)
self.eco_account.refresh_from_db()
@@ -239,7 +245,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
deduction = EcoAccountDeduction.objects.create(
intervention=self.intervention,
account=self.eco_account,
surface=0
surface=1.10
)
self.assertEqual(1, self.intervention.deductions.count())
self.assertEqual(1, self.eco_account.deductions.count())
@@ -262,6 +268,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.eco_account.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_account, self.eco_account.deductions.count())
self.assertEqual(deduction.surface, test_surface)
@@ -275,6 +282,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
def test_remove_deduction(self):
intervention = self.deduction.intervention
account = self.deduction.account
deducted_surface = self.deduction.surface
# Prepare url and form data to be posted
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_account_log_count = account.log.count()
pre_edit_account_rest = account.deductable_rest
num_deductions_intervention = intervention.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_account - 1, account.deductions.count())
self.assertEqual(account.deductable_rest, pre_edit_account_rest + deducted_surface)
# Expect logs to be set
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(
deleted=None, # only show those which are not deleted individually
intervention__deleted=None, # and don't show the ones whose intervention has been deleted
).order_by(
"-modified__timestamp"
)
table = CompensationTable(
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.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.tables.eco_account import EcoAccountTable
from konova.contexts import BaseContext
from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal
from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm
from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP
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, \
@@ -42,6 +41,8 @@ def index_view(request: HttpRequest):
template = "generic_index.html"
eco_accounts = EcoAccount.objects.filter(
deleted=None,
).order_by(
"-modified__timestamp"
)
table = EcoAccountTable(
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
diff_states = abs(sum_before_states - sum_after_states)
# 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
deductions = acc.deductions.filter(
@@ -259,7 +261,7 @@ def remove_view(request: HttpRequest, id: str):
messages.info(request, CANCEL_ACC_RECORDED_OR_DEDUCTED)
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(
request=request,
msg_success=_("Eco-account removed"),

View File

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

View File

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

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.template.loader import render_to_string
from django.utils.html import format_html
from django.utils.timezone import localtime
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
import django_tables2 as tables
from konova.sub_settings.django_settings import DEFAULT_DATE_TIME_FORMAT
from konova.utils.tables import BaseTable, TableRenderMixin
from konova.utils.tables import BaseTable, TableRenderMixin, TableOrderMixin
from ema.filters import EmaTableFilter
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
in the future by inheriting.
@@ -37,7 +34,7 @@ class EmaTable(BaseTable, TableRenderMixin):
)
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=True,
orderable=False,
accessor="geometry",
)
r = tables.Column(
@@ -93,28 +90,6 @@ class EmaTable(BaseTable, TableRenderMixin):
)
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):
""" Renders the registered column for a EMA
@@ -136,22 +111,3 @@ class EmaTable(BaseTable, TableRenderMixin):
icn_filled=recorded,
)
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 %}
</td>
</tr>
{% empty %}
<div class="alert alert-danger mb-0">
{% trans 'Missing' %}
</div>
{% endfor %}
</tbody>
</table>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -263,9 +263,6 @@ class NewInterventionForm(BaseForm):
handler=handler,
)
# Process the geometry form
geometry = geom_form.save(action)
# Finally create main object, holding the other objects
intervention = Intervention.objects.create(
identifier=identifier,
@@ -273,7 +270,7 @@ class NewInterventionForm(BaseForm):
responsible=responsibility_data,
legal=legal_data,
created=action,
geometry=geometry,
modified=action,
comment=comment,
)
@@ -282,6 +279,12 @@ class NewInterventionForm(BaseForm):
# Add the performing user as the first user having access to the data
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
@@ -370,9 +373,6 @@ class EditInterventionForm(NewInterventionForm):
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.identifier = identifier
@@ -381,5 +381,10 @@ class EditInterventionForm(NewInterventionForm):
self.instance.modified = user_action
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

View File

@@ -163,6 +163,10 @@ class NewEcoAccountDeductionModalForm(BaseModalForm):
self.cleaned_data["account"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED)
return deduction
def check_for_recorded_instance(self):
# Ignore super() implementation
return
class EditEcoAccountDeductionModalForm(NewEcoAccountDeductionModalForm):
deduction = None
@@ -231,6 +235,16 @@ class EditEcoAccountDeductionModalForm(NewEcoAccountDeductionModalForm):
old_account.send_notification_mail_on_deduction_change(data_changes)
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):
""" Removing modal form for EcoAccountDeduction
@@ -249,4 +263,8 @@ class RemoveEcoAccountDeductionModalForm(RemoveModalForm):
with transaction.atomic():
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.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):
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.models import Intervention
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
class InterventionTable(BaseTable, TableRenderMixin):
class InterventionTable(BaseTable, TableRenderMixin, TableOrderMixin):
id = tables.Column(
verbose_name=_("Identifier"),
orderable=True,
@@ -31,7 +31,7 @@ class InterventionTable(BaseTable, TableRenderMixin):
)
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=True,
orderable=False,
accessor="geometry",
)
c = tables.Column(
@@ -124,28 +124,6 @@ class InterventionTable(BaseTable, TableRenderMixin):
)
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):
""" Renders the recorded column for an intervention
@@ -168,22 +146,3 @@ class InterventionTable(BaseTable, TableRenderMixin):
)
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>
{% 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">
<table class="table table-hover">
<thead>

View File

@@ -11,7 +11,7 @@ from django.core.exceptions import ObjectDoesNotExist
from django.urls import reverse
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.tests.test_views import BaseWorkflowTestCase
from user.models import UserActionLogEntry, UserAction
@@ -78,6 +78,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
self.assertEqual(1, obj.log.count())
self.assertEqual(obj.log.first().action, UserAction.CREATED)
self.assertEqual(obj.log.first().user, self.superuser)
self.assertEqual(obj.created, obj.modified)
except ObjectDoesNotExist:
# Fail if there is no such object
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")
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
self.client_user.post(check_url, post_data)
# Update intervention 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
# in an unwanted assertion error
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")
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
self.client_user.post(record_url, post_data)

View File

@@ -11,6 +11,7 @@ import json
import pika
import xmltodict
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 konova.sub_settings.django_settings import DEFAULT_DATE_FORMAT
@@ -92,6 +93,9 @@ class EgonGmlBuilder:
)["summed"]
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):
comp_type = "Ersatzzahlung"
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}",
"#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": {
"@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/88140/{comp_type_code}",
"#text": comp_type

View File

@@ -21,8 +21,21 @@ class InterventionQualityChecker(AbstractQualityChecker):
self._check_legal_data()
self._check_compensations()
self._check_geometry()
self._check_payment_documents()
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):
""" Checks data quality of related Responsibility

View File

@@ -45,6 +45,8 @@ def index_view(request: HttpRequest):
deleted=None, # not deleted
).select_related(
"legal"
).order_by(
"-modified__timestamp"
)
table = InterventionTable(
request=request,
@@ -157,6 +159,8 @@ def detail_view(request: HttpRequest, id: str):
if last_checked:
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 = {
"obj": intervention,
"last_checked": last_checked,
@@ -168,6 +172,7 @@ def detail_view(request: HttpRequest, id: str):
"is_zb_member": in_group(_user, ZB_GROUP),
"is_ets_member": in_group(_user, ETS_GROUP),
"LANIS_LINK": intervention.get_LANIS_link(),
"has_payment_without_document": has_payment_without_document,
TAB_TITLE_IDENTIFIER: f"{intervention.identifier} - {intervention.title}",
}

View File

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

View File

@@ -134,24 +134,21 @@ class BaseForm(forms.Form):
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_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
return
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.forms import MultiPolygonField
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 konova.forms.base_form import BaseForm
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 user.models import UserActionLogEntry
@@ -62,6 +63,7 @@ class SimpleGeomForm(BaseForm):
geom = self.data["geom"]
if geom is None or len(geom) == 0:
# empty geometry is a valid geometry
self.cleaned_data["geom"] = MultiPolygon(srid=DEFAULT_SRID_RLP).ewkt
return is_valid
geom = json.loads(geom)
@@ -74,9 +76,26 @@ class SimpleGeomForm(BaseForm):
# this case)
features = []
features_json = geom.get("features", [])
accepted_ogr_types = [
"Polygon",
"Polygon25D",
"MultiPolygon",
"MultiPolygon25D",
]
for feature in features_json:
g = gdal.OGRGeometry(json.dumps(feature.get("geometry", feature)), srs=DEFAULT_SRID_RLP)
if g.geom_type not in ["Polygon", "MultiPolygon"]:
feature_geom = feature.get("geometry", feature)
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."))
is_valid = False
return is_valid
@@ -88,6 +107,8 @@ class SimpleGeomForm(BaseForm):
return is_valid
features.append(polygon)
# Unionize all geometry features into one new MultiPolygon
form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
for feature in features:
form_geom = form_geom.union(feature)
@@ -128,6 +149,17 @@ class SimpleGeomForm(BaseForm):
geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)),
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)
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)
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
from django.contrib.gis.db.models import MultiPolygonField
from django.contrib.gis.geos import Polygon
from django.db import models, transaction
from django.utils import timezone
from konova.models import BaseResource, UuidModel
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
@@ -29,7 +27,18 @@ class Geometry(BaseResource):
def save(self, *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):
""" Checks for new geometry overlaps
@@ -44,9 +53,8 @@ class Geometry(BaseResource):
return None
self.recheck_existing_conflicts()
overlapping_geoms = Geometry.objects.filter(
geom__intersects=self.geom,
geom__intersects=self.geom_small_buffered,
).exclude(
id=self.id
).distinct()
@@ -68,14 +76,14 @@ class Geometry(BaseResource):
"""
all_conflicts_as_conflicting = self.conflicts_geometries.all()
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.delete()
all_conflicted_by_conflicts = self.conflicted_by_geometries.all()
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.delete()
@@ -108,6 +116,11 @@ class Geometry(BaseResource):
"""
from konova.models import Parcel, District, ParcelIntersection, Municipal, ParcelGroup
if self.geom.empty:
# Nothing to do
return
parcel_fetcher = ParcelWFSFetcher(
geometry_id=self.id,
)
@@ -213,16 +226,22 @@ class Geometry(BaseResource):
geojson (dict): The FeatureCollection json (as dict)
"""
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 = {
"type": "FeatureCollection",
"crs": {
"type": "name",
"properties": {
"name": f"urn:ogc:def:crs:EPSG::{geom.srid}"
}
},
"features": [
json.loads(x.geojson) for x in polygons
{
"type": "Feature",
"geometry": json.loads(geom.json),
}
]
}
return geojson

View File

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

View File

@@ -10,5 +10,5 @@ BASE_TITLE_SHORT = "KSP"
BASE_TITLE = "KSP - Kompensationsverzeichnis Service Portal"
BASE_FRONTEND_TITLE = "Kompensationsverzeichnis Service Portal"
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"

View File

@@ -15,7 +15,7 @@ DEFAULT_SRID_RLP = 25832
# Needed to redirect to LANIS
## 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.
LANIS_ZOOM_LUT = {
1000000000: 6,

View File

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

View File

@@ -2,17 +2,32 @@
<div class="col-sm-12">
<div class="card">
<div class="card-header rlp-r">
<h5>
{% fa5_icon 'search-location' %}
{% trans 'Spatial reference' %}
</h5>
<div class="row">
<div class="col-6">
<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 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 class="row justify-content-center">
<span class="spinner-border rlp-r-inv" role="status"></span>
</div>
</div>
{% else %}
<div class="alert alert-danger">
{% translate 'No geometry entry found on database. Please contact an admin!' %}
</div>
{% endif %}
</div>
</div>
</div>

View File

@@ -115,6 +115,19 @@ class BaseTestCase(TestCase):
"""
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):
""" Creates an intervention which can be used for tests
@@ -187,6 +200,7 @@ class BaseTestCase(TestCase):
eco_account = EcoAccount.objects.create(
identifier="TEST",
title="Test_title",
deductable_surface=500,
legal=lega_data,
responsible=responsible_data,
created=action,

View File

@@ -92,11 +92,14 @@ class Mailer:
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
Args:
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:
@@ -108,18 +111,21 @@ class Mailer:
"EMAIL_REPLY_TO": EMAIL_REPLY_TO,
}
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(
user_mail_address,
_("{} - Shared access given").format(obj_identifier),
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
Args:
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:
@@ -131,18 +137,21 @@ class Mailer:
"EMAIL_REPLY_TO": EMAIL_REPLY_TO,
}
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(
user_mail_address,
_("{} - Shared access removed").format(obj_identifier),
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
Args:
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:
@@ -154,18 +163,21 @@ class Mailer:
"EMAIL_REPLY_TO": EMAIL_REPLY_TO,
}
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(
user_mail_address,
_("{} - Shared data unrecorded").format(obj_identifier),
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
Args:
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:
@@ -177,18 +189,21 @@ class Mailer:
"EMAIL_REPLY_TO": EMAIL_REPLY_TO,
}
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(
user_mail_address,
_("{} - Shared data recorded").format(obj_identifier),
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
Args:
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:
@@ -200,14 +215,14 @@ class Mailer:
"EMAIL_REPLY_TO": EMAIL_REPLY_TO,
}
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(
user_mail_address,
_("{} - Shared data checked").format(obj_identifier),
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
Args:
@@ -215,7 +230,7 @@ class Mailer:
obj_title (str): Title of the main object
team (Team): Team to be notified
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:
"""
@@ -227,14 +242,14 @@ class Mailer:
"data_changes": data_changes,
}
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(
user_mail_address,
_("{} - Deduction changed").format(obj_identifier),
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
Args:
@@ -250,7 +265,7 @@ class Mailer:
"EMAIL_REPLY_TO": EMAIL_REPLY_TO,
}
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(
user_mail_address,
_("{} - 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.db.models import F
from django.http import HttpRequest
from django.template.loader import render_to_string
from django.utils.html import format_html
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
@@ -173,4 +176,56 @@ class TableRenderMixin:
max_length = 75
if len(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
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 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:
@@ -72,33 +73,34 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
self.geometry_property_name = geometry_property_name
def _create_spatial_filter(self,
geometry_operation: str,
filter_srid: str = None):
geometry_operation: str):
""" 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:
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:
spatial_filter (str): The spatial filter element
"""
from konova.models import Geometry
if filter_srid is None:
filter_srid = DEFAULT_SRID_RLP
geom_gml = Geometry.objects.filter(
geom = Geometry.objects.filter(
id=self.geometry_id
).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(
gml=AsGML(MakeValid('transformed'))
).first().gml
gml=AsGML(MakeValid('smaller'))
).first()
geom_gml = geom.gml
spatial_filter = f"<Filter><{geometry_operation}><PropertyName>{self.geometry_property_name}</PropertyName>{geom_gml}</{geometry_operation}></Filter>"
return spatial_filter
def _create_post_data(self,
geometry_operation: str,
filter_srid: str = None,
typenames: str = None,
start_index: int = 0,
):
@@ -106,15 +108,13 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
Args:
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:
_filter (str): A proper xml WFS filter
"""
start_index = str(start_index)
spatial_filter = self._create_spatial_filter(
geometry_operation,
filter_srid
geometry_operation
)
_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
@@ -144,7 +144,6 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
while start_index is not None:
post_body = self._create_post_data(
spatial_operator,
filter_srid,
typenames,
start_index
)

View File

@@ -32,16 +32,16 @@ def get_geom_parcels(request: HttpRequest, id: str):
parcels = geom.get_underlying_parcels()
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
no_geometry_given = geos_geom is None
if parcels_are_currently_calculated:
# Parcels are being calculated right now. Change the status code, so polling stays active for fetching
# resutls after the calculation
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")
municipals = parcels.order_by("municipal").distinct("municipal").values("municipal__id")
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.db.models import Q
from django.http import HttpRequest
from django.shortcuts import render
from django.utils import timezone
@@ -50,22 +51,22 @@ def home_view(request: HttpRequest):
)
# Then fetch only user related ones
user_interventions = interventions.filter(
users__in=[user]
)
Q(users__in=[user]) | Q(teams__in=user.shared_teams)
).distinct()
# Repeat for other objects
comps = Compensation.objects.filter(
deleted=None,
)
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(
deleted=None,
)
user_ecco_accs = eco_accs.filter(
users__in=[user]
)
Q(users__in=[user]) | Q(teams__in=user.shared_teams)
).distinct()
additional_context = {
"msgs": msgs,

Binary file not shown.

View File

@@ -43,7 +43,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\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/eco_account/includes/deductions.html:31
#: 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/templates/intervention/detail/view.html:87
#: user/models/user_action.py:22
@@ -357,7 +357,7 @@ msgid "Show only unrecorded"
msgstr "Nur unverzeichnete anzeigen"
#: 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/templates/intervention/detail/includes/compensations.html:30
msgid "Identifier"
@@ -376,12 +376,12 @@ msgstr "Automatisch generiert"
#: compensation/templates/compensation/detail/eco_account/view.html:32
#: compensation/templates/compensation/report/compensation/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/report/report.html:12
#: intervention/forms/intervention.py:41 intervention/tables.py:28
#: 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/report/report.html:12
#: konova/forms/modals/document_form.py:24
@@ -411,7 +411,7 @@ msgstr "Kompensation XY; Flur ABC"
#: ema/templates/ema/detail/includes/documents.html:34
#: intervention/forms/intervention.py:199
#: 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/revocation.html:38
#: konova/forms/modals/document_form.py:59
@@ -446,37 +446,43 @@ msgstr "Neue Kompensation"
msgid "Edit compensation"
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"
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"
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/utils/quality.py:83
msgid "Agreement date"
msgstr "Vereinbarungsdatum"
#: compensation/forms/eco_account.py:43
#: compensation/forms/eco_account.py:44
msgid "When did the parties agree on this?"
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
msgid "New Eco-Account"
msgstr "Neues Ökokonto"
#: compensation/forms/eco_account.py:78
#: compensation/forms/eco_account.py:79
msgid "Eco-Account XY; Location ABC"
msgstr "Ökokonto XY; Flur ABC"
#: compensation/forms/eco_account.py:140
#: compensation/forms/eco_account.py:143
msgid "Edit Eco-Account"
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/templates/compensation/detail/eco_account/view.html:63
#: compensation/templates/compensation/report/eco_account/report.html:20
@@ -734,14 +740,14 @@ msgstr ""
msgid "Pieces"
msgstr "Stück"
#: compensation/models/eco_account.py:56
#: compensation/models/eco_account.py:62
msgid ""
"Deductable surface can not be larger than existing surfaces in after states"
msgstr ""
"Die abbuchbare Fläche darf die Gesamtfläche der Zielzustände nicht "
"überschreiten"
#: compensation/models/eco_account.py:63
#: compensation/models/eco_account.py:69
msgid ""
"Deductable surface can not be smaller than the sum of already existing "
"deductions. Please contact the responsible users for the deductions!"
@@ -750,54 +756,42 @@ msgstr ""
"wollen. Kontaktieren Sie die für die Abbuchungen verantwortlichen Nutzer!"
#: 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
msgid "Parcel gmrkng"
msgstr "Gemarkung"
#: 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"
msgstr "Freigegeben"
#: 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"
msgstr "Zuletzt bearbeitet"
#: 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 {}"
msgstr "Öffne {}"
#: compensation/tables/compensation.py:163
#: compensation/tables/compensation.py:141
#: compensation/templates/compensation/detail/compensation/view.html:96
#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:58
#: compensation/templates/compensation/detail/eco_account/view.html:48
#: ema/tables.py:130 ema/templates/ema/detail/view.html:38
#: intervention/tables.py:161
#: ema/tables.py:105 ema/templates/ema/detail/view.html:38
#: intervention/tables.py:139
#: intervention/templates/intervention/detail/view.html:90
msgid "Not recorded yet"
msgstr "Noch nicht verzeichnet"
#: compensation/tables/compensation.py:166
#: compensation/tables/eco_account.py:150 ema/tables.py:133
#: intervention/tables.py:164
#: compensation/tables/compensation.py:144
#: compensation/tables/eco_account.py:131 ema/tables.py:108
#: intervention/tables.py:142
msgid "Recorded on {} by {}"
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/templates/compensation/detail/eco_account/view.html:36
#: konova/templates/konova/widgets/progressbar.html:3
@@ -808,7 +802,7 @@ msgstr "Verfügbar"
msgid "Eco Accounts"
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."
msgstr ""
"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
#: intervention/templates/intervention/detail/includes/compensations.html:38
#: 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/revocation.html:43
#: templates/log.html:10 user/templates/user/team/index.html:33
@@ -876,6 +870,34 @@ msgstr "Keine Zusatzmerkmale"
msgid "Remove action"
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/eco_account/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/eco_account/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
msgid "Created on"
msgstr "Erstellt"
@@ -971,7 +993,7 @@ msgstr "Erstellt"
#: compensation/templates/compensation/detail/compensation/includes/documents.html:63
#: compensation/templates/compensation/detail/eco_account/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
msgid "Edit document"
msgstr "Dokument bearbeiten"
@@ -979,7 +1001,7 @@ msgstr "Dokument bearbeiten"
#: compensation/templates/compensation/detail/compensation/includes/documents.html:66
#: compensation/templates/compensation/detail/eco_account/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"
msgstr "Dokument löschen"
@@ -1186,7 +1208,7 @@ msgid "Recorded on"
msgstr "Verzeichnet am"
#: 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
msgid "Edit Deduction"
msgstr "Abbuchung bearbeiten"
@@ -1200,25 +1222,6 @@ msgstr "Abbuchung entfernen"
msgid "No surface deductable"
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
#: ema/templates/ema/detail/view.html:57
msgid "Action handler"
@@ -1274,8 +1277,8 @@ msgid "Compensation {} edited"
msgstr "Kompensation {} bearbeitet"
#: compensation/views/compensation/compensation.py:185
#: compensation/views/eco_account/eco_account.py:159 ema/views/ema.py:211
#: intervention/views/intervention.py:225
#: compensation/views/eco_account/eco_account.py:159 ema/views/ema.py:210
#: intervention/views/intervention.py:228
msgid "Edit {}"
msgstr "Bearbeite {}"
@@ -1297,11 +1300,11 @@ msgstr "Ökokonto {} hinzugefügt"
msgid "Eco-Account {} edited"
msgstr "Ökokonto {} bearbeitet"
#: compensation/views/eco_account/eco_account.py:263
#: compensation/views/eco_account/eco_account.py:265
msgid "Eco-account removed"
msgstr "Ökokonto entfernt"
#: ema/forms.py:42 ema/views/ema.py:94
#: ema/forms.py:42 ema/views/ema.py:93
msgid "New EMA"
msgstr "Neue EMA hinzufügen"
@@ -1309,11 +1312,11 @@ msgstr "Neue EMA hinzufügen"
msgid "Edit 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"
msgstr "Ersatzzahlungsmaßnahmen (EMA)"
#: ema/tables.py:66
#: ema/tables.py:63
msgid "EMA explanation"
msgstr ""
"EMA sind Kompensationen, die durch Ersatzzahlungen finanziert wurden. "
@@ -1321,7 +1324,7 @@ msgstr ""
"Maßnahmen aus Ersatzzahlungen, die nach 2015 rechtskräftig wurden, werden "
"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"
msgstr ""
@@ -1329,15 +1332,15 @@ msgstr ""
msgid "Payment funded compensation"
msgstr "Ersatzzahlungsmaßnahme"
#: ema/views/ema.py:51
#: ema/views/ema.py:50
msgid "EMAs - Overview"
msgstr "EMAs - Übersicht"
#: ema/views/ema.py:84
#: ema/views/ema.py:83
msgid "EMA {} added"
msgstr "EMA {} hinzugefügt"
#: ema/views/ema.py:201
#: ema/views/ema.py:200
msgid "EMA {} edited"
msgstr "EMA {} bearbeitet"
@@ -1551,6 +1554,13 @@ msgstr "Ökokonto gelöscht! Abbuchung ungültig!"
msgid "Eco-account not recorded! Deduction invalid!"
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/report/report.html:69
msgid "Payments"
@@ -1635,23 +1645,23 @@ msgstr "Eingriffe - Übersicht"
msgid "Intervention {} added"
msgstr "Eingriff {} hinzugefügt"
#: intervention/views/intervention.py:213
#: intervention/views/intervention.py:216
msgid "Intervention {} edited"
msgstr "Eingriff {} bearbeitet"
#: intervention/views/intervention.py:249
#: intervention/views/intervention.py:253
msgid "{} removed"
msgstr "{} entfernt"
#: konova/decorators.py:33
#: konova/decorators.py:30
msgid "You need to be staff to perform this action!"
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!"
msgstr "Hierfür müssen Sie Administrator sein!"
#: konova/decorators.py:66
#: konova/decorators.py:63
msgid ""
"+++ Attention: You are not part of any group. You won't be able to create, "
"edit or do anything. Please contact an administrator. +++"
@@ -1673,7 +1683,7 @@ msgid "Search for file number"
msgstr "Nach Aktenzeichen suchen"
#: 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"
msgstr "Kreis"
@@ -1686,7 +1696,7 @@ msgid "Search for parcel gmrkng"
msgstr "Nach Gemarkung suchen"
#: 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"
msgstr "Flur"
@@ -1695,7 +1705,7 @@ msgid "Search for parcel"
msgstr "Nach Flur suchen"
#: 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"
msgstr "Flurstückzähler"
@@ -1704,7 +1714,7 @@ msgid "Search for parcel counter"
msgstr "Nach Flurstückzähler suchen"
#: 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"
msgstr "Flurstücknenner"
@@ -1860,37 +1870,37 @@ msgstr ""
msgid "English"
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."
msgstr ""
"Flurstücke können nicht berechnet werden, da keine Geometrie eingegeben "
"wurde."
#: konova/templates/konova/includes/parcels/parcel_table_frame.html:11
#: konova/templates/konova/includes/parcels/parcel_table_frame.html:12
msgid "Parcels found"
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"
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"
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"
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"
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"
msgstr "Gemarkungsschlüssel"
#: konova/templates/konova/includes/parcels/parcels.html:7
#: konova/templates/konova/includes/parcels/parcels.html:9
msgid "Spatial reference"
msgstr "Raumreferenz"
@@ -1938,39 +1948,39 @@ msgstr "In Zwischenablage kopiert"
msgid "Search"
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"
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"
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"
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"
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"
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"
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"
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"
msgstr "Anfrage für neuen API Token"
#: konova/utils/mailer.py:420
#: konova/utils/mailer.py:435
msgid "Resubmission - {}"
msgstr "Wiedervorlage - {}"
@@ -2210,9 +2220,17 @@ msgstr "{} wurde erfolgreich vom Nutzer {} geprüft! {}"
#: konova/utils/quality.py:32
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"
msgstr "Home"
@@ -2289,8 +2307,8 @@ msgid "Server Error"
msgstr ""
#: templates/500.html:10
msgid "Something happened. We are working on it!"
msgstr "Irgendetwas ist passiert. Wir arbeiten daran!"
msgid "Something happened. Admins have been informed. We are working on it!"
msgstr "Irgendetwas ist passiert. Die Administratoren wurden informiert. Wir arbeiten daran!"
#: templates/email/api/verify_token.html:7
msgid "Hello support"
@@ -2657,13 +2675,15 @@ msgstr ""
msgid "User"
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."
msgstr "Keine Geometrie vorhanden"
#: templates/modal/modal_session_timed_out.html:3
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
msgid "Kompensationsverzeichnis Service Portal"
@@ -2710,7 +2730,7 @@ msgstr ""
"vorbei. \n"
" "
#: templates/table/gmrkng_col.html:6
#: templates/table/gmrkng_col.html:12
msgid ""
"If the geometry is not empty, the parcels are currently recalculated. Please "
"refresh this page in a few moments."
@@ -2882,35 +2902,27 @@ msgstr ""
" "
#: 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"
msgstr "Benachrichtigungseinstellungen ändern"
#: user/templates/user/index.html:53
#: user/templates/user/index.html:45
msgid "Notification settings"
msgstr "Benachrichtigungen"
#: user/templates/user/index.html:58
#: user/templates/user/index.html:50
msgid "Manage teams"
msgstr ""
#: user/templates/user/index.html:61 user/templates/user/team/index.html:19
#: user/views.py:169
#: user/templates/user/index.html:53 user/templates/user/team/index.html:19
#: user/views.py:171
msgid "Teams"
msgstr ""
#: user/templates/user/index.html:66
#: user/templates/user/index.html:58
msgid "See or edit your API token"
msgstr "API token einsehen oder neu generieren"
#: user/templates/user/index.html:69
#: user/templates/user/index.html:61
msgid "API"
msgstr ""
@@ -2974,23 +2986,23 @@ msgstr "Neuer Token generiert. Administratoren sind informiert."
msgid "User API token"
msgstr "API Nutzer Token"
#: user/views.py:180
#: user/views.py:183
msgid "New team added"
msgstr "Neues Team hinzugefügt"
#: user/views.py:194
#: user/views.py:198
msgid "Team edited"
msgstr "Team bearbeitet"
#: user/views.py:208
#: user/views.py:213
msgid "Team removed"
msgstr "Team gelöscht"
#: user/views.py:222
#: user/views.py:228
msgid "You are not a member of this team"
msgstr "Sie sind kein Mitglied dieses Teams"
#: user/views.py:229
#: user/views.py:235
msgid "Left Team"
msgstr "Team verlassen"
@@ -4493,6 +4505,12 @@ msgstr ""
msgid "Unable to connect to qpid with SASL mechanism %s"
msgstr ""
#~ msgid "Change default configuration for your KSP map"
#~ msgstr "Karteneinstellungen ändern"
#~ msgid "Map settings"
#~ msgstr "Karte"
#~ msgid "There are errors on this intervention:"
#~ msgstr "Es liegen Fehler in diesem Eingriff vor:"

View File

@@ -7,7 +7,7 @@
<h1 class="display-4">{% trans 'Server Error' %}</h1>
<hr>
<p class="lead">
{% trans 'Something happened. We are working on it!' %}
{% trans 'Something happened. Admins have been informed. We are working on it!' %}
</p>
</div>
{% 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": "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": 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": "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": "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": "TopPlusOpen", "attribution": "BKG", "url": "https://sgx.geodatenzentrum.de/wms_topplus_open?", "name": "web", "active": false },
{ "folder": 0, "type": "OSM", "title": "Open Street Map", "attribution": "OSM", "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": 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":
@@ -57,7 +59,10 @@
{ "title": "MAE", "parent": 4 },
{ "title": "Schutzgebiete", "parent": 3 },
{ "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":

View File

@@ -394,16 +394,12 @@ netgis.MapOpenLayers.prototype.clearAll = function()
{
for ( var i = 0; i < this.layers.length; i++ )
{
if(this.layers[i] === this.editLayer){
continue;
};
this.map.removeLayer( this.layers[ i ] );
}
this.layers = [this.editLayer];
this.layers = [];
this.snapFeatures.clear();
this.snapFeatures.push(this.editLayer);
};
netgis.MapOpenLayers.prototype.onUpdateStyle = function( e )
@@ -1116,10 +1112,15 @@ netgis.MapOpenLayers.prototype.updateEditOutput = function()
{
var features = this.editLayer.getSource().getFeatures();
// Output
var proj = this.client.config.map.projection;
var format = new ol.format.GeoJSON();
//var output = format.writeFeatures( features );
var output = format.writeFeaturesObject( features );
var output = format.writeFeaturesObject( features, { dataProjection: proj, featureProjection: proj } );
output[ "crs" ] =
{
"type": "name",
"properties": { "name": "urn:ogc:def:crs:" + proj.replace( ":", "::" ) }
};
if ( ! this.editEventsSilent )
this.client.invoke( netgis.Events.EDIT_FEATURES_CHANGE, output );
@@ -1139,15 +1140,9 @@ netgis.MapOpenLayers.prototype.updateEditLayerItem = function()
netgis.MapOpenLayers.prototype.onEditFeaturesLoaded = function( e )
{
var json = e;
var format = new ol.format.GeoJSON();
var features = format.readFeatures( json );
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 ] } );
var json = e;
var self = this;
window.setTimeout( function() { self.createLayerGeoJSON( "Import", json ); }, 10 );
};
netgis.MapOpenLayers.prototype.onDragEnter = function( e )
@@ -1254,7 +1249,6 @@ netgis.MapOpenLayers.prototype.onImportShapefile = function( e )
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 projection = format.readProjection( data );
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: 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 );
};
@@ -1274,19 +1287,142 @@ netgis.MapOpenLayers.prototype.createLayerGML = function( title, data )
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.WFS();
//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, { 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 );
};
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 )
{
var self = this;
@@ -1312,22 +1448,20 @@ netgis.MapOpenLayers.prototype.addImportedFeatures = function( features )
this.editLayer.getSource().addFeatures( features );
this.editEventsSilent = false;
this.updateEditOutput();
/*
// 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 );
// Zoom Imported Features
if ( features.length > 0 )
this.view.fit( layer.getSource().getExtent(), {} );
this.client.invoke( netgis.Events.LAYER_CREATED, { id: id, title: title, checked: true, folder: "import" } );
*/
{
var extent = features[ 0 ].getGeometry().getExtent();
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 )

View File

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

View File

@@ -1,4 +1,4 @@
{% load i18n %}
{% load i18n fontawesome_5 %}
{% comment %}
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 %}
<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>
{% endif %}

View File

@@ -1,9 +1,16 @@
{% load i18n fontawesome_5 %}
{% 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' %}
{% if geometry.geom is None or geometry.geom.empty %}
<span class="text-info" title="{% translate 'No geometry added, yet.' %}">
{% fa5_icon 'search-location' %}
{% fa5_icon 'question' %}
</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):
if self.request.user.is_anonymous:
return User.objects.none()
qs = User.objects.filter(
id__in=self.forwarded.get("members", [])
).exclude(
id__in=self.forwarded.get("admins", [])
)
if self.q:
# Due to privacy concerns only a full username match will return the proper user entry
qs = qs.filter(
name__icontains=self.q
username__icontains=self.q
)
qs = qs.order_by(
"username"

View File

@@ -2,6 +2,7 @@ from django.db import models
from konova.models import UuidModel, DeletableObjectMixin
from konova.utils.mailer import Mailer
from user.enums import UserNotificationEnum
from user.models import UserActionLogEntry
@@ -41,7 +42,10 @@ class Team(UuidModel, DeletableObjectMixin):
"""
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):
""" Sends a mail to the team members in case of removed shared access
@@ -54,7 +58,10 @@ class Team(UuidModel, DeletableObjectMixin):
"""
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):
""" Sends a mail to the team members in case of unrecorded data
@@ -67,7 +74,10 @@ class Team(UuidModel, DeletableObjectMixin):
"""
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):
""" Sends a mail to the team members in case of unrecorded data
@@ -80,7 +90,10 @@ class Team(UuidModel, DeletableObjectMixin):
"""
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):
""" Sends a mail to the team members in case of checked data
@@ -93,7 +106,10 @@ class Team(UuidModel, DeletableObjectMixin):
"""
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):
""" Sends a mail to the team members in case of changed deduction values
@@ -107,7 +123,10 @@ class Team(UuidModel, DeletableObjectMixin):
"""
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):
""" Sends a mail to the team members in case of deleted data
@@ -120,7 +139,10 @@ class Team(UuidModel, DeletableObjectMixin):
"""
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):
""" Removes a user from the team

View File

@@ -38,14 +38,6 @@
</article>
<hr>
<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">
<a href="{% url 'user:notifications' %}" title="{% trans 'Change notification configurations' %}">
<button class="btn btn-default">