Compare commits

..

93 Commits
v0.6 ... 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
d52db88a96 Merge pull request '202 Geometry conflict checking to background' (#204) from 202_Geometry_conflict_checking_to_background into master
Reviewed-on: SGD-Nord/konova#204
2022-09-15 12:13:17 +02:00
9bba982ce5 202 Geometry conflict checking to background
* wraps check_for_conflicts() in celery based method
* fixes bug on InterventionEditForm where geometry's save() has been called twice
2022-09-15 12:12:35 +02:00
0647116168 Merge pull request 'map_client_update' (#203) from map_client_update into master
Reviewed-on: SGD-Nord/konova#203
2022-09-15 11:39:00 +02:00
8591156aef Webfonts 2022-09-14 09:12:59 +02:00
b73b209344 Bugfix Webfonts Netgis Client 2022-09-14 09:10:20 +02:00
6a30af647a NEtgis Client Bugfix
* fixes bug where initial geometry has not been rendered on client loading
* needs to be replaced asap by a proper bugfix from the original devs
2022-09-13 13:43:43 +02:00
c08f5145fe Netgis client update
* adds new version to sources
2022-09-12 13:12:59 +02:00
968eea85d4 Merge pull request 'Login required on modals' (#201) from modal_login_required into master
Reviewed-on: SGD-Nord/konova#201
2022-09-12 12:11:15 +02:00
9283c12162 Login required on modals
* adds new login_required_modal decorator
    * can be used before regular login_required decorator to return a proper session-timed-out message
2022-08-25 11:34:09 +02:00
3f907c2b44 Merge pull request '104_Class_based_views' (#200) from 104_Class_based_views into master
Reviewed-on: SGD-Nord/konova#200
2022-08-22 11:09:51 +02:00
a4de394a54 Share views
* replaces function based share views with class based
* improves team-share autocomplete search
* renames internal share url names
2022-08-22 10:58:07 +02:00
a16f68012d EcoAccount views
* splits compensation/views/eco_account.py (+700 lines) into separate files in new module
    * view files can now be found in /compensation/views/eco_account/...
2022-08-22 10:17:49 +02:00
276e7ed4bb CompensationAction views
* replaces function based action views with class based
2022-08-22 08:38:23 +02:00
1b5cbba48b State views
* replaces function based state views with class based
2022-08-22 08:27:36 +02:00
d6057575ea Ema Deadline views
* replaces ema deadline views with class based
2022-08-22 08:12:55 +02:00
89c1352503 Deadline views
* replaces function based views for deadlines with class based views
2022-08-22 08:07:35 +02:00
ed82109af9 Document views
* replaces function based views for creating, editing, removing and fetching documents with class based views
    * implemented for all major data types
2022-08-22 07:52:22 +02:00
c80145366e Record class view
* adds AbstractRecordView to konova/views/record.py
    * implements for all major data types
2022-08-19 11:01:33 +02:00
096385a9eb Resubmission class view
* adds AbstractResubmissionView to konova app
    * implemented for all major data types
    * replaces function based views
2022-08-19 10:47:59 +02:00
7e68d12ee9 Konova views
* splits konova/views.py into separate files in new module
    * view files can now be found in /konova/views/...
* introduces first class based view AbstractLogView
    * implemented for Ema, Intervention, Compensation and EcoAccount
2022-08-19 10:25:27 +02:00
9654f6873d EcoAccount views
* splits compensation/views/eco_account.py (+700 lines) into separate files in new module
    * view files can now be found in /compensation/views/eco_account/...
2022-08-19 08:27:42 +02:00
e930250226 Compensation views
* splits compensation/views/compensation.py (+700 lines) into separate files in new module
    * view files can now be found in /compensation/views/compensation/...
2022-08-19 08:12:32 +02:00
b51f743be2 EMA views
* splits ema/views.py (+700 lines) into separate files in new module
    * view files can now be found in /ema/views/...
2022-08-19 07:51:49 +02:00
ac0db79928 Intervention views
* splits intervention/views.py (+700 lines) into separate files in new module
    * view files can now be found in /intervention/views/...
2022-08-19 07:34:09 +02:00
ad4d64457b Filters refactoring
* splits filters.py of compensation and ema app into separate files in new /filters module
* optimizes entry search for multi keyword input
2022-08-18 13:19:04 +02:00
d785285805 Autocomplete refactoring
* refactors konova/autocompletes.py by splitting into individual files and moving them to fitting apps
     * autocomplete files now live in APPNAME/autocomplete/...
2022-08-18 11:25:06 +02:00
4b3a35c30e Konova filter mixins
* refactors konova/mixins.py into individual files in konova/mixins/...
2022-08-18 10:45:15 +02:00
047a4c7a2e Tables refactoring
* splits compensation tables.py into individual files for compensation table and eco account table
2022-08-18 10:24:11 +02:00
0ce913c672 User forms
* refactors user/forms.py by splitting into modals package and regular forms
    * regular forms can now be found at user/forms/user.py and user/forms/team.py
    * modal forms can now be found at user/forms/modals/...
2022-08-18 10:17:09 +02:00
2f67c2f569 Intervention forms
* refactors intervention/forms and ../modalForms into individual files in separated packages
    * forms.py has been renamed into intervention.py, now can be found as intervention/forms/intervention.py
    * modalForms.py has been split into individual files living in modals package, can be found as intervention/forms/modals/...
2022-08-18 10:08:51 +02:00
ea7a53eb4f Compensation forms refactoring
* splits compensation/forms.py and /modalForms.py into individual files inside new packages
    * general forms stay in new files in compensation/forms
    * modal forms stay in new files in compensation/forms/modals
2022-08-18 09:54:49 +02:00
241 changed files with 15576 additions and 7741 deletions

View File

@@ -55,7 +55,7 @@ class TimespanReportForm(BaseForm):
code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID],
),
widget=autocomplete.ModelSelect2(
url="codes-conservation-office-autocomplete",
url="codelist:conservation-office-autocomplete",
attrs={
"data-placeholder": _("Click for selection")
}

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

@@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""

View File

@@ -0,0 +1,74 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from dal_select2.views import Select2GroupQuerySetView
from django.db.models import Q
from codelist.models import KonovaCode
class KonovaCodeAutocomplete(Select2GroupQuerySetView):
"""
Provides simple autocomplete functionality for codes
Parameter support:
* q: Search for a word inside long_name of a code
* c: Search inside a special codelist
"""
paginate_by = 50
def order_by(self, qs):
""" Orders by a predefined value
Wrapped in a function to provide inheritance-based different orders
Args:
qs (QuerySet): The queryset to be ordered
Returns:
qs (QuerySet): The ordered queryset
"""
return qs.order_by(
"long_name"
)
def get_queryset(self):
if self.request.user.is_anonymous:
return KonovaCode.objects.none()
qs = KonovaCode.objects.filter(
is_archived=False,
is_selectable=True,
is_leaf=True,
)
qs = self.order_by(qs)
if self.c:
qs = qs.filter(
code_lists__in=[self.c]
)
if self.q:
# Remove whitespaces from self.q and split input in all keywords (if multiple given)
q = dict.fromkeys(self.q.strip().split(" "))
# Create one filter looking up for all keys where all keywords can be found in the same result
_filter = Q()
for keyword in q:
q_or = Q()
q_or |= Q(long_name__icontains=keyword)
q_or |= Q(short_name__icontains=keyword)
q_or |= Q(parent__long_name__icontains=keyword)
q_or |= Q(parent__short_name__icontains=keyword)
q_or |= Q(parent__parent__long_name__icontains=keyword)
q_or |= Q(parent__parent__short_name__icontains=keyword)
_filter.add(q_or, Q.AND)
qs = qs.filter(_filter).distinct()
return qs
def get_result_label(self, result):
return f"{result.long_name}"
def get_selected_result_label(self, result):
return f"{result.__str__()}"

View File

@@ -0,0 +1,110 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
import collections
from django.core.exceptions import ImproperlyConfigured
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
from codelist.autocomplete.base import KonovaCodeAutocomplete
from konova.utils.message_templates import UNGROUPED
class BiotopeCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_BIOTOPES_ID
super().__init__(*args, **kwargs)
def order_by(self, qs):
""" Orders by a predefined value
Wrapped in a function to provide inheritance-based different orders
Args:
qs (QuerySet): The queryset to be ordered
Returns:
qs (QuerySet): The ordered queryset
"""
return qs.order_by(
"short_name",
)
def get_result_label(self, result):
return f"{result.long_name} ({result.short_name})"
def get_results(self, context):
"""Return the options grouped by a common related model.
Raises ImproperlyConfigured if self.group_by_name is not configured
"""
if not self.group_by_related:
raise ImproperlyConfigured("Missing group_by_related.")
super_groups = collections.OrderedDict()
object_list = context['object_list']
for result in object_list:
group = result.parent if result.parent else None
group_name = f"{group.long_name} ({group.short_name})" if group else UNGROUPED
super_group = result.parent.parent if result.parent else None
super_group_name = f"{super_group.long_name} ({super_group.short_name})" if super_group else UNGROUPED
super_groups.setdefault(super_group_name, {})
super_groups[super_group_name].setdefault(group_name, [])
super_groups[super_group_name][group_name].append(result)
return [{
'id': None,
'text': super_group,
'children': [{
"id": None,
"text": group,
"children": [{
'id': self.get_result_value(result),
'text': self.get_result_label(result),
'selected_text': self.get_selected_result_label(result),
} for result in results]
} for group, results in groups.items()]
} for super_group, groups in super_groups.items()]
class BiotopeExtraCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
paginate_by = 200
def __init__(self, *args, **kwargs):
self.c = CODELIST_BIOTOPES_EXTRA_CODES_ID
super().__init__(*args, **kwargs)
def order_by(self, qs):
""" Orders by a predefined value
Wrapped in a function to provide inheritance-based different orders
Args:
qs (QuerySet): The queryset to be ordered
Returns:
qs (QuerySet): The ordered queryset
"""
return qs.order_by(
"long_name",
)
def get_result_label(self, result):
return f"{result.long_name} ({result.short_name})"

View File

@@ -0,0 +1,45 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID
from codelist.autocomplete.base import KonovaCodeAutocomplete
class CompensationActionCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_COMPENSATION_ACTION_ID
super().__init__(*args, **kwargs)
def order_by(self, qs):
return qs.order_by(
"parent__long_name"
)
class CompensationActionDetailCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
paginate_by = 200
def __init__(self, *args, **kwargs):
self.c = CODELIST_COMPENSATION_ACTION_DETAIL_ID
super().__init__(*args, **kwargs)
def order_by(self, qs):
return qs.order_by(
"long_name"
)

View File

@@ -0,0 +1,24 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from codelist.settings import CODELIST_HANDLER_ID
from codelist.autocomplete.base import KonovaCodeAutocomplete
class HandlerCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_HANDLER_ID
super().__init__(*args, **kwargs)
def get_result_label(self, result):
return result.long_name

View File

@@ -0,0 +1,24 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from codelist.settings import CODELIST_LAW_ID
from codelist.autocomplete.base import KonovaCodeAutocomplete
class LawCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_LAW_ID
super().__init__(*args, **kwargs)
def get_result_label(self, result):
return f"{result.long_name} ({result.short_name})"

View File

@@ -0,0 +1,41 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_REGISTRATION_OFFICE_ID
from codelist.autocomplete.base import KonovaCodeAutocomplete
class RegistrationOfficeCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_REGISTRATION_OFFICE_ID
super().__init__(*args, **kwargs)
def order_by(self, qs):
return qs.order_by(
"parent__long_name"
)
class ConservationOfficeCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_CONSERVATION_OFFICE_ID
super().__init__(*args, **kwargs)
def get_result_label(self, result):
return f"{result.long_name} ({result.short_name})"

View File

@@ -0,0 +1,21 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from codelist.autocomplete.base import KonovaCodeAutocomplete
from codelist.settings import CODELIST_PROCESS_TYPE_ID
class ProcessTypeCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_PROCESS_TYPE_ID
super().__init__(*args, **kwargs)

View File

@@ -50,7 +50,7 @@ class KonovaCode(models.Model):
def __str__(self, with_parent: bool = True):
ret_val = ""
if self.parent and with_parent:
if self.parent and self.parent.long_name and with_parent:
ret_val += self.parent.long_name + " > "
ret_val += self.long_name
if self.short_name and self.short_name != self.long_name:

View File

@@ -7,8 +7,24 @@ Created on: 23.08.21
"""
from django.urls import path
from codelist.autocomplete.biotope import BiotopeCodeAutocomplete, BiotopeExtraCodeAutocomplete
from codelist.autocomplete.compensation_action import CompensationActionDetailCodeAutocomplete, \
CompensationActionCodeAutocomplete
from codelist.autocomplete.handler import HandlerCodeAutocomplete
from codelist.autocomplete.law import LawCodeAutocomplete
from codelist.autocomplete.office import ConservationOfficeCodeAutocomplete, RegistrationOfficeCodeAutocomplete
from codelist.autocomplete.process_type import ProcessTypeCodeAutocomplete
app_name = "codelist"
urlpatterns = [
path("atcmplt/codes/biotope", BiotopeCodeAutocomplete.as_view(), name="biotope-autocomplete"),
path("atcmplt/codes/biotope/extra", BiotopeExtraCodeAutocomplete.as_view(),
name="biotope-extra-type-autocomplete"),
path("atcmplt/codes/law", LawCodeAutocomplete.as_view(), name="law-autocomplete"),
path("atcmplt/codes/reg-off", RegistrationOfficeCodeAutocomplete.as_view(), name="registration-office-autocomplete"),
path("atcmplt/codes/cons-off", ConservationOfficeCodeAutocomplete.as_view(), name="conservation-office-autocomplete"),
path("atcmplt/codes/handler", HandlerCodeAutocomplete.as_view(), name="handler-autocomplete"),
path("atcmplt/codes/comp/action", CompensationActionCodeAutocomplete.as_view(), name="compensation-action-autocomplete"),
path("atcmplt/codes/comp/action/detail", CompensationActionDetailCodeAutocomplete.as_view(), name="compensation-action-detail-autocomplete"),
path("atcmplt/codes/prc-type", ProcessTypeCodeAutocomplete.as_view(), name="process-type-autocomplete"),
]

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

@@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""

View File

@@ -0,0 +1,34 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from dal_select2.views import Select2QuerySetView
from django.db.models import Q
from compensation.models import EcoAccount
class EcoAccountAutocomplete(Select2QuerySetView):
""" Autocomplete for ecoAccount entries
Only returns entries that are already recorded and not deleted
"""
def get_queryset(self):
if self.request.user.is_anonymous:
return EcoAccount.objects.none()
qs = EcoAccount.objects.filter(
deleted=None,
recorded__isnull=False,
).order_by(
"identifier"
)
if self.q:
qs = qs.filter(
Q(identifier__icontains=self.q) |
Q(title__icontains=self.q)
).distinct()
return qs

View File

@@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""

View File

@@ -1,17 +1,14 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 29.07.21
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
import django_filters
from django.utils.translation import gettext_lazy as _
from django import forms
from django.db.models import QuerySet, Q
from konova.filters.mixins import ConservationOfficeTableFilterMixin
from konova.filters.table_filters import QueryTableFilter, CheckboxTableFilter, SelectionTableFilter, AbstractTableFilter
from konova.filters.table_filters import AbstractTableFilter, CheckboxTableFilter, QueryTableFilter, \
SelectionTableFilter
class SelectionCompensationTableFilter(SelectionTableFilter):
@@ -114,71 +111,3 @@ class CompensationTableFilter(AbstractTableFilter):
)
# Overwrite final queryset as well
self.qs = self.checkbox_filter.qs
class CheckboxEcoAccountTableFilter(CheckboxTableFilter):
sr = django_filters.BooleanFilter(
method='filter_only_show_unrecorded',
label=_("Show only unrecorded"),
label_suffix=_(""),
widget=forms.CheckboxInput(
attrs={
"class": "form-check-input",
}
)
)
def filter_only_show_unrecorded(self, queryset, name, value) -> QuerySet:
""" Filters queryset depending on value of 'show_recorded' setting
Args:
queryset ():
name ():
value ():
Returns:
"""
if value:
return queryset.filter(
recorded=None,
)
else:
return queryset
class SelectionEcoAccountTableFilter(ConservationOfficeTableFilterMixin):
""" Special selection table filter for eco accounts
EcoAccounts only need a selection filter for conservation office
"""
pass
class EcoAccountTableFilter(AbstractTableFilter):
""" TableFilter for eco accounts
"""
def __init__(self, user=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = user
qs = kwargs.get("queryset", None)
request_data = kwargs.get("data", None)
# Pipe the queryset through all needed filters
self.selection_filter = SelectionEcoAccountTableFilter(
data=request_data,
queryset=qs,
)
self.query_filter = QueryTableFilter(
data=request_data,
queryset=self.selection_filter.qs,
)
self.checkbox_filter = CheckboxEcoAccountTableFilter(
user=user,
data=request_data,
queryset=self.query_filter.qs,
)
# Overwrite the final queryset result
self.qs = self.checkbox_filter.qs

View File

@@ -0,0 +1,82 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from django import forms
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
import django_filters
from konova.filters.mixins.office import ConservationOfficeTableFilterMixin
from konova.filters.table_filters import AbstractTableFilter, CheckboxTableFilter, QueryTableFilter
class CheckboxEcoAccountTableFilter(CheckboxTableFilter):
sr = django_filters.BooleanFilter(
method='filter_only_show_unrecorded',
label=_("Show only unrecorded"),
label_suffix=_(""),
widget=forms.CheckboxInput(
attrs={
"class": "form-check-input",
}
)
)
def filter_only_show_unrecorded(self, queryset, name, value) -> QuerySet:
""" Filters queryset depending on value of 'show_recorded' setting
Args:
queryset ():
name ():
value ():
Returns:
"""
if value:
return queryset.filter(
recorded=None,
)
else:
return queryset
class SelectionEcoAccountTableFilter(ConservationOfficeTableFilterMixin):
""" Special selection table filter for eco accounts
EcoAccounts only need a selection filter for conservation office
"""
pass
class EcoAccountTableFilter(AbstractTableFilter):
""" TableFilter for eco accounts
"""
def __init__(self, user=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = user
qs = kwargs.get("queryset", None)
request_data = kwargs.get("data", None)
# Pipe the queryset through all needed filters
self.selection_filter = SelectionEcoAccountTableFilter(
data=request_data,
queryset=qs,
)
self.query_filter = QueryTableFilter(
data=request_data,
queryset=self.selection_filter.qs,
)
self.checkbox_filter = CheckboxEcoAccountTableFilter(
user=user,
data=request_data,
queryset=self.query_filter.qs,
)
# Overwrite the final queryset result
self.qs = self.checkbox_filter.qs

View File

@@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""

View File

@@ -0,0 +1,241 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from dal import autocomplete
from django import forms
from django.db import transaction
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from compensation.forms.mixins import CEFCompensationFormMixin, CoherenceCompensationFormMixin, PikCompensationFormMixin
from compensation.models import Compensation
from intervention.inputs import GenerateInput
from intervention.models import Intervention
from konova.forms import BaseForm, SimpleGeomForm
from konova.utils.message_templates import COMPENSATION_ADDED_TEMPLATE, EDITED_GENERAL_DATA
from user.models import UserActionLogEntry, User
class AbstractCompensationForm(BaseForm):
""" Abstract form for compensations
Holds all important form fields, which are used in compensation and eco account forms
"""
identifier = forms.CharField(
label=_("Identifier"),
label_suffix="",
max_length=255,
help_text=_("Generated automatically"),
widget=GenerateInput(
attrs={
"class": "form-control",
"url": None, # Needs to be set in inheriting constructors
}
)
)
title = forms.CharField(
label=_("Title"),
label_suffix="",
help_text=_("An explanatory name"),
max_length=255,
widget=forms.TextInput(
attrs={
"placeholder": _("Compensation XY; Location ABC"),
"class": "form-control",
}
)
)
comment = forms.CharField(
label_suffix="",
label=_("Comment"),
required=False,
help_text=_("Additional comment"),
widget=forms.Textarea(
attrs={
"rows": 5,
"class": "form-control"
}
)
)
class Meta:
abstract = True
class NewCompensationForm(AbstractCompensationForm,
CEFCompensationFormMixin,
CoherenceCompensationFormMixin,
PikCompensationFormMixin):
""" Form for creating new compensations.
Can be initialized with an intervention id for preselecting the related intervention.
form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
...
The intervention id will not be resolved into the intervention ORM object but instead will be used to initialize
the related form field.
"""
intervention = forms.ModelChoiceField(
label=_("compensates intervention"),
label_suffix="",
help_text=_("Select the intervention for which this compensation compensates"),
queryset=Intervention.objects.filter(
deleted=None,
),
widget=autocomplete.ModelSelect2(
url="intervention:autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
"data-minimum-input-length": 3,
}
),
)
# Define a field order for a nicer layout instead of running with the inheritance result
field_order = [
"identifier",
"title",
"intervention",
"is_pik",
"is_cef",
"is_coherence_keeping",
"comment",
]
def __init__(self, *args, **kwargs):
intervention_id = kwargs.pop("intervention_id", None)
super().__init__(*args, **kwargs)
self.form_title = _("New compensation")
# If the compensation shall directly be initialized from an intervention, we need to fill in the intervention id
# and disable the form field.
# Furthermore the action_url needs to be set accordingly.
if intervention_id is not None:
self.initialize_form_field("intervention", intervention_id)
self.disable_form_field("intervention")
self.action_url = reverse("compensation:new", args=(intervention_id,))
self.cancel_redirect = reverse("intervention:detail", args=(intervention_id,))
else:
self.action_url = reverse("compensation:new")
self.cancel_redirect = reverse("compensation:index")
tmp = Compensation()
identifier = tmp.generate_new_identifier()
self.initialize_form_field("identifier", identifier)
self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:new-id")
def __create_comp(self, user):
""" Creates the compensation from form data
Args:
user (User): The performing user
Returns:
comp (Compensation): The compensation object
"""
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
intervention = self.cleaned_data.get("intervention", None)
is_cef = self.cleaned_data.get("is_cef", None)
is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None)
is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.get_created_action(user)
# Finally create main object
comp = Compensation.objects.create(
identifier=identifier,
title=title,
intervention=intervention,
created=action,
modified=action,
is_cef=is_cef,
is_coherence_keeping=is_coherence_keeping,
is_pik=is_pik,
comment=comment,
)
# Add the log entry to the main objects log list
comp.log.add(action)
return comp, action
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
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
class EditCompensationForm(NewCompensationForm):
""" Form for editing compensations
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Edit compensation")
self.action_url = reverse("compensation:edit", args=(self.instance.id,))
self.cancel_redirect = reverse("compensation:detail", args=(self.instance.id,))
# Initialize form data
form_data = {
"identifier": self.instance.identifier,
"title": self.instance.title,
"intervention": self.instance.intervention,
"is_cef": self.instance.is_cef,
"is_coherence_keeping": self.instance.is_coherence_keeping,
"is_pik": self.instance.is_pik,
"comment": self.instance.comment,
}
disabled_fields = []
self.load_initial_data(
form_data,
disabled_fields
)
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)
intervention = self.cleaned_data.get("intervention", None)
is_cef = self.cleaned_data.get("is_cef", None)
is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None)
is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None)
self.instance.identifier = identifier
self.instance.title = title
self.instance.intervention = intervention
self.instance.is_cef = is_cef
self.instance.is_coherence_keeping = is_coherence_keeping
self.instance.comment = comment
self.instance.is_pik = is_pik
self.instance.modified = action
self.instance.save()
self.instance.log.add(action)
intervention.mark_as_edited(user, self.request, EDITED_GENERAL_DATA)
# 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,232 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from django import forms
from django.db import transaction
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from compensation.forms.compensation import AbstractCompensationForm
from compensation.forms.mixins import CompensationResponsibleFormMixin, PikCompensationFormMixin
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
class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMixin, PikCompensationFormMixin):
""" Form for creating eco accounts
Inherits from basic AbstractCompensationForm and further form fields from CompensationResponsibleFormMixin
"""
surface = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_("Available Surface"),
label_suffix="",
required=False,
help_text=_("The amount that can be used for deductions"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00"
}
)
)
registration_date = forms.DateField(
label=_("Agreement date"),
label_suffix="",
help_text=_("When did the parties agree on this?"),
required=False,
widget=forms.DateInput(
attrs={
"type": "date",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
field_order = [
"identifier",
"title",
"conservation_office",
"registration_date",
"surface",
"conservation_file_number",
"is_pik",
"handler_type",
"handler_detail",
"comment",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New Eco-Account")
self.action_url = reverse("compensation:acc:new")
self.cancel_redirect = reverse("compensation:acc:index")
tmp = EcoAccount()
identifier = tmp.generate_new_identifier()
self.initialize_form_field("identifier", identifier)
self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:acc:new-id")
self.fields["title"].widget.attrs["placeholder"] = _("Eco-Account XY; Location ABC")
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
registration_date = self.cleaned_data.get("registration_date", None)
handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", None)
surface = self.cleaned_data.get("surface", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.get_created_action(user)
handler = Handler.objects.create(
type=handler_type,
detail=handler_detail,
)
responsible = Responsibility.objects.create(
handler=handler,
conservation_file_number=conservation_file_number,
conservation_office=conservation_office,
)
legal = Legal.objects.create(
registration_date=registration_date
)
# Finally create main object
acc = EcoAccount.objects.create(
identifier=identifier,
title=title,
responsible=responsible,
deductable_surface=surface,
created=action,
modified=action,
comment=comment,
is_pik=is_pik,
legal=legal
)
acc.share_with_user(user)
# 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
class EditEcoAccountForm(NewEcoAccountForm):
""" Form for editing eco accounts
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Edit Eco-Account")
self.action_url = reverse("compensation:acc:edit", args=(self.instance.id,))
self.cancel_redirect = reverse("compensation:acc:detail", args=(self.instance.id,))
# Initialize form data
reg_date = self.instance.legal.registration_date
if reg_date is not None:
reg_date = reg_date.isoformat()
form_data = {
"identifier": self.instance.identifier,
"title": self.instance.title,
"surface": self.instance.deductable_surface,
"handler_type": self.instance.responsible.handler.type,
"handler_detail": self.instance.responsible.handler.detail,
"registration_date": reg_date,
"conservation_office": self.instance.responsible.conservation_office,
"conservation_file_number": self.instance.responsible.conservation_file_number,
"is_pik": self.instance.is_pik,
"comment": self.instance.comment,
}
disabled_fields = []
self.load_initial_data(
form_data,
disabled_fields
)
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
registration_date = self.cleaned_data.get("registration_date", None)
handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", None)
surface = self.cleaned_data.get("surface", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
comment = self.cleaned_data.get("comment", None)
is_pik = self.cleaned_data.get("is_pik", None)
# Create log entry
action = UserActionLogEntry.get_edited_action(user)
# Update responsible data
self.instance.responsible.handler.type = handler_type
self.instance.responsible.handler.detail = handler_detail
self.instance.responsible.handler.save()
self.instance.responsible.conservation_office = conservation_office
self.instance.responsible.conservation_file_number = conservation_file_number
self.instance.responsible.save()
# Update legal data
self.instance.legal.registration_date = registration_date
self.instance.legal.save()
# Update main oject data
self.instance.identifier = identifier
self.instance.title = title
self.instance.deductable_surface = surface
self.instance.comment = comment
self.instance.is_pik = is_pik
self.instance.modified = action
self.instance.save()
# 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

@@ -1,539 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.12.20
"""
from dal import autocomplete
from user.models import User
from django.db import transaction
from django.urls import reverse_lazy, reverse
from django.utils.translation import gettext_lazy as _
from django import forms
from codelist.models import KonovaCode
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID
from compensation.models import Compensation, EcoAccount
from intervention.inputs import GenerateInput
from intervention.models import Intervention, Responsibility, Legal, Handler
from konova.forms import BaseForm, SimpleGeomForm
from konova.utils.message_templates import EDITED_GENERAL_DATA, COMPENSATION_ADDED_TEMPLATE
from user.models import UserActionLogEntry
class AbstractCompensationForm(BaseForm):
""" Abstract form for compensations
Holds all important form fields, which are used in compensation and eco account forms
"""
identifier = forms.CharField(
label=_("Identifier"),
label_suffix="",
max_length=255,
help_text=_("Generated automatically"),
widget=GenerateInput(
attrs={
"class": "form-control",
"url": None, # Needs to be set in inheriting constructors
}
)
)
title = forms.CharField(
label=_("Title"),
label_suffix="",
help_text=_("An explanatory name"),
max_length=255,
widget=forms.TextInput(
attrs={
"placeholder": _("Compensation XY; Location ABC"),
"class": "form-control",
}
)
)
comment = forms.CharField(
label_suffix="",
label=_("Comment"),
required=False,
help_text=_("Additional comment"),
widget=forms.Textarea(
attrs={
"rows": 5,
"class": "form-control"
}
)
)
class Meta:
abstract = True
class CompensationResponsibleFormMixin(forms.Form):
""" Encapsulates form fields used in different compensation related models like EcoAccount or EMA
"""
conservation_office = forms.ModelChoiceField(
label=_("Conservation office"),
label_suffix="",
help_text=_("Select the responsible office"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID],
),
widget=autocomplete.ModelSelect2(
url="codes-conservation-office-autocomplete",
attrs={
"data-placeholder": _("Click for selection")
}
),
)
conservation_file_number = forms.CharField(
label=_("Conservation office file number"),
label_suffix="",
max_length=255,
required=False,
widget=forms.TextInput(
attrs={
"placeholder": _("ETS-123/ABC.456"),
"class": "form-control",
}
)
)
handler_type = forms.ModelChoiceField(
label=_("Eco-Account handler type"),
label_suffix="",
help_text=_("What type of handler is responsible for the ecoaccount?"),
required=False,
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_HANDLER_ID],
),
widget=autocomplete.ModelSelect2(
url="codes-handler-autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
}
),
)
handler_detail = forms.CharField(
label=_("Eco-Account handler detail"),
label_suffix="",
max_length=255,
required=False,
help_text=_("Detail input on the handler"),
widget=forms.TextInput(
attrs={
"placeholder": _("Company Mustermann"),
"class": "form-control",
}
)
)
class CEFCompensationFormMixin(forms.Form):
""" A form mixin, providing CEF compensation field
"""
is_cef = forms.BooleanField(
label_suffix="",
label=_("Is CEF"),
help_text=_("Optionally: Whether this compensation is a CEF compensation?"),
required=False,
widget=forms.CheckboxInput()
)
class CoherenceCompensationFormMixin(forms.Form):
""" A form mixin, providing coherence compensation field
"""
is_coherence_keeping = forms.BooleanField(
label_suffix="",
label=_("Is coherence keeping"),
help_text=_("Optionally: Whether this compensation is a coherence keeping compensation?"),
required=False,
widget=forms.CheckboxInput()
)
class PikCompensationFormMixin(forms.Form):
""" A form mixin, providing PIK compensation field
"""
is_pik = forms.BooleanField(
label_suffix="",
label=_("Is PIK"),
help_text=_("Optionally: Whether this compensation is a compensation integrated in production?"),
required=False,
widget=forms.CheckboxInput()
)
class NewCompensationForm(AbstractCompensationForm,
CEFCompensationFormMixin,
CoherenceCompensationFormMixin,
PikCompensationFormMixin):
""" Form for creating new compensations.
Can be initialized with an intervention id for preselecting the related intervention.
form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
...
The intervention id will not be resolved into the intervention ORM object but instead will be used to initialize
the related form field.
"""
intervention = forms.ModelChoiceField(
label=_("compensates intervention"),
label_suffix="",
help_text=_("Select the intervention for which this compensation compensates"),
queryset=Intervention.objects.filter(
deleted=None,
),
widget=autocomplete.ModelSelect2(
url="interventions-autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
"data-minimum-input-length": 3,
}
),
)
# Define a field order for a nicer layout instead of running with the inheritance result
field_order = [
"identifier",
"title",
"intervention",
"is_pik",
"is_cef",
"is_coherence_keeping",
"comment",
]
def __init__(self, *args, **kwargs):
intervention_id = kwargs.pop("intervention_id", None)
super().__init__(*args, **kwargs)
self.form_title = _("New compensation")
# If the compensation shall directly be initialized from an intervention, we need to fill in the intervention id
# and disable the form field.
# Furthermore the action_url needs to be set accordingly.
if intervention_id is not None:
self.initialize_form_field("intervention", intervention_id)
self.disable_form_field("intervention")
self.action_url = reverse("compensation:new", args=(intervention_id,))
self.cancel_redirect = reverse("intervention:detail", args=(intervention_id,))
else:
self.action_url = reverse("compensation:new")
self.cancel_redirect = reverse("compensation:index")
tmp = Compensation()
identifier = tmp.generate_new_identifier()
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:
""" Creates the compensation from form data
Args:
user (User): The performing user
geom_form (SimpleGeomForm): The geometry form
Returns:
comp (Compensation): The compensation object
"""
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
intervention = self.cleaned_data.get("intervention", None)
is_cef = self.cleaned_data.get("is_cef", None)
is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None)
is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None)
# 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(
identifier=identifier,
title=title,
intervention=intervention,
created=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
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
comp = self.__create_comp(user, geom_form)
comp.intervention.mark_as_edited(user, edit_comment=COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
return comp
class EditCompensationForm(NewCompensationForm):
""" Form for editing compensations
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Edit compensation")
self.action_url = reverse("compensation:edit", args=(self.instance.id,))
self.cancel_redirect = reverse("compensation:detail", args=(self.instance.id,))
# Initialize form data
form_data = {
"identifier": self.instance.identifier,
"title": self.instance.title,
"intervention": self.instance.intervention,
"is_cef": self.instance.is_cef,
"is_coherence_keeping": self.instance.is_coherence_keeping,
"is_pik": self.instance.is_pik,
"comment": self.instance.comment,
}
disabled_fields = []
self.load_initial_data(
form_data,
disabled_fields
)
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
intervention = self.cleaned_data.get("intervention", None)
is_cef = self.cleaned_data.get("is_cef", None)
is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None)
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
self.instance.is_pik = is_pik
self.instance.modified = action
self.instance.save()
self.instance.log.add(action)
intervention.mark_as_edited(user, self.request, EDITED_GENERAL_DATA)
return self.instance
class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMixin, PikCompensationFormMixin):
""" Form for creating eco accounts
Inherits from basic AbstractCompensationForm and further form fields from CompensationResponsibleFormMixin
"""
surface = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_("Available Surface"),
label_suffix="",
required=False,
help_text=_("The amount that can be used for deductions"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00"
}
)
)
registration_date = forms.DateField(
label=_("Agreement date"),
label_suffix="",
help_text=_("When did the parties agree on this?"),
required=False,
widget=forms.DateInput(
attrs={
"type": "date",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
field_order = [
"identifier",
"title",
"conservation_office",
"registration_date",
"surface",
"conservation_file_number",
"is_pik",
"handler_type",
"handler_detail",
"comment",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New Eco-Account")
self.action_url = reverse("compensation:acc:new")
self.cancel_redirect = reverse("compensation:acc:index")
tmp = EcoAccount()
identifier = tmp.generate_new_identifier()
self.initialize_form_field("identifier", identifier)
self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:acc:new-id")
self.fields["title"].widget.attrs["placeholder"] = _("Eco-Account XY; Location ABC")
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
registration_date = self.cleaned_data.get("registration_date", None)
handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", None)
surface = self.cleaned_data.get("surface", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None)
# 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,
detail=handler_detail,
)
responsible = Responsibility.objects.create(
handler=handler,
conservation_file_number=conservation_file_number,
conservation_office=conservation_office,
)
legal = Legal.objects.create(
registration_date=registration_date
)
# Finally create main object
acc = EcoAccount.objects.create(
identifier=identifier,
title=title,
responsible=responsible,
deductable_surface=surface,
created=action,
geometry=geometry,
comment=comment,
is_pik=is_pik,
legal=legal
)
acc.share_with_user(user)
# Add the log entry to the main objects log list
acc.log.add(action)
return acc
class EditEcoAccountForm(NewEcoAccountForm):
""" Form for editing eco accounts
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Edit Eco-Account")
self.action_url = reverse("compensation:acc:edit", args=(self.instance.id,))
self.cancel_redirect = reverse("compensation:acc:detail", args=(self.instance.id,))
# Initialize form data
reg_date = self.instance.legal.registration_date
if reg_date is not None:
reg_date = reg_date.isoformat()
form_data = {
"identifier": self.instance.identifier,
"title": self.instance.title,
"surface": self.instance.deductable_surface,
"handler_type": self.instance.responsible.handler.type,
"handler_detail": self.instance.responsible.handler.detail,
"registration_date": reg_date,
"conservation_office": self.instance.responsible.conservation_office,
"conservation_file_number": self.instance.responsible.conservation_file_number,
"is_pik": self.instance.is_pik,
"comment": self.instance.comment,
}
disabled_fields = []
self.load_initial_data(
form_data,
disabled_fields
)
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
registration_date = self.cleaned_data.get("registration_date", None)
handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", None)
surface = self.cleaned_data.get("surface", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
comment = self.cleaned_data.get("comment", None)
is_pik = self.cleaned_data.get("is_pik", None)
# 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
self.instance.responsible.handler.save()
self.instance.responsible.conservation_office = conservation_office
self.instance.responsible.conservation_file_number = conservation_file_number
self.instance.responsible.save()
# Update legal data
self.instance.legal.registration_date = registration_date
self.instance.legal.save()
# Update main oject data
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
self.instance.save()
# Add the log entry to the main objects log list
self.instance.log.add(action)
return self.instance

View File

@@ -0,0 +1,117 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from dal import autocomplete
from django import forms
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID
class CompensationResponsibleFormMixin(forms.Form):
""" Encapsulates form fields used in different compensation related models like EcoAccount or EMA
"""
conservation_office = forms.ModelChoiceField(
label=_("Conservation office"),
label_suffix="",
help_text=_("Select the responsible office"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID],
),
widget=autocomplete.ModelSelect2(
url="codelist:conservation-office-autocomplete",
attrs={
"data-placeholder": _("Click for selection")
}
),
)
conservation_file_number = forms.CharField(
label=_("Conservation office file number"),
label_suffix="",
max_length=255,
required=False,
widget=forms.TextInput(
attrs={
"placeholder": _("ETS-123/ABC.456"),
"class": "form-control",
}
)
)
handler_type = forms.ModelChoiceField(
label=_("Eco-Account handler type"),
label_suffix="",
help_text=_("What type of handler is responsible for the ecoaccount?"),
required=False,
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_HANDLER_ID],
),
widget=autocomplete.ModelSelect2(
url="codelist:handler-autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
}
),
)
handler_detail = forms.CharField(
label=_("Eco-Account handler detail"),
label_suffix="",
max_length=255,
required=False,
help_text=_("Detail input on the handler"),
widget=forms.TextInput(
attrs={
"placeholder": _("Company Mustermann"),
"class": "form-control",
}
)
)
class CEFCompensationFormMixin(forms.Form):
""" A form mixin, providing CEF compensation field
"""
is_cef = forms.BooleanField(
label_suffix="",
label=_("Is CEF"),
help_text=_("Optionally: Whether this compensation is a CEF compensation?"),
required=False,
widget=forms.CheckboxInput()
)
class CoherenceCompensationFormMixin(forms.Form):
""" A form mixin, providing coherence compensation field
"""
is_coherence_keeping = forms.BooleanField(
label_suffix="",
label=_("Is coherence keeping"),
help_text=_("Optionally: Whether this compensation is a coherence keeping compensation?"),
required=False,
widget=forms.CheckboxInput()
)
class PikCompensationFormMixin(forms.Form):
""" A form mixin, providing PIK compensation field
"""
is_pik = forms.BooleanField(
label_suffix="",
label=_("Is PIK"),
help_text=_("Optionally: Whether this compensation is a compensation integrated in production?"),
required=False,
widget=forms.CheckboxInput()
)

View File

@@ -1,534 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.10.21
"""
from bootstrap_modal_forms.utils import is_ajax
from dal import autocomplete
from django import forms
from django.contrib import messages
from django.http import HttpRequest, HttpResponseRedirect
from django.shortcuts import render
from django.utils.translation import pgettext_lazy as _con, gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, \
CODELIST_COMPENSATION_ACTION_DETAIL_ID
from compensation.models import CompensationDocument, EcoAccountDocument
from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple, \
CompensationStateTreeRadioSelect
from konova.contexts import BaseContext
from konova.forms.modals import BaseModalForm, NewDocumentModalForm, RemoveModalForm
from konova.models import DeadlineType
from konova.utils.message_templates import FORM_INVALID, ADDED_COMPENSATION_STATE, \
ADDED_COMPENSATION_ACTION, PAYMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED
class NewPaymentForm(BaseModalForm):
""" Form handling payment related input
"""
amount = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_con("money", "Amount"), # contextual translation
label_suffix=_(""),
help_text=_("in Euro"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00",
}
)
)
due = forms.DateField(
label=_("Due on"),
label_suffix=_(""),
required=False,
help_text=_("Due on which date"),
widget=forms.DateInput(
attrs={
"type": "date",
"data-provide": "datepicker",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
comment = forms.CharField(
max_length=200,
required=False,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment, maximum {} letters").format(200),
widget=forms.Textarea(
attrs={
"rows": 5,
"class": "form-control"
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.intervention = self.instance
self.form_title = _("Payment")
self.form_caption = _("Add a payment for intervention '{}'").format(self.intervention.title)
def is_valid(self):
"""
Checks on form validity.
For this form we need to make sure that a date or a comment is set.
If both are missing, the user needs to enter at least an explanation why
there is no date to be entered.
Returns:
is_valid (bool): True if valid, False otherwise
"""
super_valid = super().is_valid()
date = self.cleaned_data["due"]
comment = self.cleaned_data["comment"] or None
if not date and not comment:
# At least one needs to be set!
self.add_error(
"comment",
_("If there is no date you can enter, please explain why.")
)
return False
return super_valid
def save(self):
pay = self.instance.add_payment(self)
return pay
class EditPaymentModalForm(NewPaymentForm):
""" Form handling edit for Payment
"""
payment = None
def __init__(self, *args, **kwargs):
self.payment = kwargs.pop("payment", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit payment")
form_date = {
"amount": self.payment.amount,
"due": str(self.payment.due_on),
"comment": self.payment.comment,
}
self.load_initial_data(form_date, disabled_fields=[])
def save(self):
payment = self.payment
payment.amount = self.cleaned_data.get("amount", None)
payment.due_on = self.cleaned_data.get("due", None)
payment.comment = self.cleaned_data.get("comment", None)
payment.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=PAYMENT_EDITED)
self.instance.send_data_to_egon()
return payment
class RemovePaymentModalForm(RemoveModalForm):
""" Removing modal form for Payment
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
payment = None
def __init__(self, *args, **kwargs):
payment = kwargs.pop("payment", None)
self.payment = payment
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_payment(self)
class NewStateModalForm(BaseModalForm):
""" Form handling state related input
Compensation states refer to 'before' and 'after' states of a compensated surface. Basically it means:
What has been on this area before changes/compensations have been applied and what will be the result ('after')?
"""
biotope_type = forms.ChoiceField(
label=_("Biotope Type"),
label_suffix="",
required=True,
help_text=_("Select the biotope type"),
widget=CompensationStateTreeRadioSelect(),
)
biotope_extra = forms.ModelMultipleChoiceField(
label=_("Biotope additional type"),
label_suffix="",
required=False,
help_text=_("Select an additional biotope type"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_ID],
),
widget=autocomplete.ModelSelect2Multiple(
url="codes-biotope-extra-type-autocomplete",
attrs={
"data-placeholder": _("Biotope additional type"),
}
),
)
surface = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_("Surface"),
label_suffix="",
required=True,
help_text=_("in m²"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00"
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New state")
self.form_caption = _("Insert data for the new state")
choices = KonovaCode.objects.filter(
code_lists__in=[CODELIST_BIOTOPES_ID],
is_archived=False,
is_leaf=True,
).values_list("id", flat=True)
choices = [
(choice, choice)
for choice in choices
]
self.fields["biotope_type"].choices = choices
def save(self, is_before_state: bool = False):
state = self.instance.add_state(self, is_before_state)
self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_STATE)
return state
def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None):
""" Generic processing of request
Wraps the request processing logic, so we don't need the same code everywhere a RemoveModalForm is being used
+++
The generic method from super class can not be used, since we need to do some request parameter check in here.
+++
Args:
request (HttpRequest): The incoming request
msg_success (str): The message in case of successful removing
msg_error (str): The message in case of an error
Returns:
"""
redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home")
template = self.template
if request.method == "POST":
if self.is_valid():
# Modal forms send one POST for checking on data validity. This can be used to return possible errors
# on the form. A second POST (if no errors occured) is sent afterwards and needs to process the
# saving/commiting of the data to the database. is_ajax() performs this check. The first request is
# an ajax call, the second is a regular form POST.
if not is_ajax(request.META):
is_before_state = bool(request.GET.get("before", False))
self.save(is_before_state=is_before_state)
messages.success(
request,
msg_success
)
return HttpResponseRedirect(redirect_url)
else:
context = {
"form": self,
}
context = BaseContext(request, context).context
return render(request, template, context)
elif request.method == "GET":
context = {
"form": self,
}
context = BaseContext(request, context).context
return render(request, template, context)
else:
raise NotImplementedError
class EditCompensationStateModalForm(NewStateModalForm):
state = None
def __init__(self, *args, **kwargs):
self.state = kwargs.pop("state", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit state")
biotope_type_id = self.state.biotope_type.id if self.state.biotope_type else None
form_data = {
"biotope_type": biotope_type_id,
"biotope_extra": self.state.biotope_type_details.all(),
"surface": self.state.surface,
}
self.load_initial_data(form_data)
def save(self, is_before_state: bool = False):
state = self.state
biotope_type_id = self.cleaned_data.get("biotope_type", None)
state.biotope_type = KonovaCode.objects.get(id=biotope_type_id)
state.biotope_type_details.set(self.cleaned_data.get("biotope_extra", []))
state.surface = self.cleaned_data.get("surface", None)
state.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=COMPENSATION_STATE_EDITED)
return state
class RemoveCompensationStateModalForm(RemoveModalForm):
""" Removing modal form for CompensationState
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
state = None
def __init__(self, *args, **kwargs):
state = kwargs.pop("state", None)
self.state = state
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_state(self)
class RemoveCompensationActionModalForm(RemoveModalForm):
""" Removing modal form for CompensationAction
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
action = None
def __init__(self, *args, **kwargs):
action = kwargs.pop("action", None)
self.action = action
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_action(self)
class NewDeadlineModalForm(BaseModalForm):
""" Form handling deadline related input
"""
type = forms.ChoiceField(
label=_("Deadline Type"),
label_suffix="",
required=True,
help_text=_("Select the deadline type"),
choices=DeadlineType.choices,
widget=forms.Select(
attrs={
"class": "form-control"
}
)
)
date = forms.DateField(
label=_("Date"),
label_suffix="",
required=True,
help_text=_("Select date"),
widget=forms.DateInput(
attrs={
"type": "date",
"data-provide": "datepicker",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
comment = forms.CharField(
required=False,
max_length=200,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment, maximum {} letters").format(200),
widget=forms.Textarea(
attrs={
"cols": 30,
"rows": 5,
"class": "form-control",
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New deadline")
self.form_caption = _("Insert data for the new deadline")
def save(self):
deadline = self.instance.add_deadline(self)
return deadline
class EditDeadlineModalForm(NewDeadlineModalForm):
deadline = None
def __init__(self, *args, **kwargs):
self.deadline = kwargs.pop("deadline", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit deadline")
form_data = {
"type": self.deadline.type,
"date": str(self.deadline.date),
"comment": self.deadline.comment,
}
self.load_initial_data(form_data)
def save(self):
deadline = self.deadline
deadline.type = self.cleaned_data.get("type", None)
deadline.date = self.cleaned_data.get("date", None)
deadline.comment = self.cleaned_data.get("comment", None)
deadline.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=DEADLINE_EDITED)
return deadline
class NewActionModalForm(BaseModalForm):
""" Form handling action related input
Compensation actions are the actions performed on the area, which shall be compensated. Actions will change the
surface of the area, the biotopes, and have an environmental impact. With actions the before-after states can change
(not in the process logic in Konova, but in the real world).
"""
from compensation.models import UnitChoices
action_type = forms.MultipleChoiceField(
label=_("Action Type"),
label_suffix="",
required=True,
help_text=_("An action can consist of multiple different action types. All the selected action types are expected to be performed according to the amount and unit below on this form."),
choices=[],
widget=CompensationActionTreeCheckboxSelectMultiple(),
)
action_type_details = forms.ModelMultipleChoiceField(
label=_("Action Type detail"),
label_suffix="",
required=False,
help_text=_("Select the action type detail"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_COMPENSATION_ACTION_DETAIL_ID],
),
widget=autocomplete.ModelSelect2Multiple(
url="codes-compensation-action-detail-autocomplete",
attrs={
"data-placeholder": _("Action Type detail"),
}
),
)
unit = forms.ChoiceField(
label=_("Unit"),
label_suffix="",
required=True,
help_text=_("Select the unit"),
choices=UnitChoices.choices,
widget=forms.Select(
attrs={
"class": "form-control"
}
)
)
amount = forms.DecimalField(
label=_("Amount"),
label_suffix="",
required=True,
help_text=_("Insert the amount"),
decimal_places=2,
min_value=0.00,
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00",
}
)
)
comment = forms.CharField(
required=False,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment"),
widget=forms.Textarea(
attrs={
"rows": 5,
"class": "form-control",
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New action")
self.form_caption = _("Insert data for the new action")
choices =KonovaCode.objects.filter(
code_lists__in=[CODELIST_COMPENSATION_ACTION_ID],
is_archived=False,
is_leaf=True,
).values_list("id", flat=True)
choices = [
(choice, choice)
for choice in choices
]
self.fields["action_type"].choices = choices
def save(self):
action = self.instance.add_action(self)
self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_ACTION)
return action
class EditCompensationActionModalForm(NewActionModalForm):
action = None
def __init__(self, *args, **kwargs):
self.action = kwargs.pop("action", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit action")
form_data = {
"action_type": list(self.action.action_type.values_list("id", flat=True)),
"action_type_details": self.action.action_type_details.all(),
"amount": self.action.amount,
"unit": self.action.unit,
"comment": self.action.comment,
}
self.load_initial_data(form_data)
def save(self):
action = self.action
action.action_type.set(self.cleaned_data.get("action_type", []))
action.action_type_details.set(self.cleaned_data.get("action_type_details", []))
action.amount = self.cleaned_data.get("amount", None)
action.unit = self.cleaned_data.get("unit", None)
action.comment = self.cleaned_data.get("comment", None)
action.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=COMPENSATION_ACTION_EDITED)
return action
class NewCompensationDocumentModalForm(NewDocumentModalForm):
document_model = CompensationDocument
class NewEcoAccountDocumentModalForm(NewDocumentModalForm):
document_model = EcoAccountDocument

View File

@@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""

View File

@@ -0,0 +1,155 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from dal import autocomplete
from django import forms
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID
from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple
from konova.forms.modals import BaseModalForm, RemoveModalForm
from konova.utils.message_templates import COMPENSATION_ACTION_EDITED, ADDED_COMPENSATION_ACTION
class NewCompensationActionModalForm(BaseModalForm):
""" Form handling action related input
Compensation actions are the actions performed on the area, which shall be compensated. Actions will change the
surface of the area, the biotopes, and have an environmental impact. With actions the before-after states can change
(not in the process logic in Konova, but in the real world).
"""
from compensation.models import UnitChoices
action_type = forms.MultipleChoiceField(
label=_("Action Type"),
label_suffix="",
required=True,
help_text=_("An action can consist of multiple different action types. All the selected action types are expected to be performed according to the amount and unit below on this form."),
choices=[],
widget=CompensationActionTreeCheckboxSelectMultiple(),
)
action_type_details = forms.ModelMultipleChoiceField(
label=_("Action Type detail"),
label_suffix="",
required=False,
help_text=_("Select the action type detail"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_COMPENSATION_ACTION_DETAIL_ID],
),
widget=autocomplete.ModelSelect2Multiple(
url="codelist:compensation-action-detail-autocomplete",
attrs={
"data-placeholder": _("Action Type detail"),
}
),
)
unit = forms.ChoiceField(
label=_("Unit"),
label_suffix="",
required=True,
help_text=_("Select the unit"),
choices=UnitChoices.choices,
widget=forms.Select(
attrs={
"class": "form-control"
}
)
)
amount = forms.DecimalField(
label=_("Amount"),
label_suffix="",
required=True,
help_text=_("Insert the amount"),
decimal_places=2,
min_value=0.00,
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00",
}
)
)
comment = forms.CharField(
required=False,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment"),
widget=forms.Textarea(
attrs={
"rows": 5,
"class": "form-control",
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New action")
self.form_caption = _("Insert data for the new action")
choices =KonovaCode.objects.filter(
code_lists__in=[CODELIST_COMPENSATION_ACTION_ID],
is_archived=False,
is_leaf=True,
).values_list("id", flat=True)
choices = [
(choice, choice)
for choice in choices
]
self.fields["action_type"].choices = choices
def save(self):
action = self.instance.add_action(self)
self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_ACTION)
return action
class EditCompensationActionModalForm(NewCompensationActionModalForm):
action = None
def __init__(self, *args, **kwargs):
self.action = kwargs.pop("action", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit action")
form_data = {
"action_type": list(self.action.action_type.values_list("id", flat=True)),
"action_type_details": self.action.action_type_details.all(),
"amount": self.action.amount,
"unit": self.action.unit,
"comment": self.action.comment,
}
self.load_initial_data(form_data)
def save(self):
action = self.action
action.action_type.set(self.cleaned_data.get("action_type", []))
action.action_type_details.set(self.cleaned_data.get("action_type_details", []))
action.amount = self.cleaned_data.get("amount", None)
action.unit = self.cleaned_data.get("unit", None)
action.comment = self.cleaned_data.get("comment", None)
action.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=COMPENSATION_ACTION_EDITED)
return action
class RemoveCompensationActionModalForm(RemoveModalForm):
""" Removing modal form for CompensationAction
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
action = None
def __init__(self, *args, **kwargs):
action = kwargs.pop("action", None)
self.action = action
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_action(self)

View File

@@ -0,0 +1,92 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from django import forms
from django.utils.translation import gettext_lazy as _
from konova.forms.modals import BaseModalForm
from konova.models import DeadlineType
from konova.utils.message_templates import DEADLINE_EDITED
class NewDeadlineModalForm(BaseModalForm):
""" Form handling deadline related input
"""
type = forms.ChoiceField(
label=_("Deadline Type"),
label_suffix="",
required=True,
help_text=_("Select the deadline type"),
choices=DeadlineType.choices,
widget=forms.Select(
attrs={
"class": "form-control"
}
)
)
date = forms.DateField(
label=_("Date"),
label_suffix="",
required=True,
help_text=_("Select date"),
widget=forms.DateInput(
attrs={
"type": "date",
"data-provide": "datepicker",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
comment = forms.CharField(
required=False,
max_length=200,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment, maximum {} letters").format(200),
widget=forms.Textarea(
attrs={
"cols": 30,
"rows": 5,
"class": "form-control",
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New deadline")
self.form_caption = _("Insert data for the new deadline")
def save(self):
deadline = self.instance.add_deadline(self)
return deadline
class EditDeadlineModalForm(NewDeadlineModalForm):
deadline = None
def __init__(self, *args, **kwargs):
self.deadline = kwargs.pop("deadline", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit deadline")
form_data = {
"type": self.deadline.type,
"date": str(self.deadline.date),
"comment": self.deadline.comment,
}
self.load_initial_data(form_data)
def save(self):
deadline = self.deadline
deadline.type = self.cleaned_data.get("type", None)
deadline.date = self.cleaned_data.get("date", None)
deadline.comment = self.cleaned_data.get("comment", None)
deadline.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=DEADLINE_EDITED)
return deadline

View File

@@ -0,0 +1,17 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from compensation.models import CompensationDocument, EcoAccountDocument
from konova.forms.modals import NewDocumentModalForm
class NewCompensationDocumentModalForm(NewDocumentModalForm):
document_model = CompensationDocument
class NewEcoAccountDocumentModalForm(NewDocumentModalForm):
document_model = EcoAccountDocument

View File

@@ -0,0 +1,136 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from django import forms
from django.utils.translation import pgettext_lazy as _con, gettext_lazy as _
from konova.forms.modals import RemoveModalForm, BaseModalForm
from konova.utils.message_templates import PAYMENT_EDITED
class NewPaymentForm(BaseModalForm):
""" Form handling payment related input
"""
amount = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_con("money", "Amount"), # contextual translation
label_suffix=_(""),
help_text=_("in Euro"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00",
}
)
)
due = forms.DateField(
label=_("Due on"),
label_suffix=_(""),
required=False,
help_text=_("Due on which date"),
widget=forms.DateInput(
attrs={
"type": "date",
"data-provide": "datepicker",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
comment = forms.CharField(
max_length=200,
required=False,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment, maximum {} letters").format(200),
widget=forms.Textarea(
attrs={
"rows": 5,
"class": "form-control"
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.intervention = self.instance
self.form_title = _("Payment")
self.form_caption = _("Add a payment for intervention '{}'").format(self.intervention.title)
def is_valid(self):
"""
Checks on form validity.
For this form we need to make sure that a date or a comment is set.
If both are missing, the user needs to enter at least an explanation why
there is no date to be entered.
Returns:
is_valid (bool): True if valid, False otherwise
"""
super_valid = super().is_valid()
date = self.cleaned_data["due"]
comment = self.cleaned_data["comment"] or None
if not date and not comment:
# At least one needs to be set!
self.add_error(
"comment",
_("If there is no date you can enter, please explain why.")
)
return False
return super_valid
def save(self):
pay = self.instance.add_payment(self)
return pay
class EditPaymentModalForm(NewPaymentForm):
""" Form handling edit for Payment
"""
payment = None
def __init__(self, *args, **kwargs):
self.payment = kwargs.pop("payment", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit payment")
form_date = {
"amount": self.payment.amount,
"due": str(self.payment.due_on),
"comment": self.payment.comment,
}
self.load_initial_data(form_date, disabled_fields=[])
def save(self):
payment = self.payment
payment.amount = self.cleaned_data.get("amount", None)
payment.due_on = self.cleaned_data.get("due", None)
payment.comment = self.cleaned_data.get("comment", None)
payment.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=PAYMENT_EDITED)
self.instance.send_data_to_egon()
return payment
class RemovePaymentModalForm(RemoveModalForm):
""" Removing modal form for Payment
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
payment = None
def __init__(self, *args, **kwargs):
payment = kwargs.pop("payment", None)
self.payment = payment
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_payment(self)

View File

@@ -0,0 +1,179 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from bootstrap_modal_forms.utils import is_ajax
from dal import autocomplete
from django import forms
from django.contrib import messages
from django.http import HttpResponseRedirect, HttpRequest
from django.shortcuts import render
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
from intervention.inputs import CompensationStateTreeRadioSelect
from konova.contexts import BaseContext
from konova.forms.modals import RemoveModalForm, BaseModalForm
from konova.utils.message_templates import COMPENSATION_STATE_EDITED, FORM_INVALID, ADDED_COMPENSATION_STATE
class NewCompensationStateModalForm(BaseModalForm):
""" Form handling state related input
Compensation states refer to 'before' and 'after' states of a compensated surface. Basically it means:
What has been on this area before changes/compensations have been applied and what will be the result ('after')?
"""
biotope_type = forms.ChoiceField(
label=_("Biotope Type"),
label_suffix="",
required=True,
help_text=_("Select the biotope type"),
widget=CompensationStateTreeRadioSelect(),
)
biotope_extra = forms.ModelMultipleChoiceField(
label=_("Biotope additional type"),
label_suffix="",
required=False,
help_text=_("Select an additional biotope type"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_ID],
),
widget=autocomplete.ModelSelect2Multiple(
url="codelist:biotope-extra-type-autocomplete",
attrs={
"data-placeholder": _("Biotope additional type"),
}
),
)
surface = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_("Surface"),
label_suffix="",
required=True,
help_text=_("in m²"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00"
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New state")
self.form_caption = _("Insert data for the new state")
choices = KonovaCode.objects.filter(
code_lists__in=[CODELIST_BIOTOPES_ID],
is_archived=False,
is_leaf=True,
).values_list("id", flat=True)
choices = [
(choice, choice)
for choice in choices
]
self.fields["biotope_type"].choices = choices
def save(self, is_before_state: bool = False):
state = self.instance.add_state(self, is_before_state)
self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_STATE)
return state
def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None):
""" Generic processing of request
Wraps the request processing logic, so we don't need the same code everywhere a RemoveModalForm is being used
+++
The generic method from super class can not be used, since we need to do some request parameter check in here.
+++
Args:
request (HttpRequest): The incoming request
msg_success (str): The message in case of successful removing
msg_error (str): The message in case of an error
Returns:
"""
redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home")
template = self.template
if request.method == "POST":
if self.is_valid():
# Modal forms send one POST for checking on data validity. This can be used to return possible errors
# on the form. A second POST (if no errors occured) is sent afterwards and needs to process the
# saving/commiting of the data to the database. is_ajax() performs this check. The first request is
# an ajax call, the second is a regular form POST.
if not is_ajax(request.META):
is_before_state = bool(request.GET.get("before", False))
self.save(is_before_state=is_before_state)
messages.success(
request,
msg_success
)
return HttpResponseRedirect(redirect_url)
else:
context = {
"form": self,
}
context = BaseContext(request, context).context
return render(request, template, context)
elif request.method == "GET":
context = {
"form": self,
}
context = BaseContext(request, context).context
return render(request, template, context)
else:
raise NotImplementedError
class EditCompensationStateModalForm(NewCompensationStateModalForm):
state = None
def __init__(self, *args, **kwargs):
self.state = kwargs.pop("state", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit state")
biotope_type_id = self.state.biotope_type.id if self.state.biotope_type else None
form_data = {
"biotope_type": biotope_type_id,
"biotope_extra": self.state.biotope_type_details.all(),
"surface": self.state.surface,
}
self.load_initial_data(form_data)
def save(self, is_before_state: bool = False):
state = self.state
biotope_type_id = self.cleaned_data.get("biotope_type", None)
state.biotope_type = KonovaCode.objects.get(id=biotope_type_id)
state.biotope_type_details.set(self.cleaned_data.get("biotope_extra", []))
state.surface = self.cleaned_data.get("surface", None)
state.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=COMPENSATION_STATE_EDITED)
return state
class RemoveCompensationStateModalForm(RemoveModalForm):
""" Removing modal form for CompensationState
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
state = None
def __init__(self, *args, **kwargs):
state = kwargs.pop("state", None)
self.state = state
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_state(self)

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
@@ -160,7 +167,7 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
Returns:
"""
return reverse("compensation:acc:share", args=(self.id, self.access_token))
return reverse("compensation:acc:share-token", args=(self.id, self.access_token))
def send_notification_mail_on_deduction_change(self, data_change: dict):
""" Sends notification mails for changes on the deduction
@@ -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

@@ -1,346 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 01.12.20
"""
from konova.utils.message_templates import DATA_IS_UNCHECKED, DATA_CHECKED_ON_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE
from django.http import HttpRequest
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from compensation.filters import CompensationTableFilter, EcoAccountTableFilter
from compensation.models import Compensation, EcoAccount
from konova.utils.tables import BaseTable, TableRenderMixin
import django_tables2 as tables
class CompensationTable(BaseTable, TableRenderMixin):
id = tables.Column(
verbose_name=_("Identifier"),
orderable=True,
accessor="identifier",
)
t = tables.Column(
verbose_name=_("Title"),
orderable=True,
accessor="title",
)
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=True,
accessor="geometry",
)
c = tables.Column(
verbose_name=_("Checked"),
orderable=True,
empty_values=[],
accessor="intervention__checked",
)
r = tables.Column(
verbose_name=_("Recorded"),
orderable=True,
empty_values=[],
accessor="intervention__recorded",
)
e = tables.Column(
verbose_name=_("Editable"),
orderable=True,
empty_values=[],
accessor="intervention__users",
)
lm = tables.Column(
verbose_name=_("Last edit"),
orderable=True,
accessor="modified__timestamp",
)
class Meta(BaseTable.Meta):
template_name = "django_tables2/bootstrap4.html"
def __init__(self, request: HttpRequest, *args, **kwargs):
self.title = _("Compensations")
self.add_new_url = reverse("compensation:new")
qs = kwargs.get("queryset", None)
self.filter = CompensationTableFilter(
user=request.user,
data=request.GET,
queryset=qs,
)
kwargs["queryset"] = self.filter.qs
super().__init__(request, *args, **kwargs)
def render_id(self, value, record: Compensation):
""" Renders the id column for a compensation
Args:
value (str): The identifier value
record (Compensation): The compensation record
Returns:
"""
context = {
"tooltip": _("Open {}").format(_("Intervention")),
"content": value,
"url": reverse("compensation:detail", args=(record.id,)),
"has_revocations": record.intervention.legal.revocations.exists()
}
html = render_to_string(
"table/revocation_warning_col.html",
context
)
return html
def render_c(self, value, record: Compensation):
""" Renders the checked column for a compensation
checked is set by the main object Intervention
Args:
value (str): The identifier value
record (Compensation): The compensation record
Returns:
"""
html = ""
checked = value is not None
tooltip = DATA_IS_UNCHECKED
previously_checked = record.intervention.get_last_checked_action()
if checked:
checked_on = value.get_timestamp_str_formatted()
tooltip = DATA_CHECKED_ON_TEMPLATE.format(checked_on, record.intervention.checked.user)
html += self.render_checked_star(
tooltip=tooltip,
icn_filled=checked,
)
if previously_checked and not checked:
checked_on = previously_checked.get_timestamp_str_formatted()
tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(checked_on, previously_checked.user)
html += self.render_previously_checked_star(
tooltip=tooltip,
)
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
Args:
value (str): The identifier value
record (Compensation): The compensation record
Returns:
"""
html = ""
recorded = value is not None
tooltip = _("Not recorded yet")
if recorded:
on = value.get_timestamp_str_formatted()
tooltip = _("Recorded on {} by {}").format(on, record.intervention.recorded.user)
html += self.render_bookmark(
tooltip=tooltip,
icn_filled=recorded,
)
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)
class EcoAccountTable(BaseTable, TableRenderMixin):
id = tables.Column(
verbose_name=_("Identifier"),
orderable=True,
accessor="identifier",
)
t = tables.Column(
verbose_name=_("Title"),
orderable=True,
accessor="title",
)
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=True,
accessor="geometry",
)
av = tables.Column(
verbose_name=_("Available"),
orderable=True,
empty_values=[],
attrs={
"th": {
"class": "w-20",
}
}
)
r = tables.Column(
verbose_name=_("Recorded"),
orderable=True,
empty_values=[],
accessor="recorded",
)
e = tables.Column(
verbose_name=_("Editable"),
orderable=True,
empty_values=[],
accessor="users",
)
lm = tables.Column(
verbose_name=_("Last edit"),
orderable=True,
accessor="modified__timestamp",
)
class Meta(BaseTable.Meta):
template_name = "django_tables2/bootstrap4.html"
def __init__(self, request: HttpRequest, *args, **kwargs):
self.title = _("Eco Accounts")
self.add_new_url = reverse("compensation:acc:new")
qs = kwargs.get("queryset", None)
self.filter = EcoAccountTableFilter(
user=request.user,
data=request.GET,
queryset=qs,
)
kwargs["queryset"] = self.filter.qs
super().__init__(request, *args, **kwargs)
def render_id(self, value, record: EcoAccount):
""" Renders the id column for an eco account
Args:
value (str): The identifier value
record (EcoAccount): The eco account record
Returns:
"""
html = ""
html += self.render_link(
tooltip=_("Open {}").format(_("Eco-account")),
href=reverse("compensation:acc:detail", args=(record.id,)),
txt=value,
new_tab=False,
)
return format_html(html)
def render_av(self, value, record: EcoAccount):
""" Renders the available column for an eco account
Args:
value (str): The identifier value
record (EcoAccount): The eco account record
Returns:
"""
value_total, value_relative = record.get_available_rest()
html = render_to_string("konova/widgets/progressbar.html", {"value": value_relative})
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: EcoAccount):
""" Renders the recorded column for an eco account
Args:
value (str): The identifier value
record (EcoAccount): The eco account record
Returns:
"""
html = ""
checked = value is not None
tooltip = _("Not recorded yet. Can not be used for deductions, yet.")
if checked:
on = value.get_timestamp_str_formatted()
tooltip = _("Recorded on {} by {}").format(on, record.recorded.user)
html += self.render_bookmark(
tooltip=tooltip,
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

@@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""

View File

@@ -0,0 +1,150 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from django.http import HttpRequest
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.html import format_html
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, TableOrderMixin
import django_tables2 as tables
class CompensationTable(BaseTable, TableRenderMixin, TableOrderMixin):
id = tables.Column(
verbose_name=_("Identifier"),
orderable=True,
accessor="identifier",
)
t = tables.Column(
verbose_name=_("Title"),
orderable=True,
accessor="title",
)
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=False,
accessor="geometry",
)
c = tables.Column(
verbose_name=_("Checked"),
orderable=True,
empty_values=[],
accessor="intervention__checked",
)
r = tables.Column(
verbose_name=_("Recorded"),
orderable=True,
empty_values=[],
accessor="intervention__recorded",
)
e = tables.Column(
verbose_name=_("Editable"),
orderable=True,
empty_values=[],
accessor="intervention__users",
)
lm = tables.Column(
verbose_name=_("Last edit"),
orderable=True,
accessor="modified__timestamp",
)
class Meta(BaseTable.Meta):
template_name = "django_tables2/bootstrap4.html"
def __init__(self, request: HttpRequest, *args, **kwargs):
self.title = _("Compensations")
self.add_new_url = reverse("compensation:new")
qs = kwargs.get("queryset", None)
self.filter = CompensationTableFilter(
user=request.user,
data=request.GET,
queryset=qs,
)
kwargs["queryset"] = self.filter.qs
super().__init__(request, *args, **kwargs)
def render_id(self, value, record: Compensation):
""" Renders the id column for a compensation
Args:
value (str): The identifier value
record (Compensation): The compensation record
Returns:
"""
context = {
"tooltip": _("Open {}").format(_("Intervention")),
"content": value,
"url": reverse("compensation:detail", args=(record.id,)),
"has_revocations": record.intervention.legal.revocations.exists()
}
html = render_to_string(
"table/revocation_warning_col.html",
context
)
return html
def render_c(self, value, record: Compensation):
""" Renders the checked column for a compensation
checked is set by the main object Intervention
Args:
value (str): The identifier value
record (Compensation): The compensation record
Returns:
"""
html = ""
checked = value is not None
tooltip = DATA_IS_UNCHECKED
previously_checked = record.intervention.get_last_checked_action()
if checked:
checked_on = value.get_timestamp_str_formatted()
tooltip = DATA_CHECKED_ON_TEMPLATE.format(checked_on, record.intervention.checked.user)
html += self.render_checked_star(
tooltip=tooltip,
icn_filled=checked,
)
if previously_checked and not checked:
checked_on = previously_checked.get_timestamp_str_formatted()
tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(checked_on, previously_checked.user)
html += self.render_previously_checked_star(
tooltip=tooltip,
)
return format_html(html)
def render_r(self, value, record: Compensation):
""" Renders the registered column for a compensation
Args:
value (str): The identifier value
record (Compensation): The compensation record
Returns:
"""
html = ""
recorded = value is not None
tooltip = _("Not recorded yet")
if recorded:
on = value.get_timestamp_str_formatted()
tooltip = _("Recorded on {} by {}").format(on, record.intervention.recorded.user)
html += self.render_bookmark(
tooltip=tooltip,
icn_filled=recorded,
)
return format_html(html)

View File

@@ -0,0 +1,136 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from django.http import HttpRequest
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.html import format_html
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, TableOrderMixin
import django_tables2 as tables
class EcoAccountTable(BaseTable, TableRenderMixin, TableOrderMixin):
id = tables.Column(
verbose_name=_("Identifier"),
orderable=True,
accessor="identifier",
)
t = tables.Column(
verbose_name=_("Title"),
orderable=True,
accessor="title",
)
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=False,
accessor="geometry",
)
av = tables.Column(
verbose_name=_("Available"),
orderable=True,
accessor="deductable_rest",
attrs={
"th": {
"class": "w-20",
}
}
)
r = tables.Column(
verbose_name=_("Recorded"),
orderable=True,
empty_values=[],
accessor="recorded",
)
e = tables.Column(
verbose_name=_("Editable"),
orderable=True,
empty_values=[],
accessor="users",
)
lm = tables.Column(
verbose_name=_("Last edit"),
orderable=True,
accessor="modified__timestamp",
)
class Meta(BaseTable.Meta):
template_name = "django_tables2/bootstrap4.html"
def __init__(self, request: HttpRequest, *args, **kwargs):
self.title = _("Eco Accounts")
self.add_new_url = reverse("compensation:acc:new")
qs = kwargs.get("queryset", None)
self.filter = EcoAccountTableFilter(
user=request.user,
data=request.GET,
queryset=qs,
)
kwargs["queryset"] = self.filter.qs
super().__init__(request, *args, **kwargs)
def render_id(self, value, record: EcoAccount):
""" Renders the id column for an eco account
Args:
value (str): The identifier value
record (EcoAccount): The eco account record
Returns:
"""
html = ""
html += self.render_link(
tooltip=_("Open {}").format(_("Eco-account")),
href=reverse("compensation:acc:detail", args=(record.id,)),
txt=value,
new_tab=False,
)
return format_html(html)
def render_av(self, value, record: EcoAccount):
""" Renders the available column for an eco account
Args:
value (float): The deductable_rest
record (EcoAccount): The eco account record
Returns:
"""
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_r(self, value, record: EcoAccount):
""" Renders the recorded column for an eco account
Args:
value (str): The identifier value
record (EcoAccount): The eco account record
Returns:
"""
html = ""
checked = value is not None
tooltip = _("Not recorded yet. Can not be used for deductions, yet.")
if checked:
on = value.get_timestamp_str_formatted()
tooltip = _("Recorded on {} by {}").format(on, record.recorded.user)
html += self.render_bookmark(
tooltip=tooltip,
icn_filled=checked,
)
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

@@ -15,7 +15,7 @@
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'compensation:acc:resubmission-create' obj.id %}">
{% fa5_icon 'bell' %}
</button>
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'compensation:acc:share-create' obj.id %}">
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'compensation:acc:share-form' obj.id %}">
{% fa5_icon 'share-alt' %}
</button>
{% if is_ets_member %}

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

@@ -6,7 +6,20 @@ Created on: 24.08.21
"""
from django.urls import path
from compensation.views.compensation import *
from compensation.views.compensation.document import EditCompensationDocumentView, NewCompensationDocumentView, \
GetCompensationDocumentView, RemoveCompensationDocumentView
from compensation.views.compensation.resubmission import CompensationResubmissionView
from compensation.views.compensation.report import report_view
from compensation.views.compensation.deadline import NewCompensationDeadlineView, EditCompensationDeadlineView, \
RemoveCompensationDeadlineView
from compensation.views.compensation.action import NewCompensationActionView, EditCompensationActionView, \
RemoveCompensationActionView
from compensation.views.compensation.state import NewCompensationStateView, EditCompensationStateView, \
RemoveCompensationStateView
from compensation.views.compensation.compensation import index_view, new_view, new_id_view, detail_view, edit_view, \
remove_view
from compensation.views.compensation.log import CompensationLogView
urlpatterns = [
# Main compensation
@@ -15,28 +28,28 @@ urlpatterns = [
path('new/<intervention_id>', new_view, name='new'),
path('new', new_view, name='new'),
path('<id>', detail_view, name='detail'),
path('<id>/log', log_view, name='log'),
path('<id>/log', CompensationLogView.as_view(), name='log'),
path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', remove_view, name='remove'),
path('<id>/state/new', state_new_view, name='new-state'),
path('<id>/state/<state_id>/edit', state_edit_view, name='state-edit'),
path('<id>/state/<state_id>/remove', state_remove_view, name='state-remove'),
path('<id>/state/new', NewCompensationStateView.as_view(), name='new-state'),
path('<id>/state/<state_id>/edit', EditCompensationStateView.as_view(), name='state-edit'),
path('<id>/state/<state_id>/remove', RemoveCompensationStateView.as_view(), name='state-remove'),
path('<id>/action/new', action_new_view, name='new-action'),
path('<id>/action/<action_id>/edit', action_edit_view, name='action-edit'),
path('<id>/action/<action_id>/remove', action_remove_view, name='action-remove'),
path('<id>/action/new', NewCompensationActionView.as_view(), name='new-action'),
path('<id>/action/<action_id>/edit', EditCompensationActionView.as_view(), name='action-edit'),
path('<id>/action/<action_id>/remove', RemoveCompensationActionView.as_view(), name='action-remove'),
path('<id>/deadline/new', deadline_new_view, name="new-deadline"),
path('<id>/deadline/<deadline_id>/edit', deadline_edit_view, name='deadline-edit'),
path('<id>/deadline/<deadline_id>/remove', deadline_remove_view, name='deadline-remove'),
path('<id>/deadline/new', NewCompensationDeadlineView.as_view(), name="new-deadline"),
path('<id>/deadline/<deadline_id>/edit', EditCompensationDeadlineView.as_view(), name='deadline-edit'),
path('<id>/deadline/<deadline_id>/remove', RemoveCompensationDeadlineView.as_view(), name='deadline-remove'),
path('<id>/report', report_view, name='report'),
path('<id>/resub', create_resubmission_view, name='resubmission-create'),
path('<id>/resub', CompensationResubmissionView.as_view(), name='resubmission-create'),
# Documents
path('<id>/document/new/', new_document_view, name='new-doc'),
path('<id>/document/<doc_id>', get_document_view, name='get-doc'),
path('<id>/document/<doc_id>/remove/', remove_document_view, name='remove-doc'),
path('<id>/document/<doc_id>/edit/', edit_document_view, name='edit-doc'),
path('<id>/document/new/', NewCompensationDocumentView.as_view(), name='new-doc'),
path('<id>/document/<doc_id>', GetCompensationDocumentView.as_view(), name='get-doc'),
path('<id>/document/<doc_id>/remove/', RemoveCompensationDocumentView.as_view(), name='remove-doc'),
path('<id>/document/<doc_id>/edit/', EditCompensationDocumentView.as_view(), name='edit-doc'),
]

View File

@@ -6,7 +6,25 @@ Created on: 24.08.21
"""
from django.urls import path
from compensation.views.eco_account import *
from compensation.autocomplete.eco_account import EcoAccountAutocomplete
from compensation.views.eco_account.eco_account import index_view, new_view, new_id_view, edit_view, remove_view, \
detail_view
from compensation.views.eco_account.log import EcoAccountLogView
from compensation.views.eco_account.record import EcoAccountRecordView
from compensation.views.eco_account.report import report_view
from compensation.views.eco_account.resubmission import EcoAccountResubmissionView
from compensation.views.eco_account.state import NewEcoAccountStateView, EditEcoAccountStateView, \
RemoveEcoAccountStateView
from compensation.views.eco_account.action import NewEcoAccountActionView, EditEcoAccountActionView, \
RemoveEcoAccountActionView
from compensation.views.eco_account.deadline import NewEcoAccountDeadlineView, EditEcoAccountDeadlineView, \
RemoveEcoAccountDeadlineView
from compensation.views.eco_account.share import EcoAccountShareByTokenView, EcoAccountShareFormView
from compensation.views.eco_account.document import GetEcoAccountDocumentView, NewEcoAccountDocumentView, \
EditEcoAccountDocumentView, RemoveEcoAccountDocumentView
from compensation.views.eco_account.deduction import NewEcoAccountDeductionView, EditEcoAccountDeductionView, \
RemoveEcoAccountDeductionView
app_name = "acc"
urlpatterns = [
@@ -14,37 +32,39 @@ urlpatterns = [
path('new/', new_view, name='new'),
path('new/id', new_id_view, name='new-id'),
path('<id>', detail_view, name='detail'),
path('<id>/log', log_view, name='log'),
path('<id>/record', record_view, name='record'),
path('<id>/log', EcoAccountLogView.as_view(), name='log'),
path('<id>/record', EcoAccountRecordView.as_view(), name='record'),
path('<id>/report', report_view, name='report'),
path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', remove_view, name='remove'),
path('<id>/resub', create_resubmission_view, name='resubmission-create'),
path('<id>/resub', EcoAccountResubmissionView.as_view(), name='resubmission-create'),
path('<id>/state/new', state_new_view, name='new-state'),
path('<id>/state/<state_id>/edit', state_edit_view, name='state-edit'),
path('<id>/state/<state_id>/remove', state_remove_view, name='state-remove'),
path('<id>/state/new', NewEcoAccountStateView.as_view(), name='new-state'),
path('<id>/state/<state_id>/edit', EditEcoAccountStateView.as_view(), name='state-edit'),
path('<id>/state/<state_id>/remove', RemoveEcoAccountStateView.as_view(), name='state-remove'),
path('<id>/action/new', action_new_view, name='new-action'),
path('<id>/action/<action_id>/edit', action_edit_view, name='action-edit'),
path('<id>/action/<action_id>/remove', action_remove_view, name='action-remove'),
path('<id>/action/new', NewEcoAccountActionView.as_view(), name='new-action'),
path('<id>/action/<action_id>/edit', EditEcoAccountActionView.as_view(), name='action-edit'),
path('<id>/action/<action_id>/remove', RemoveEcoAccountActionView.as_view(), name='action-remove'),
path('<id>/deadline/new', deadline_new_view, name="new-deadline"),
path('<id>/deadline/<deadline_id>/edit', deadline_edit_view, name='deadline-edit'),
path('<id>/deadline/<deadline_id>/remove', deadline_remove_view, name='deadline-remove'),
path('<id>/deadline/new', NewEcoAccountDeadlineView.as_view(), name="new-deadline"),
path('<id>/deadline/<deadline_id>/edit', EditEcoAccountDeadlineView.as_view(), name='deadline-edit'),
path('<id>/deadline/<deadline_id>/remove', RemoveEcoAccountDeadlineView.as_view(), name='deadline-remove'),
path('<id>/share/<token>', share_view, name='share'),
path('<id>/share', create_share_view, name='share-create'),
path('<id>/share/<token>', EcoAccountShareByTokenView.as_view(), name='share-token'),
path('<id>/share', EcoAccountShareFormView.as_view(), name='share-form'),
# Documents
path('<id>/document/new/', new_document_view, name='new-doc'),
path('<id>/document/<doc_id>', get_document_view, name='get-doc'),
path('<id>/document/<doc_id>/edit', edit_document_view, name='edit-doc'),
path('<id>/document/<doc_id>/remove/', remove_document_view, name='remove-doc'),
path('<id>/document/new/', NewEcoAccountDocumentView.as_view(), name='new-doc'),
path('<id>/document/<doc_id>', GetEcoAccountDocumentView.as_view(), name='get-doc'),
path('<id>/document/<doc_id>/edit', EditEcoAccountDocumentView.as_view(), name='edit-doc'),
path('<id>/document/<doc_id>/remove/', RemoveEcoAccountDocumentView.as_view(), name='remove-doc'),
# Eco-account deductions
path('<id>/deduction/<deduction_id>/remove', deduction_remove_view, name='remove-deduction'),
path('<id>/deduction/<deduction_id>/edit', deduction_edit_view, name='edit-deduction'),
path('<id>/deduct/new', new_deduction_view, name='new-deduction'),
path('<id>/deduction/<deduction_id>/remove', RemoveEcoAccountDeductionView.as_view(), name='remove-deduction'),
path('<id>/deduction/<deduction_id>/edit', EditEcoAccountDeductionView.as_view(), name='edit-deduction'),
path('<id>/deduct/new', NewEcoAccountDeductionView.as_view(), name='new-deduction'),
]
# Autocomplete
path("atcmplt/eco-accounts", EcoAccountAutocomplete.as_view(), name="autocomplete"),
]

View File

@@ -5,6 +5,5 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.21
"""
from .compensation import *
from .eco_account import *
from .payment import *

View File

@@ -1,683 +0,0 @@
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Sum
from django.http import HttpRequest, JsonResponse
from django.shortcuts import render
from django.utils.translation import gettext_lazy as _
from compensation.forms.forms import NewCompensationForm, EditCompensationForm
from compensation.forms.modalForms import NewStateModalForm, NewDeadlineModalForm, NewActionModalForm, \
NewCompensationDocumentModalForm, RemoveCompensationActionModalForm, RemoveCompensationStateModalForm, \
EditCompensationStateModalForm, EditCompensationActionModalForm, EditDeadlineModalForm
from compensation.models import Compensation, CompensationState, CompensationAction, CompensationDocument
from compensation.tables import CompensationTable
from intervention.models import Intervention
from konova.contexts import BaseContext
from konova.decorators import *
from konova.forms.modals import RemoveModalForm,RemoveDeadlineModalForm, EditDocumentModalForm, \
ResubmissionModalForm
from konova.forms import SimpleGeomForm
from konova.models import Deadline
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.documents import get_document, remove_document
from konova.utils.generators import generate_qr_code
from konova.utils.message_templates import FORM_INVALID, IDENTIFIER_REPLACED, DATA_UNSHARED_EXPLANATION, \
CHECKED_RECORDED_RESET, COMPENSATION_ADDED_TEMPLATE, COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, \
COMPENSATION_STATE_REMOVED, COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_ADDED, \
DEADLINE_ADDED, DEADLINE_REMOVED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, \
DEADLINE_EDITED, RECORDED_BLOCKS_EDIT, PARAMS_INVALID, DATA_CHECKED_PREVIOUSLY_TEMPLATE
from konova.utils.user_checks import in_group
@login_required
@any_group_check
def index_view(request: HttpRequest):
"""
Renders the index view for compensation
Args:
request (HttpRequest): The incoming request
Returns:
A rendered view
"""
template = "generic_index.html"
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
)
table = CompensationTable(
request=request,
queryset=compensations
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Compensations - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@default_group_required
@shared_access_required(Intervention, "intervention_id")
def new_view(request: HttpRequest, intervention_id: str = None):
"""
Renders a view for a new compensation creation
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "compensation/form/view.html"
if intervention_id is not None:
try:
intervention = Intervention.objects.get(id=intervention_id)
except ObjectDoesNotExist:
messages.error(request, PARAMS_INVALID)
return redirect("home")
if intervention.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("intervention:detail", id=intervention_id)
data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None)
comp = data_form.save(request.user, geom_form)
if generated_identifier != comp.identifier:
messages.info(
request,
IDENTIFIER_REPLACED.format(
generated_identifier,
comp.identifier
)
)
messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
return redirect("compensation:detail", id=comp.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New compensation"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@default_group_required
def new_id_view(request: HttpRequest):
""" JSON endpoint
Provides fetching of free identifiers for e.g. AJAX calls
"""
tmp = Compensation()
identifier = tmp.generate_new_identifier()
while Compensation.objects.filter(identifier=identifier).exists():
identifier = tmp.generate_new_identifier()
return JsonResponse(
data={
"gen_data": identifier
}
)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def edit_view(request: HttpRequest, id: str):
"""
Renders a view for editing compensations
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "compensation/form/view.html"
# Get object from db
comp = get_object_or_404(Compensation, id=id)
if comp.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("compensation:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditCompensationForm(request.POST or None, instance=comp)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
# Preserve state of intervention recorded/checked to determine whether the user must be informed or not
# about a change of the recorded/checked state
intervention_recorded = comp.intervention.recorded is not None
intervention_checked = comp.intervention.checked is not None
# The data form takes the geom form for processing, as well as the performing user
comp = data_form.save(request.user, geom_form)
if intervention_recorded or intervention_checked:
messages.info(request, CHECKED_RECORDED_RESET)
messages.success(request, _("Compensation {} edited").format(comp.identifier))
return redirect("compensation:detail", id=comp.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(comp.identifier),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@any_group_check
def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
Returns:
"""
template = "compensation/detail/compensation/view.html"
comp = get_object_or_404(Compensation, id=id)
geom_form = SimpleGeomForm(instance=comp)
parcels = comp.get_underlying_parcels()
_user = request.user
is_data_shared = comp.intervention.is_shared_with(_user)
# Order states according to surface
before_states = comp.before_states.all().prefetch_related("biotope_type").order_by("-surface")
after_states = comp.after_states.all().prefetch_related("biotope_type").order_by("-surface")
actions = comp.actions.all().prefetch_related("action_type")
# Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = before_states.aggregate(Sum("surface"))["surface__sum"] or 0
sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0
diff_states = abs(sum_before_states - sum_after_states)
request = comp.set_status_messages(request)
last_checked = comp.intervention.get_last_checked_action()
last_checked_tooltip = ""
if last_checked:
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user)
context = {
"obj": comp,
"last_checked": last_checked,
"last_checked_tooltip": last_checked_tooltip,
"geom_form": geom_form,
"parcels": parcels,
"has_access": is_data_shared,
"actions": actions,
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"is_default_member": in_group(_user, DEFAULT_GROUP),
"is_zb_member": in_group(_user, ZB_GROUP),
"is_ets_member": in_group(_user, ETS_GROUP),
"LANIS_LINK": comp.get_LANIS_link(),
TAB_TITLE_IDENTIFIER: f"{comp.identifier} - {comp.title}",
"has_finished_deadlines": comp.get_finished_deadlines().exists(),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def log_view(request: HttpRequest, id: str):
""" Renders a log view using modal
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
template = "modal/modal_generic.html"
body_template = "log.html"
context = {
"modal_body_template": body_template,
"log": comp.log.all(),
"modal_title": _("Log"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def remove_view(request: HttpRequest, id: str):
""" Renders a modal view for removing the compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
form = RemoveModalForm(request.POST or None, instance=comp, request=request)
return form.process_request(
request=request,
msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier),
redirect_url=reverse("compensation:index"),
)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def new_document_view(request: HttpRequest, id: str):
""" Renders a form for uploading new documents
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id to which the new document will be related
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
form = NewCompensationDocumentModalForm(request.POST or None, request.FILES or None, instance=comp, request=request)
return form.process_request(
request,
msg_success=DOCUMENT_ADDED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def get_document_view(request: HttpRequest, id: str, doc_id: str):
""" Returns the document as downloadable file
Wraps the generic document fetcher function from konova.utils.
Args:
request (HttpRequest): The incoming request
id (str): The compensation id
doc_id (str): The document id
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
doc = get_object_or_404(CompensationDocument, id=doc_id)
return get_document(doc)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def remove_document_view(request: HttpRequest, id: str, doc_id: str):
""" Removes the document from the database and file system
Wraps the generic functionality from konova.utils.
Args:
request (HttpRequest): The incoming request
id (str): The compensation id
doc_id (str): The document id
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
doc = get_object_or_404(CompensationDocument, id=doc_id)
return remove_document(
request,
doc
)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def edit_document_view(request: HttpRequest, id: str, doc_id: str):
""" Removes the document from the database and file system
Wraps the generic functionality from konova.utils.
Args:
request (HttpRequest): The incoming request
id (str): The compensation id
doc_id (str): The document id
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
doc = get_object_or_404(CompensationDocument, id=doc_id)
form = EditDocumentModalForm(request.POST or None, request.FILES or None, instance=comp, document=doc, request=request)
return form.process_request(
request,
DOCUMENT_EDITED,
reverse("compensation:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def state_new_view(request: HttpRequest, id: str):
""" Renders a form for adding new states for a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id to which the new state will be related
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
form = NewStateModalForm(request.POST or None, instance=comp, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_STATE_ADDED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def action_new_view(request: HttpRequest, id: str):
""" Renders a form for adding new actions for a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id to which the new state will be related
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
form = NewActionModalForm(request.POST or None, instance=comp, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_ACTION_ADDED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def action_edit_view(request: HttpRequest, id: str, action_id: str):
""" Renders a form for editing actions for a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
action_id (str): The action's id
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
action = get_object_or_404(CompensationAction, id=action_id)
form = EditCompensationActionModalForm(request.POST or None, instance=comp, action=action, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_ACTION_EDITED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def deadline_new_view(request: HttpRequest, id: str):
""" Renders a form for adding new states for a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id to which the new state will be related
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
form = NewDeadlineModalForm(request.POST or None, instance=comp, request=request)
return form.process_request(
request,
msg_success=DEADLINE_ADDED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def deadline_edit_view(request: HttpRequest, id: str, deadline_id: str):
""" Renders a form for editing deadlines from a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
deadline_id (str): The deadline's id
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
deadline = get_object_or_404(Deadline, id=deadline_id)
form = EditDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request)
return form.process_request(
request,
msg_success=DEADLINE_EDITED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str):
""" Renders a form for removing deadlines from a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
deadline_id (str): The deadline's id
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
deadline = get_object_or_404(Deadline, id=deadline_id)
form = RemoveDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request)
return form.process_request(
request,
msg_success=DEADLINE_REMOVED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def state_remove_view(request: HttpRequest, id: str, state_id: str):
""" Renders a form for removing a compensation state
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
state_id (str): The state's id
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
state = get_object_or_404(CompensationState, id=state_id)
form = RemoveCompensationStateModalForm(request.POST or None, instance=comp, state=state, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_STATE_REMOVED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def state_edit_view(request: HttpRequest, id: str, state_id: str):
""" Renders a form for editing a compensation state
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
state_id (str): The state's id
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
state = get_object_or_404(CompensationState, id=state_id)
form = EditCompensationStateModalForm(request.POST or None, instance=comp, state=state, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_STATE_EDITED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def action_remove_view(request: HttpRequest, id: str, action_id: str):
""" Renders a form for removing a compensation action
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
id (str): The action's id
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
action = get_object_or_404(CompensationAction, id=action_id)
form = RemoveCompensationActionModalForm(request.POST or None, instance=comp, action=action, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_ACTION_REMOVED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
)
def report_view(request: HttpRequest, id: str):
""" Renders the public report view
Args:
request (HttpRequest): The incoming request
id (str): The id of the intervention
Returns:
"""
# Reuse the compensation report template since compensations are structurally identical
template = "compensation/report/compensation/report.html"
comp = get_object_or_404(Compensation, id=id)
tab_title = _("Report {}").format(comp.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not comp.is_ready_for_publish():
template = "report/unavailable.html"
context = {
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)
# Prepare data for map viewer
geom_form = SimpleGeomForm(
instance=comp
)
parcels = comp.get_underlying_parcels()
qrcode_url = request.build_absolute_uri(reverse("compensation:report", args=(id,)))
qrcode_img = generate_qr_code(qrcode_url, 10)
qrcode_lanis_url = comp.get_LANIS_link()
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
# Order states by surface
before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type")
after_states = comp.after_states.all().order_by("-surface").prefetch_related("biotope_type")
actions = comp.actions.all().prefetch_related("action_type")
context = {
"obj": comp,
"qrcode": {
"img": qrcode_img,
"url": qrcode_url,
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url,
},
"has_access": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
"geom_form": geom_form,
"parcels": parcels,
"actions": actions,
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def create_resubmission_view(request: HttpRequest, id: str):
""" Renders resubmission form for a compensation
Args:
request (HttpRequest): The incoming request
id (str): Compensation's id
Returns:
"""
com = get_object_or_404(Compensation, id=id)
form = ResubmissionModalForm(request.POST or None, instance=com, request=request)
form.action_url = reverse("compensation:resubmission-create", args=(id,))
return form.process_request(
request,
msg_success=_("Resubmission set"),
redirect_url=reverse("compensation:detail", args=(id,))
)

View File

@@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""

View File

@@ -0,0 +1,57 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.decorators import method_decorator
from compensation.forms.modals.compensation_action import RemoveCompensationActionModalForm, \
EditCompensationActionModalForm, NewCompensationActionModalForm
from compensation.models import Compensation, CompensationAction
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.utils.message_templates import COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_EDITED, \
COMPENSATION_ACTION_ADDED
from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \
AbstractRemoveCompensationActionView
class NewCompensationActionView(AbstractNewCompensationActionView):
model = Compensation
redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditCompensationActionView(AbstractEditCompensationActionView):
model = Compensation
redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveCompensationActionView(AbstractRemoveCompensationActionView):
model = Compensation
redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -0,0 +1,275 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Sum
from django.http import HttpRequest, JsonResponse
from django.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from compensation.forms.compensation import EditCompensationForm, NewCompensationForm
from compensation.models import Compensation
from compensation.tables.compensation import CompensationTable
from intervention.models import Intervention
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 DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \
RECORDED_BLOCKS_EDIT, CHECKED_RECORDED_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \
COMPENSATION_ADDED_TEMPLATE
from konova.utils.user_checks import in_group
@login_required
@any_group_check
def index_view(request: HttpRequest):
"""
Renders the index view for compensation
Args:
request (HttpRequest): The incoming request
Returns:
A rendered view
"""
template = "generic_index.html"
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,
queryset=compensations
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Compensations - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@default_group_required
@shared_access_required(Intervention, "intervention_id")
def new_view(request: HttpRequest, intervention_id: str = None):
"""
Renders a view for a new compensation creation
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "compensation/form/view.html"
if intervention_id is not None:
try:
intervention = Intervention.objects.get(id=intervention_id)
except ObjectDoesNotExist:
messages.error(request, PARAMS_INVALID)
return redirect("home")
if intervention.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("intervention:detail", id=intervention_id)
data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None)
comp = data_form.save(request.user, geom_form)
if generated_identifier != comp.identifier:
messages.info(
request,
IDENTIFIER_REPLACED.format(
generated_identifier,
comp.identifier
)
)
messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
return redirect("compensation:detail", id=comp.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New compensation"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@default_group_required
def new_id_view(request: HttpRequest):
""" JSON endpoint
Provides fetching of free identifiers for e.g. AJAX calls
"""
tmp = Compensation()
identifier = tmp.generate_new_identifier()
while Compensation.objects.filter(identifier=identifier).exists():
identifier = tmp.generate_new_identifier()
return JsonResponse(
data={
"gen_data": identifier
}
)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def edit_view(request: HttpRequest, id: str):
"""
Renders a view for editing compensations
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "compensation/form/view.html"
# Get object from db
comp = get_object_or_404(Compensation, id=id)
if comp.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("compensation:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditCompensationForm(request.POST or None, instance=comp)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
# Preserve state of intervention recorded/checked to determine whether the user must be informed or not
# about a change of the recorded/checked state
intervention_recorded = comp.intervention.recorded is not None
intervention_checked = comp.intervention.checked is not None
# The data form takes the geom form for processing, as well as the performing user
comp = data_form.save(request.user, geom_form)
if intervention_recorded or intervention_checked:
messages.info(request, CHECKED_RECORDED_RESET)
messages.success(request, _("Compensation {} edited").format(comp.identifier))
return redirect("compensation:detail", id=comp.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(comp.identifier),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@any_group_check
def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
Returns:
"""
template = "compensation/detail/compensation/view.html"
comp = get_object_or_404(Compensation, id=id)
geom_form = SimpleGeomForm(instance=comp)
parcels = comp.get_underlying_parcels()
_user = request.user
is_data_shared = comp.intervention.is_shared_with(_user)
# Order states according to surface
before_states = comp.before_states.all().prefetch_related("biotope_type").order_by("-surface")
after_states = comp.after_states.all().prefetch_related("biotope_type").order_by("-surface")
actions = comp.actions.all().prefetch_related("action_type")
# Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = before_states.aggregate(Sum("surface"))["surface__sum"] or 0
sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0
diff_states = abs(sum_before_states - sum_after_states)
request = comp.set_status_messages(request)
last_checked = comp.intervention.get_last_checked_action()
last_checked_tooltip = ""
if last_checked:
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user)
context = {
"obj": comp,
"last_checked": last_checked,
"last_checked_tooltip": last_checked_tooltip,
"geom_form": geom_form,
"parcels": parcels,
"has_access": is_data_shared,
"actions": actions,
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"is_default_member": in_group(_user, DEFAULT_GROUP),
"is_zb_member": in_group(_user, ZB_GROUP),
"is_ets_member": in_group(_user, ETS_GROUP),
"LANIS_LINK": comp.get_LANIS_link(),
TAB_TITLE_IDENTIFIER: f"{comp.identifier} - {comp.title}",
"has_finished_deadlines": comp.get_finished_deadlines().exists(),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required_modal
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def remove_view(request: HttpRequest, id: str):
""" Renders a modal view for removing the compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
form = RemoveModalForm(request.POST or None, instance=comp, request=request)
return form.process_request(
request=request,
msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier),
redirect_url=reverse("compensation:index"),
)

View File

@@ -0,0 +1,49 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import Compensation
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.deadline import AbstractRemoveDeadlineView, AbstractEditDeadlineView, AbstractNewDeadlineView
class NewCompensationDeadlineView(AbstractNewDeadlineView):
model = Compensation
redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditCompensationDeadlineView(AbstractEditDeadlineView):
model = Compensation
redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveCompensationDeadlineView(AbstractRemoveDeadlineView):
model = Compensation
redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -0,0 +1,66 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.forms.modals.document import NewCompensationDocumentModalForm
from compensation.models import Compensation, CompensationDocument
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.forms.modals import EditDocumentModalForm
from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \
AbstractEditDocumentView
class NewCompensationDocumentView(AbstractNewDocumentView):
model = Compensation
form = NewCompensationDocumentModalForm
redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class GetCompensationDocumentView(AbstractGetDocumentView):
model = Compensation
document_model = CompensationDocument
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveCompensationDocumentView(AbstractRemoveDocumentView):
model = Compensation
document_model = CompensationDocument
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditCompensationDocumentView(AbstractEditDocumentView):
model = Compensation
document_model = CompensationDocument
form = EditDocumentModalForm
redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -0,0 +1,24 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import Compensation
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.log import AbstractLogView
class CompensationLogView(AbstractLogView):
model = Compensation
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -0,0 +1,79 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.http import HttpRequest
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from compensation.models import Compensation
from konova.contexts import BaseContext
from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.generators import generate_qr_code
def report_view(request: HttpRequest, id: str):
""" Renders the public report view
Args:
request (HttpRequest): The incoming request
id (str): The id of the intervention
Returns:
"""
# Reuse the compensation report template since compensations are structurally identical
template = "compensation/report/compensation/report.html"
comp = get_object_or_404(Compensation, id=id)
tab_title = _("Report {}").format(comp.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not comp.is_ready_for_publish():
template = "report/unavailable.html"
context = {
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)
# Prepare data for map viewer
geom_form = SimpleGeomForm(
instance=comp
)
parcels = comp.get_underlying_parcels()
qrcode_url = request.build_absolute_uri(reverse("compensation:report", args=(id,)))
qrcode_img = generate_qr_code(qrcode_url, 10)
qrcode_lanis_url = comp.get_LANIS_link()
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
# Order states by surface
before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type")
after_states = comp.after_states.all().order_by("-surface").prefetch_related("biotope_type")
actions = comp.actions.all().prefetch_related("action_type")
context = {
"obj": comp,
"qrcode": {
"img": qrcode_img,
"url": qrcode_url,
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url,
},
"has_access": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
"geom_form": geom_form,
"parcels": parcels,
"actions": actions,
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)

View File

@@ -0,0 +1,26 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import Compensation
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.resubmission import AbstractResubmissionView
class CompensationResubmissionView(AbstractResubmissionView):
model = Compensation
redirect_url_base = "compensation:detail"
form_action_url_base = "compensation:resubmission-create"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -0,0 +1,50 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import Compensation
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \
AbstractRemoveCompensationStateView
class NewCompensationStateView(AbstractNewCompensationStateView):
model = Compensation
redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditCompensationStateView(AbstractEditCompensationStateView):
model = Compensation
redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveCompensationStateView(AbstractRemoveCompensationStateView):
model = Compensation
redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -1,865 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 09.08.21
"""
from django.contrib import messages
from django.db.models import Sum
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpRequest, Http404, JsonResponse
from django.shortcuts import render, get_object_or_404, redirect
from compensation.forms.forms import NewEcoAccountForm, EditEcoAccountForm
from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm, \
NewEcoAccountDocumentModalForm, RemoveCompensationActionModalForm, RemoveCompensationStateModalForm, \
EditCompensationStateModalForm, EditCompensationActionModalForm, EditDeadlineModalForm
from compensation.models import EcoAccount, EcoAccountDocument, CompensationState, CompensationAction
from compensation.tables import EcoAccountTable
from intervention.forms.modalForms import NewDeductionModalForm, ShareModalForm, RemoveEcoAccountDeductionModalForm, \
EditEcoAccountDeductionModalForm
from konova.contexts import BaseContext
from konova.decorators import any_group_check, default_group_required, conservation_office_group_required, \
shared_access_required
from konova.forms.modals import RemoveModalForm, RecordModalForm, \
RemoveDeadlineModalForm, EditDocumentModalForm, ResubmissionModalForm
from konova.forms import SimpleGeomForm
from konova.models import Deadline
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.documents import get_document, remove_document
from konova.utils.generators import generate_qr_code
from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, \
CANCEL_ACC_RECORDED_OR_DEDUCTED, DEDUCTION_REMOVED, DEDUCTION_ADDED, DOCUMENT_ADDED, COMPENSATION_STATE_REMOVED, \
COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_ADDED, DEADLINE_ADDED, DEADLINE_REMOVED, \
DEDUCTION_EDITED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED, \
RECORDED_BLOCKS_EDIT
from konova.utils.user_checks import in_group
@login_required
@any_group_check
def index_view(request: HttpRequest):
"""
Renders the index view for eco accounts
Args:
request (HttpRequest): The incoming request
Returns:
A rendered view
"""
template = "generic_index.html"
eco_accounts = EcoAccount.objects.filter(
deleted=None,
)
table = EcoAccountTable(
request=request,
queryset=eco_accounts
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@default_group_required
def new_view(request: HttpRequest):
"""
Renders a view for a new eco account creation
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "compensation/form/view.html"
data_form = NewEcoAccountForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None)
acc = data_form.save(request.user, geom_form)
if generated_identifier != acc.identifier:
messages.info(
request,
IDENTIFIER_REPLACED.format(
generated_identifier,
acc.identifier
)
)
messages.success(request, _("Eco-Account {} added").format(acc.identifier))
return redirect("compensation:acc:detail", id=acc.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New Eco-Account"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@default_group_required
def new_id_view(request: HttpRequest):
""" JSON endpoint
Provides fetching of free identifiers for e.g. AJAX calls
"""
tmp = EcoAccount()
identifier = tmp.generate_new_identifier()
while EcoAccount.objects.filter(identifier=identifier).exists():
identifier = tmp.generate_new_identifier()
return JsonResponse(
data={
"gen_data": identifier
}
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def edit_view(request: HttpRequest, id: str):
"""
Renders a view for editing compensations
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "compensation/form/view.html"
# Get object from db
acc = get_object_or_404(EcoAccount, id=id)
if acc.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("compensation:acc:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditEcoAccountForm(request.POST or None, instance=acc)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
# The data form takes the geom form for processing, as well as the performing user
acc = data_form.save(request.user, geom_form)
messages.success(request, _("Eco-Account {} edited").format(acc.identifier))
return redirect("compensation:acc:detail", id=acc.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(acc.identifier),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@any_group_check
def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
Returns:
"""
template = "compensation/detail/eco_account/view.html"
acc = get_object_or_404(
EcoAccount.objects.prefetch_related(
"deadlines",
).select_related(
'geometry',
'responsible',
),
id=id
)
geom_form = SimpleGeomForm(instance=acc)
parcels = acc.get_underlying_parcels()
_user = request.user
is_data_shared = acc.is_shared_with(_user)
# Order states according to surface
before_states = acc.before_states.order_by("-surface")
after_states = acc.after_states.order_by("-surface")
# Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = before_states.aggregate(Sum("surface"))["surface__sum"] or 0
sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0
diff_states = abs(sum_before_states - sum_after_states)
# Calculate rest of available surface for deductions
available_total, available_relative = acc.get_available_rest()
# Prefetch related data to decrease the amount of db connections
deductions = acc.deductions.filter(
intervention__deleted=None,
)
actions = acc.actions.all()
request = acc.set_status_messages(request)
context = {
"obj": acc,
"geom_form": geom_form,
"parcels": parcels,
"has_access": is_data_shared,
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"available": available_relative,
"available_total": available_total,
"is_default_member": in_group(_user, DEFAULT_GROUP),
"is_zb_member": in_group(_user, ZB_GROUP),
"is_ets_member": in_group(_user, ETS_GROUP),
"LANIS_LINK": acc.get_LANIS_link(),
"deductions": deductions,
"actions": actions,
TAB_TITLE_IDENTIFIER: f"{acc.identifier} - {acc.title}",
"has_finished_deadlines": acc.get_finished_deadlines().exists(),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def remove_view(request: HttpRequest, id: str):
""" Renders a modal view for removing the eco account
Args:
request (HttpRequest): The incoming request
id (str): The account's id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
# If the eco account has already been recorded OR there are already deductions, it can not be deleted by a regular
# default group user
if acc.recorded is not None or acc.deductions.exists():
user = request.user
if not in_group(user, ETS_GROUP):
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)
return form.process_request(
request=request,
msg_success=_("Eco-account removed"),
redirect_url=reverse("compensation:acc:index"),
)
@login_required
@default_group_required
def deduction_remove_view(request: HttpRequest, id: str, deduction_id: str):
""" Renders a modal view for removing deductions
Args:
request (HttpRequest): The incoming request
id (str): The eco account's id
deduction_id (str): The deduction's id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
try:
eco_deduction = acc.deductions.get(id=deduction_id)
if not eco_deduction.intervention.is_shared_with(request.user):
raise ObjectDoesNotExist()
except ObjectDoesNotExist:
raise Http404("Unknown deduction")
form = RemoveEcoAccountDeductionModalForm(request.POST or None, instance=acc, deduction=eco_deduction, request=request)
return form.process_request(
request=request,
msg_success=DEDUCTION_REMOVED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
def deduction_edit_view(request: HttpRequest, id: str, deduction_id: str):
""" Renders a modal view for editing deductions
Args:
request (HttpRequest): The incoming request
id (str): The eco account's id
deduction_id (str): The deduction's id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
try:
eco_deduction = acc.deductions.get(id=deduction_id)
if not eco_deduction.intervention.is_shared_with(request.user):
raise ObjectDoesNotExist
except ObjectDoesNotExist:
raise Http404("Unknown deduction")
form = EditEcoAccountDeductionModalForm(request.POST or None, instance=acc, deduction=eco_deduction, request=request)
return form.process_request(
request=request,
msg_success=DEDUCTION_EDITED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def log_view(request: HttpRequest, id: str):
""" Renders a log view using modal
Args:
request (HttpRequest): The incoming request
id (str): The eco acount's id
Returns:
"""
comp = get_object_or_404(EcoAccount, id=id)
template = "modal/modal_generic.html"
body_template = "log.html"
context = {
"modal_body_template": body_template,
"log": comp.log.all(),
"modal_title": _("Log"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@conservation_office_group_required
@shared_access_required(EcoAccount, "id")
def record_view(request: HttpRequest, id:str):
""" Renders a modal form for recording an eco account
Args:
request (HttpRequest): The incoming request
id (str): The account's id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
form = RecordModalForm(request.POST or None, instance=acc, request=request)
msg_succ = _("{} unrecorded") if acc.recorded else _("{} recorded")
msg_succ = msg_succ.format(acc.identifier)
return form.process_request(
request,
msg_succ
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def state_new_view(request: HttpRequest, id: str):
""" Renders a form for adding new states for an eco account
Args:
request (HttpRequest): The incoming request
id (str): The account's id to which the new state will be related
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
form = NewStateModalForm(request.POST or None, instance=acc, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_STATE_ADDED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def action_new_view(request: HttpRequest, id: str):
""" Renders a form for adding new actions for an eco account
Args:
request (HttpRequest): The incoming request
id (str): The account's id to which the new state will be related
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
form = NewActionModalForm(request.POST or None, instance=acc, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_ACTION_ADDED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def state_remove_view(request: HttpRequest, id: str, state_id: str):
""" Renders a form for removing a compensation state
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
state_id (str): The state's id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
state = get_object_or_404(CompensationState, id=state_id)
form = RemoveCompensationStateModalForm(request.POST or None, instance=acc, state=state, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_STATE_REMOVED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def state_edit_view(request: HttpRequest, id: str, state_id: str):
""" Renders a form for editing a compensation state
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
state_id (str): The state's id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
state = get_object_or_404(CompensationState, id=state_id)
form = EditCompensationStateModalForm(request.POST or None, instance=acc, state=state, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_STATE_EDITED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def action_remove_view(request: HttpRequest, id: str, action_id: str):
""" Renders a form for removing a compensation action
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
id (str): The action's id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
action = get_object_or_404(CompensationAction, id=action_id)
form = RemoveCompensationActionModalForm(request.POST or None, instance=acc, action=action, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_ACTION_REMOVED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def action_edit_view(request: HttpRequest, id: str, action_id: str):
""" Renders a form for editing a compensation action
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
id (str): The action's id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
action = get_object_or_404(CompensationAction, id=action_id)
form = EditCompensationActionModalForm(request.POST or None, instance=acc, action=action, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_ACTION_EDITED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def deadline_edit_view(request: HttpRequest, id: str, deadline_id: str):
""" Renders a form for editing deadlines from a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
deadline_id (str): The deadline's id
Returns:
"""
comp = get_object_or_404(EcoAccount, id=id)
deadline = get_object_or_404(Deadline, id=deadline_id)
form = EditDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request)
return form.process_request(
request,
msg_success=DEADLINE_EDITED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str):
""" Renders a form for removing deadlines from a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
deadline_id (str): The deadline's id
Returns:
"""
comp = get_object_or_404(EcoAccount, id=id)
deadline = get_object_or_404(Deadline, id=deadline_id)
form = RemoveDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request)
return form.process_request(
request,
msg_success=DEADLINE_REMOVED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def deadline_new_view(request: HttpRequest, id: str):
""" Renders a form for adding new states for an eco account
Args:
request (HttpRequest): The incoming request
id (str): The account's id to which the new state will be related
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
form = NewDeadlineModalForm(request.POST or None, instance=acc, request=request)
return form.process_request(
request,
msg_success=DEADLINE_ADDED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def new_document_view(request: HttpRequest, id: str):
""" Renders a form for uploading new documents
Args:
request (HttpRequest): The incoming request
id (str): The account's id to which the new document will be related
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
form = NewEcoAccountDocumentModalForm(request.POST or None, request.FILES or None, instance=acc, request=request)
return form.process_request(
request,
msg_success=DOCUMENT_ADDED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data",
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def get_document_view(request: HttpRequest, id:str, doc_id: str):
""" Returns the document as downloadable file
Wraps the generic document fetcher function from konova.utils.
Args:
request (HttpRequest): The incoming request
id (str): The account id
doc_id (str): The document id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
doc = get_object_or_404(EcoAccountDocument, id=doc_id)
return get_document(doc)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def edit_document_view(request: HttpRequest, id: str, doc_id: str):
""" Removes the document from the database and file system
Wraps the generic functionality from konova.utils.
Args:
request (HttpRequest): The incoming request
id (str): The account id
doc_id (str): The document id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
doc = get_object_or_404(EcoAccountDocument, id=doc_id)
form = EditDocumentModalForm(request.POST or None, request.FILES or None, instance=acc, document=doc, request=request)
return form.process_request(
request,
DOCUMENT_EDITED,
reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def remove_document_view(request: HttpRequest, id: str, doc_id: str):
""" Removes the document from the database and file system
Wraps the generic functionality from konova.utils.
Args:
request (HttpRequest): The incoming request
id (str): The account id
doc_id (str): The document id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
doc = get_object_or_404(EcoAccountDocument, id=doc_id)
return remove_document(
request,
doc
)
@login_required
@default_group_required
def new_deduction_view(request: HttpRequest, id: str):
""" Renders a modal form view for creating deductions
Args:
request (HttpRequest): THe incoming request
id (str): The eco account's id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
if not acc.recorded:
raise Http404()
form = NewDeductionModalForm(request.POST or None, instance=acc, request=request)
return form.process_request(
request,
msg_success=DEDUCTION_ADDED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
def report_view(request:HttpRequest, id: str):
""" Renders the public report view
Args:
request (HttpRequest): The incoming request
id (str): The id of the intervention
Returns:
"""
# Reuse the compensation report template since EcoAccounts are structurally identical
template = "compensation/report/eco_account/report.html"
acc = get_object_or_404(EcoAccount, id=id)
tab_title = _("Report {}").format(acc.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not acc.is_ready_for_publish():
template = "report/unavailable.html"
context = {
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)
# Prepare data for map viewer
geom_form = SimpleGeomForm(
instance=acc
)
parcels = acc.get_underlying_parcels()
qrcode_url = request.build_absolute_uri(reverse("ema:report", args=(id,)))
qrcode_img = generate_qr_code(qrcode_url, 10)
qrcode_lanis_url = acc.get_LANIS_link()
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
# Order states by surface
before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent")
after_states = acc.after_states.all().order_by("-surface").select_related("biotope_type__parent")
actions = acc.actions.all().prefetch_related("action_type__parent")
# Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier)
deductions = acc.deductions.all()\
.distinct("intervention")\
.select_related("intervention")\
.values_list("intervention__id", "intervention__identifier", "intervention__title", named=True)
context = {
"obj": acc,
"qrcode": {
"img": qrcode_img,
"url": qrcode_url,
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url,
},
"has_access": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
"geom_form": geom_form,
"parcels": parcels,
"actions": actions,
"deductions": deductions,
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
def share_view(request: HttpRequest, id: str, token: str):
""" Performs sharing of an eco account
If token given in url is not valid, the user will be redirected to the dashboard
Args:
request (HttpRequest): The incoming request
id (str): EcoAccount's id
token (str): Access token for EcoAccount
Returns:
"""
user = request.user
obj = get_object_or_404(EcoAccount, id=id)
# Check tokens
if obj.access_token == token:
# Send different messages in case user has already been added to list of sharing users
if obj.is_shared_with(user):
messages.info(
request,
_("{} has already been shared with you").format(obj.identifier)
)
else:
messages.success(
request,
_("{} has been shared with you").format(obj.identifier)
)
obj.share_with_user(user)
return redirect("compensation:acc:detail", id=id)
else:
messages.error(
request,
_("Share link invalid"),
extra_tags="danger",
)
return redirect("home")
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def create_share_view(request: HttpRequest, id: str):
""" Renders sharing form for an eco account
Args:
request (HttpRequest): The incoming request
id (str): EcoAccount's id
Returns:
"""
obj = get_object_or_404(EcoAccount, id=id)
form = ShareModalForm(request.POST or None, instance=obj, request=request)
return form.process_request(
request,
msg_success=_("Share settings updated")
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def create_resubmission_view(request: HttpRequest, id: str):
""" Renders resubmission form for an eco account
Args:
request (HttpRequest): The incoming request
id (str): EcoAccount's id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
form = ResubmissionModalForm(request.POST or None, instance=acc, request=request)
form.action_url = reverse("compensation:acc:resubmission-create", args=(id,))
return form.process_request(
request,
msg_success=_("Resubmission set"),
redirect_url=reverse("compensation:acc:detail", args=(id,))
)

View File

@@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""

View File

@@ -0,0 +1,50 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \
AbstractRemoveCompensationActionView
class NewEcoAccountActionView(AbstractNewCompensationActionView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditEcoAccountActionView(AbstractEditCompensationActionView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveEcoAccountActionView(AbstractRemoveCompensationActionView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -0,0 +1,49 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.deadline import AbstractNewDeadlineView, AbstractEditDeadlineView, AbstractRemoveDeadlineView
class NewEcoAccountDeadlineView(AbstractNewDeadlineView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditEcoAccountDeadlineView(AbstractEditDeadlineView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveEcoAccountDeadlineView(AbstractRemoveDeadlineView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -0,0 +1,58 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.http import Http404
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount
from konova.decorators import default_group_required, login_required_modal
from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView
class NewEcoAccountDeductionView(AbstractNewDeductionView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def _custom_check(self, obj):
if not obj.recorded:
raise Http404()
class EditEcoAccountDeductionView(AbstractEditDeductionView):
def _custom_check(self, obj):
pass
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveEcoAccountDeductionView(AbstractRemoveDeductionView):
def _custom_check(self, obj):
pass
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -0,0 +1,69 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.decorators import method_decorator
from compensation.forms.modals.document import NewEcoAccountDocumentModalForm
from compensation.models import EcoAccount, EcoAccountDocument
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.forms.modals import EditDocumentModalForm
from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \
AbstractEditDocumentView
class NewEcoAccountDocumentView(AbstractNewDocumentView):
model = EcoAccount
form = NewEcoAccountDocumentModalForm
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class GetEcoAccountDocumentView(AbstractGetDocumentView):
model = EcoAccount
document_model = EcoAccountDocument
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveEcoAccountDocumentView(AbstractRemoveDocumentView):
model = EcoAccount
document_model = EcoAccountDocument
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditEcoAccountDocumentView(AbstractEditDocumentView):
model = EcoAccount
document_model = EcoAccountDocument
form = EditDocumentModalForm
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -0,0 +1,270 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.db.models import Sum
from django.http import HttpRequest, JsonResponse
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, 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.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, \
IDENTIFIER_REPLACED
from konova.utils.user_checks import in_group
@login_required
@any_group_check
def index_view(request: HttpRequest):
"""
Renders the index view for eco accounts
Args:
request (HttpRequest): The incoming request
Returns:
A rendered view
"""
template = "generic_index.html"
eco_accounts = EcoAccount.objects.filter(
deleted=None,
).order_by(
"-modified__timestamp"
)
table = EcoAccountTable(
request=request,
queryset=eco_accounts
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@default_group_required
def new_view(request: HttpRequest):
"""
Renders a view for a new eco account creation
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "compensation/form/view.html"
data_form = NewEcoAccountForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None)
acc = data_form.save(request.user, geom_form)
if generated_identifier != acc.identifier:
messages.info(
request,
IDENTIFIER_REPLACED.format(
generated_identifier,
acc.identifier
)
)
messages.success(request, _("Eco-Account {} added").format(acc.identifier))
return redirect("compensation:acc:detail", id=acc.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New Eco-Account"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@default_group_required
def new_id_view(request: HttpRequest):
""" JSON endpoint
Provides fetching of free identifiers for e.g. AJAX calls
"""
tmp = EcoAccount()
identifier = tmp.generate_new_identifier()
while EcoAccount.objects.filter(identifier=identifier).exists():
identifier = tmp.generate_new_identifier()
return JsonResponse(
data={
"gen_data": identifier
}
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def edit_view(request: HttpRequest, id: str):
"""
Renders a view for editing compensations
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "compensation/form/view.html"
# Get object from db
acc = get_object_or_404(EcoAccount, id=id)
if acc.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("compensation:acc:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditEcoAccountForm(request.POST or None, instance=acc)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
# The data form takes the geom form for processing, as well as the performing user
acc = data_form.save(request.user, geom_form)
messages.success(request, _("Eco-Account {} edited").format(acc.identifier))
return redirect("compensation:acc:detail", id=acc.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(acc.identifier),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@any_group_check
def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
Returns:
"""
template = "compensation/detail/eco_account/view.html"
acc = get_object_or_404(
EcoAccount.objects.prefetch_related(
"deadlines",
).select_related(
'geometry',
'responsible',
),
id=id
)
geom_form = SimpleGeomForm(instance=acc)
parcels = acc.get_underlying_parcels()
_user = request.user
is_data_shared = acc.is_shared_with(_user)
# Order states according to surface
before_states = acc.before_states.order_by("-surface")
after_states = acc.after_states.order_by("-surface")
# Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = before_states.aggregate(Sum("surface"))["surface__sum"] or 0
sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0
diff_states = abs(sum_before_states - sum_after_states)
# Calculate rest of available surface for deductions
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(
intervention__deleted=None,
)
actions = acc.actions.all()
request = acc.set_status_messages(request)
context = {
"obj": acc,
"geom_form": geom_form,
"parcels": parcels,
"has_access": is_data_shared,
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"available": available_relative,
"available_total": available_total,
"is_default_member": in_group(_user, DEFAULT_GROUP),
"is_zb_member": in_group(_user, ZB_GROUP),
"is_ets_member": in_group(_user, ETS_GROUP),
"LANIS_LINK": acc.get_LANIS_link(),
"deductions": deductions,
"actions": actions,
TAB_TITLE_IDENTIFIER: f"{acc.identifier} - {acc.title}",
"has_finished_deadlines": acc.get_finished_deadlines().exists(),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required_modal
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def remove_view(request: HttpRequest, id: str):
""" Renders a modal view for removing the eco account
Args:
request (HttpRequest): The incoming request
id (str): The account's id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
# If the eco account has already been recorded OR there are already deductions, it can not be deleted by a regular
# default group user
if acc.recorded is not None or acc.deductions.exists():
user = request.user
if not in_group(user, ETS_GROUP):
messages.info(request, CANCEL_ACC_RECORDED_OR_DEDUCTED)
return redirect("compensation:acc:detail", id=id)
form = RemoveEcoAccountModalForm(request.POST or None, instance=acc, request=request)
return form.process_request(
request=request,
msg_success=_("Eco-account removed"),
redirect_url=reverse("compensation:acc:index"),
)

View File

@@ -0,0 +1,24 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.log import AbstractLogView
class EcoAccountLogView(AbstractLogView):
model = EcoAccount
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -0,0 +1,24 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
from konova.views.record import AbstractRecordView
class EcoAccountRecordView(AbstractRecordView):
model = EcoAccount
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -0,0 +1,86 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.http import HttpRequest
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from compensation.models import EcoAccount
from konova.contexts import BaseContext
from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.generators import generate_qr_code
def report_view(request: HttpRequest, id: str):
""" Renders the public report view
Args:
request (HttpRequest): The incoming request
id (str): The id of the intervention
Returns:
"""
# Reuse the compensation report template since EcoAccounts are structurally identical
template = "compensation/report/eco_account/report.html"
acc = get_object_or_404(EcoAccount, id=id)
tab_title = _("Report {}").format(acc.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not acc.is_ready_for_publish():
template = "report/unavailable.html"
context = {
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)
# Prepare data for map viewer
geom_form = SimpleGeomForm(
instance=acc
)
parcels = acc.get_underlying_parcels()
qrcode_url = request.build_absolute_uri(reverse("compensation:acc:report", args=(id,)))
qrcode_img = generate_qr_code(qrcode_url, 10)
qrcode_lanis_url = acc.get_LANIS_link()
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
# Order states by surface
before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent")
after_states = acc.after_states.all().order_by("-surface").select_related("biotope_type__parent")
actions = acc.actions.all().prefetch_related("action_type__parent")
# Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier)
deductions = acc.deductions.all()\
.distinct("intervention")\
.select_related("intervention")\
.values_list("intervention__id", "intervention__identifier", "intervention__title", named=True)
context = {
"obj": acc,
"qrcode": {
"img": qrcode_img,
"url": qrcode_url,
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url,
},
"has_access": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
"geom_form": geom_form,
"parcels": parcels,
"actions": actions,
"deductions": deductions,
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)

View File

@@ -0,0 +1,26 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.resubmission import AbstractResubmissionView
class EcoAccountResubmissionView(AbstractResubmissionView):
model = EcoAccount
redirect_url_base = "compensation:acc:detail"
form_action_url_base = "compensation:acc:resubmission-create"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -0,0 +1,33 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.share import AbstractShareByTokenView, AbstractShareFormView
class EcoAccountShareByTokenView(AbstractShareByTokenView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EcoAccountShareFormView(AbstractShareFormView):
model = EcoAccount
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -0,0 +1,50 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \
AbstractRemoveCompensationStateView
class NewEcoAccountStateView(AbstractNewCompensationStateView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditEcoAccountStateView(AbstractEditCompensationStateView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveEcoAccountStateView(AbstractRemoveCompensationStateView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -6,12 +6,11 @@ Created on: 09.08.21
"""
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from compensation.forms.modalForms import NewPaymentForm, RemovePaymentModalForm, EditPaymentModalForm
from compensation.forms.modals.payment import NewPaymentForm, RemovePaymentModalForm, EditPaymentModalForm
from compensation.models import Payment
from intervention.models import Intervention
from konova.decorators import default_group_required, shared_access_required

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

@@ -5,7 +5,7 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 19.08.21
"""
from compensation.filters import EcoAccountTableFilter
from compensation.filters.eco_account import EcoAccountTableFilter
class EmaTableFilter(EcoAccountTableFilter):

View File

@@ -10,8 +10,8 @@ from django.db import transaction
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from compensation.forms.forms import AbstractCompensationForm, CompensationResponsibleFormMixin, \
PikCompensationFormMixin
from compensation.forms.mixins import CompensationResponsibleFormMixin, PikCompensationFormMixin
from compensation.forms.compensation import AbstractCompensationForm
from ema.models import Ema, EmaDocument
from intervention.models import Responsibility, Handler
from konova.forms import SimpleGeomForm
@@ -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

@@ -103,7 +103,7 @@ class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, Pik
Returns:
"""
return reverse("ema:share", args=(self.id, self.access_token))
return reverse("ema:share-token", args=(self.id, self.access_token))
class EmaDocument(AbstractDocument):

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

@@ -15,7 +15,7 @@
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'ema:resubmission-create' obj.id %}">
{% fa5_icon 'bell' %}
</button>
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'ema:share-create' obj.id %}">
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'ema:share-form' obj.id %}">
{% fa5_icon 'share-alt' %}
</button>
{% if is_ets_member %}

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

@@ -49,8 +49,8 @@ class EmaViewTestCase(CompensationViewTestCase):
self.log_url = reverse("ema:log", args=(self.ema.id,))
self.edit_url = reverse("ema:edit", args=(self.ema.id,))
self.remove_url = reverse("ema:remove", args=(self.ema.id,))
self.share_url = reverse("ema:share", args=(self.ema.id, self.ema.access_token,))
self.share_create_url = reverse("ema:share-create", args=(self.ema.id,))
self.share_url = reverse("ema:share-token", args=(self.ema.id, self.ema.access_token,))
self.share_create_url = reverse("ema:share-form", args=(self.ema.id,))
self.record_url = reverse("ema:record", args=(self.ema.id,))
self.report_url = reverse("ema:report", args=(self.ema.id,))
self.new_doc_url = reverse("ema:new-doc", args=(self.ema.id,))

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

@@ -6,7 +6,17 @@ Created on: 19.08.21
"""
from django.urls import path
from ema.views import *
from ema.views.action import NewEmaActionView, EditEmaActionView, RemoveEmaActionView
from ema.views.deadline import NewEmaDeadlineView, EditEmaDeadlineView, RemoveEmaDeadlineView
from ema.views.document import NewEmaDocumentView, EditEmaDocumentView, RemoveEmaDocumentView, GetEmaDocumentView
from ema.views.ema import index_view, new_view, new_id_view, detail_view, edit_view, remove_view
from ema.views.log import EmaLogView
from ema.views.record import EmaRecordView
from ema.views.report import report_view
from ema.views.resubmission import EmaResubmissionView
from ema.views.share import EmaShareFormView, EmaShareByTokenView
from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateView
app_name = "ema"
urlpatterns = [
@@ -14,32 +24,32 @@ urlpatterns = [
path("new/", new_view, name="new"),
path("new/id", new_id_view, name="new-id"),
path("<id>", detail_view, name="detail"),
path('<id>/log', log_view, name='log'),
path('<id>/log', EmaLogView.as_view(), name='log'),
path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', remove_view, name='remove'),
path('<id>/record', record_view, name='record'),
path('<id>/record', EmaRecordView.as_view(), name='record'),
path('<id>/report', report_view, name='report'),
path('<id>/resub', create_resubmission_view, name='resubmission-create'),
path('<id>/resub', EmaResubmissionView.as_view(), name='resubmission-create'),
path('<id>/state/new', state_new_view, name='new-state'),
path('<id>/state/<state_id>/remove', state_remove_view, name='state-remove'),
path('<id>/state/<state_id>/edit', state_edit_view, name='state-edit'),
path('<id>/state/new', NewEmaStateView.as_view(), name='new-state'),
path('<id>/state/<state_id>/remove', RemoveEmaStateView.as_view(), name='state-remove'),
path('<id>/state/<state_id>/edit', EditEmaStateView.as_view(), name='state-edit'),
path('<id>/action/new', action_new_view, name='new-action'),
path('<id>/action/<action_id>/edit', action_edit_view, name='action-edit'),
path('<id>/action/<action_id>/remove', action_remove_view, name='action-remove'),
path('<id>/action/new', NewEmaActionView.as_view(), name='new-action'),
path('<id>/action/<action_id>/edit', EditEmaActionView.as_view(), name='action-edit'),
path('<id>/action/<action_id>/remove', RemoveEmaActionView.as_view(), name='action-remove'),
path('<id>/deadline/new', deadline_new_view, name="new-deadline"),
path('<id>/deadline/<deadline_id>/edit', deadline_edit_view, name='deadline-edit'),
path('<id>/deadline/<deadline_id>/remove', deadline_remove_view, name='deadline-remove'),
path('<id>/deadline/new', NewEmaDeadlineView.as_view(), name="new-deadline"),
path('<id>/deadline/<deadline_id>/edit', EditEmaDeadlineView.as_view(), name='deadline-edit'),
path('<id>/deadline/<deadline_id>/remove', RemoveEmaDeadlineView.as_view(), name='deadline-remove'),
path('<id>/share/<token>', share_view, name='share'),
path('<id>/share', create_share_view, name='share-create'),
path('<id>/share/<token>', EmaShareByTokenView.as_view(), name='share-token'),
path('<id>/share', EmaShareFormView.as_view(), name='share-form'),
# Documents
path('<id>/document/new/', document_new_view, name='new-doc'),
path('<id>/document/<doc_id>', get_document_view, name='get-doc'),
path('<id>/document/<doc_id>/edit/', edit_document_view, name='edit-doc'),
path('<id>/document/<doc_id>/remove/', remove_document_view, name='remove-doc'),
path('<id>/document/new/', NewEmaDocumentView.as_view(), name='new-doc'),
path('<id>/document/<doc_id>', GetEmaDocumentView.as_view(), name='get-doc'),
path('<id>/document/<doc_id>/edit/', EditEmaDocumentView.as_view(), name='edit-doc'),
path('<id>/document/<doc_id>/remove/', RemoveEmaDocumentView.as_view(), name='remove-doc'),
]

View File

@@ -1,737 +0,0 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.db.models import Sum
from django.http import HttpRequest, JsonResponse
from django.shortcuts import render, get_object_or_404, redirect
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm, \
RemoveCompensationActionModalForm, RemoveCompensationStateModalForm, EditCompensationStateModalForm, \
EditCompensationActionModalForm, EditDeadlineModalForm
from compensation.models import CompensationAction, CompensationState
from ema.forms import NewEmaForm, EditEmaForm, NewEmaDocumentModalForm
from ema.tables import EmaTable
from intervention.forms.modalForms import ShareModalForm
from konova.contexts import BaseContext
from konova.decorators import conservation_office_group_required, shared_access_required
from ema.models import Ema, EmaDocument
from konova.forms.modals import RemoveModalForm, RecordModalForm, RemoveDeadlineModalForm, \
EditDocumentModalForm, ResubmissionModalForm
from konova.forms import SimpleGeomForm
from konova.models import Deadline
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.documents import get_document, remove_document
from konova.utils.generators import generate_qr_code
from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \
DOCUMENT_ADDED, COMPENSATION_STATE_REMOVED, COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, \
COMPENSATION_ACTION_ADDED, DEADLINE_ADDED, DEADLINE_REMOVED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, \
COMPENSATION_ACTION_EDITED, DEADLINE_EDITED, RECORDED_BLOCKS_EDIT
from konova.utils.user_checks import in_group
@login_required
def index_view(request: HttpRequest):
""" Renders the index view for EMAs
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "generic_index.html"
emas = Ema.objects.filter(
deleted=None,
).order_by(
"-modified"
)
table = EmaTable(
request,
queryset=emas
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("EMAs - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@conservation_office_group_required
def new_view(request: HttpRequest):
"""
Renders a view for a new eco account creation
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "ema/form/view.html"
data_form = NewEmaForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None)
ema = data_form.save(request.user, geom_form)
if generated_identifier != ema.identifier:
messages.info(
request,
IDENTIFIER_REPLACED.format(
generated_identifier,
ema.identifier
)
)
messages.success(request, _("EMA {} added").format(ema.identifier))
return redirect("ema:detail", id=ema.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New EMA"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@conservation_office_group_required
def new_id_view(request: HttpRequest):
""" JSON endpoint
Provides fetching of free identifiers for e.g. AJAX calls
"""
tmp = Ema()
identifier = tmp.generate_new_identifier()
while Ema.objects.filter(identifier=identifier).exists():
identifier = tmp.generate_new_identifier()
return JsonResponse(
data={
"gen_data": identifier
}
)
@login_required
def detail_view(request: HttpRequest, id: str):
""" Renders the detail view of an EMA
Args:
request (HttpRequest): The incoming request
id (str): The EMA id
Returns:
"""
template = "ema/detail/view.html"
ema = get_object_or_404(Ema, id=id, deleted=None)
geom_form = SimpleGeomForm(instance=ema)
parcels = ema.get_underlying_parcels()
_user = request.user
is_data_shared = ema.is_shared_with(_user)
# Order states according to surface
before_states = ema.before_states.all().order_by("-surface")
after_states = ema.after_states.all().order_by("-surface")
# Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = before_states.aggregate(Sum("surface"))["surface__sum"] or 0
sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0
diff_states = abs(sum_before_states - sum_after_states)
ema.set_status_messages(request)
context = {
"obj": ema,
"geom_form": geom_form,
"parcels": parcels,
"has_access": is_data_shared,
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"is_default_member": in_group(_user, DEFAULT_GROUP),
"is_zb_member": in_group(_user, ZB_GROUP),
"is_ets_member": in_group(_user, ETS_GROUP),
"LANIS_LINK": ema.get_LANIS_link(),
TAB_TITLE_IDENTIFIER: f"{ema.identifier} - {ema.title}",
"has_finished_deadlines": ema.get_finished_deadlines().exists(),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def log_view(request: HttpRequest, id: str):
""" Renders a log view using modal
Args:
request (HttpRequest): The incoming request
id (str): The EMA's id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
template = "modal/modal_generic.html"
body_template = "log.html"
context = {
"modal_body_template": body_template,
"log": ema.log.all(),
"modal_title": _("Log"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def edit_view(request: HttpRequest, id: str):
"""
Renders a view for editing compensations
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "compensation/form/view.html"
# Get object from db
ema = get_object_or_404(Ema, id=id)
if ema.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("ema:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditEmaForm(request.POST or None, instance=ema)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=ema)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
# The data form takes the geom form for processing, as well as the performing user
ema = data_form.save(request.user, geom_form)
messages.success(request, _("EMA {} edited").format(ema.identifier))
return redirect("ema:detail", id=ema.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(ema.identifier),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def remove_view(request: HttpRequest, id: str):
""" Renders a modal view for removing the EMA
Args:
request (HttpRequest): The incoming request
id (str): The EMA's id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
form = RemoveModalForm(request.POST or None, instance=ema, request=request)
return form.process_request(
request=request,
msg_success=_("EMA removed"),
redirect_url=reverse("ema:index"),
)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def record_view(request: HttpRequest, id: str):
""" Renders a modal view for recording the EMA
Args:
request (HttpRequest): The incoming request
id (str): The EMA's id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
msg_succ = _("{} unrecorded") if ema.recorded else _("{} recorded")
form = RecordModalForm(request.POST or None, instance=ema, request=request)
return form.process_request(
request=request,
msg_success=msg_succ.format("EMA"),
)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def state_new_view(request: HttpRequest, id: str):
""" Renders a form for adding new states for an EMA
Args:
request (HttpRequest): The incoming request
id (str): The EMA's id to which the new state will be related
Returns:
"""
ema = get_object_or_404(Ema, id=id)
form = NewStateModalForm(request.POST or None, instance=ema, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_STATE_ADDED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def action_new_view(request: HttpRequest, id: str):
""" Renders a form for adding new actions for an EMA
Args:
request (HttpRequest): The incoming request
id (str): The EMA's id to which the new state will be related
Returns:
"""
ema = get_object_or_404(Ema, id=id)
form = NewActionModalForm(request.POST or None, instance=ema, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_ACTION_ADDED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def action_edit_view(request: HttpRequest, id: str, action_id: str):
""" Renders a form for editing an actions for an EMA
Args:
request (HttpRequest): The incoming request
id (str): The EMA's id
action_id (str): The action id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
action = get_object_or_404(CompensationAction, id=action_id)
form = EditCompensationActionModalForm(request.POST or None, instance=ema, action=action, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_ACTION_EDITED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def deadline_new_view(request: HttpRequest, id: str):
""" Renders a form for adding new states for an EMA
Args:
request (HttpRequest): The incoming request
id (str): The EMA's id to which the new state will be related
Returns:
"""
ema = get_object_or_404(Ema, id=id)
form = NewDeadlineModalForm(request.POST or None, instance=ema, request=request)
return form.process_request(
request,
msg_success=DEADLINE_ADDED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def document_new_view(request: HttpRequest, id: str):
""" Renders a form for uploading new documents
Args:
request (HttpRequest): The incoming request
id (str): The EMA's id to which the new document will be related
Returns:
"""
ema = get_object_or_404(Ema, id=id)
form = NewEmaDocumentModalForm(request.POST or None, request.FILES or None, instance=ema, request=request)
return form.process_request(
request,
msg_success=DOCUMENT_ADDED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def get_document_view(request: HttpRequest, id: str, doc_id: str):
""" Returns the document as downloadable file
Wraps the generic document fetcher function from konova.utils.
Args:
request (HttpRequest): The incoming request
id (str): The EMA id
doc_id (str): The document id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
doc = get_object_or_404(EmaDocument, id=doc_id)
return get_document(doc)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def edit_document_view(request: HttpRequest, id: str, doc_id: str):
""" Removes the document from the database and file system
Wraps the generic functionality from konova.utils.
Args:
request (HttpRequest): The incoming request
id (str): The EMA id
doc_id (str): The document id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
doc = get_object_or_404(EmaDocument, id=doc_id)
form = EditDocumentModalForm(request.POST or None, request.FILES or None, instance=ema, document=doc, request=request)
return form.process_request(
request,
DOCUMENT_EDITED,
reverse("ema:detail", args=(id,)) + "#related_data"
)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def remove_document_view(request: HttpRequest, id:str, doc_id: str):
""" Removes the document from the database and file system
Wraps the generic functionality from konova.utils.
Args:
request (HttpRequest): The incoming request
id (str): The EMA id
doc_id (str): The document id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
doc = get_object_or_404(EmaDocument, id=doc_id)
return remove_document(
request,
doc
)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def state_remove_view(request: HttpRequest, id: str, state_id: str):
""" Renders a form for removing an EMA state
Args:
request (HttpRequest): The incoming request
id (str): The ema id
state_id (str): The state's id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
state = get_object_or_404(CompensationState, id=state_id)
form = RemoveCompensationStateModalForm(request.POST or None, instance=ema, state=state, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_STATE_REMOVED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def state_edit_view(request: HttpRequest, id: str, state_id: str):
""" Renders a form for editing an EMA state
Args:
request (HttpRequest): The incoming request
id (str): The ema id
state_id (str): The state's id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
state = get_object_or_404(CompensationState, id=state_id)
form = EditCompensationStateModalForm(request.POST or None, instance=ema, state=state, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_STATE_EDITED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def action_remove_view(request: HttpRequest, id: str, action_id: str):
""" Renders a form for removing an EMA action
Args:
request (HttpRequest): The incoming request
id (str): The ema id
id (str): The action's id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
action = get_object_or_404(CompensationAction, id=action_id)
form = RemoveCompensationActionModalForm(request.POST or None, instance=ema, action=action, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_ACTION_REMOVED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
)
def report_view(request:HttpRequest, id: str):
""" Renders the public report view
Args:
request (HttpRequest): The incoming request
id (str): The id of the intervention
Returns:
"""
# Reuse the compensation report template since EMAs are structurally identical
template = "ema/report/report.html"
ema = get_object_or_404(Ema, id=id)
tab_title = _("Report {}").format(ema.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not ema.is_ready_for_publish():
template = "report/unavailable.html"
context = {
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)
# Prepare data for map viewer
geom_form = SimpleGeomForm(
instance=ema,
)
parcels = ema.get_underlying_parcels()
qrcode_url = request.build_absolute_uri(reverse("ema:report", args=(id,)))
qrcode_img = generate_qr_code(qrcode_url, 10)
qrcode_lanis_url = ema.get_LANIS_link()
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
# Order states by surface
before_states = ema.before_states.all().order_by("-surface").prefetch_related("biotope_type")
after_states = ema.after_states.all().order_by("-surface").prefetch_related("biotope_type")
actions = ema.actions.all().prefetch_related("action_type")
context = {
"obj": ema,
"qrcode": {
"img": qrcode_img,
"url": qrcode_url
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url
},
"has_access": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
"geom_form": geom_form,
"parcels": parcels,
"actions": actions,
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
def share_view(request: HttpRequest, id: str, token: str):
""" Performs sharing of an ema
If token given in url is not valid, the user will be redirected to the dashboard
Args:
request (HttpRequest): The incoming request
id (str): EMA's id
token (str): Access token for EMA
Returns:
"""
user = request.user
obj = get_object_or_404(Ema, id=id)
# Check tokens
if obj.access_token == token:
# Send different messages in case user has already been added to list of sharing users
if obj.is_shared_with(user):
messages.info(
request,
_("{} has already been shared with you").format(obj.identifier)
)
else:
messages.success(
request,
_("{} has been shared with you").format(obj.identifier)
)
obj.share_with_user(user)
return redirect("ema:detail", id=id)
else:
messages.error(
request,
_("Share link invalid"),
extra_tags="danger",
)
return redirect("home")
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def create_share_view(request: HttpRequest, id: str):
""" Renders sharing form for an Ema
Args:
request (HttpRequest): The incoming request
id (str): Ema's id
Returns:
"""
obj = get_object_or_404(Ema, id=id)
form = ShareModalForm(request.POST or None, instance=obj, request=request)
return form.process_request(
request,
msg_success=_("Share settings updated")
)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def deadline_edit_view(request: HttpRequest, id: str, deadline_id: str):
""" Renders a form for editing deadlines from a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
deadline_id (str): The deadline's id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
deadline = get_object_or_404(Deadline, id=deadline_id)
form = EditDeadlineModalForm(request.POST or None, instance=ema, deadline=deadline, request=request)
return form.process_request(
request,
msg_success=DEADLINE_EDITED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str):
""" Renders a form for removing deadlines from a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
deadline_id (str): The deadline's id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
deadline = get_object_or_404(Deadline, id=deadline_id)
form = RemoveDeadlineModalForm(request.POST or None, instance=ema, deadline=deadline, request=request)
return form.process_request(
request,
msg_success=DEADLINE_REMOVED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def create_resubmission_view(request: HttpRequest, id: str):
""" Renders resubmission form for an EMA
Args:
request (HttpRequest): The incoming request
id (str): EMA's id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
form = ResubmissionModalForm(request.POST or None, instance=ema, request=request)
form.action_url = reverse("ema:resubmission-create", args=(id,))
return form.process_request(
request,
msg_success=_("Resubmission set"),
redirect_url=reverse("ema:detail", args=(id,))
)

7
ema/views/__init__.py Normal file
View File

@@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""

Some files were not shown because too many files have changed in this diff Show More