Compare commits

...

161 Commits
v0.1 ... v0.5

Author SHA1 Message Date
f6dcb6c6db Merge pull request 'Konova Code fix' (#173) from konova_code_migration into master
Reviewed-on: SGD-Nord/konova#173
2022-06-02 09:37:36 +02:00
7bacbecdec Konova Code fix
* adds command sync_codelist
    * provides updating of all codes to the newest version (id)
    * must be run once on staging, can be dropped afterwards since the root for the problem has been resolved on the codelist management application
2022-05-31 16:53:13 +02:00
58ce00a5a6 Merge pull request '158_PIK' (#171) from 158_PIK into master
Reviewed-on: SGD-Nord/konova#171
2022-05-31 13:41:54 +02:00
be885306c5 #158 is_pik added
* adds model and form mixin for PIK
* integrates mixins for compensation, ema and ecoaccount
* adds migration files
* extends API
* extends API test data
* adds is_xy fields to compensation, ema and ecoaccount reports
* adds is_pik information to detail views
* adds/updates translations
2022-05-31 13:33:44 +02:00
8b67df7617 HOTFIX: Team sharing
* fixes bug where entries would show up on index views as they would be shared (are shared but using a 'deleted' Team, which still exists on the db)
2022-05-31 12:58:35 +02:00
ab9af7ae2f Merge pull request '169_Unknown_admin_on_teams' (#170) from 169_Unknown_admin_on_teams into master
Reviewed-on: SGD-Nord/konova#170
2022-05-31 09:48:33 +02:00
f085caac5d #169 Team delete-restore
* removes unused code snippets
2022-05-31 09:47:32 +02:00
7f8d900c10 #169 Team delete-restore
* adds tests for user app
2022-05-31 09:10:44 +02:00
e7031d0bc2 #169 Team delete-restore
* adds restorable delete functionality to Team model
* refactors minor code model parts by introducing DeletableObjectMixin
* only non-deleted Teams can be chosen for sharing
* deleted Teams can be restored using the proper function on the backend admin
* deleted Teams do not provide
* adds migration
2022-05-30 15:38:16 +02:00
8aa3fbd97a #169 Admin on teams
* adds admin column on team index view
* refactors Team model, so multiple members can become admins
* adds team migration for switch from fkey->m2m structure
* renames 'Group' to 'Permission' on user index view to avoid confusion between 'Groups' and Teams
* adds new autocomplete route for team-admin selection based on already selected members of the TeamForm
2022-05-30 14:35:31 +02:00
eb3b9eb5c1 Merge pull request '#163 Checked icons improvement' (#168) from 163_Checked_workflow_improvements into master
Reviewed-on: SGD-Nord/konova#168
2022-05-30 10:38:48 +02:00
1e86a1ce5e #163 Checked icons improvement
* adds a second star icon on currently unchecked but previously checked entries
   --> can be detected easier for another check run
* simplifies some related code parts
* moves some translation string into message_templates.py
* enables session timeout after 60 minutes
* improves comment card layout sizing
* adds/updates translations
2022-05-30 10:26:34 +02:00
fbab67f897 Merge pull request '#138 Bugfix' (#167) from 138_New_map_client into master
Reviewed-on: SGD-Nord/konova#167
2022-05-27 15:02:05 +02:00
59c5072619 #138 Bugfix
* fixes bug where empty geometry would have lead to exception during is_valid check on SimpleGeomForm
2022-05-27 15:01:43 +02:00
51525d79f5 Merge pull request '138_New_map_client' (#166) from 138_New_map_client into master
Reviewed-on: SGD-Nord/konova#166
2022-05-27 08:27:08 +02:00
4f482595c6 Merge branch 'master' into 138_New_map_client
# Conflicts:
#	konova/models/geometry.py
#	konova/urls.py
#	locale/de/LC_MESSAGES/django.mo
#	locale/de/LC_MESSAGES/django.po
2022-05-25 09:22:15 +02:00
57aa39a670 #138 Configuration extended
* adds more layers and subfolders to the layer tree
* changes colours for tools
2022-05-25 09:11:54 +02:00
2f4301d09f #138 Netgis map client
* updates netgis map client to most recent version
* removes trigger delay on clicking events
* adds further customization options to config.json
2022-05-23 16:02:28 +02:00
fec43e1bed Merge pull request '#164 Retranslating' (#165) from 164_Retranslate_binding_on_date into master
Reviewed-on: SGD-Nord/konova#165
2022-05-23 15:36:58 +02:00
9f32c2fdd9 #164 Retranslating
* retranslates Bestandskraftdatum
2022-05-23 15:36:28 +02:00
b310349c1a Merge pull request '160_Number_of_parcels' (#162) from 160_Number_of_parcels into master
Reviewed-on: SGD-Nord/konova#162
2022-05-19 09:12:46 +02:00
946f3af77c #138 WIP update
* implements new build for netgis map client
2022-05-12 13:22:46 +02:00
e73b7633a3 #160 Parcel calc fix
* fixes minor bug where invalid geometry (self intersecting) could not be used properly as input for WFS parcel intersection calculation
    * future enhancements regarding map client will make sure invalid geometries can not be added in the first place
2022-05-11 16:03:53 +02:00
d1dc61cbd7 #160 Parcel number to parcel table
* adds number of all underlying parcels into parcel table
* reworks minor code parts of parcel related logic
* fixes bug where under certain circumstances a parcel would have been added twice to a geometry
* removes unused parcel fetching on intervention detail view
2022-05-11 15:52:29 +02:00
3ba3785ef1 Merge pull request 'js_tree_element_improvement' (#161) from js_tree_element_improvement into master
Reviewed-on: SGD-Nord/konova#161
2022-05-11 13:17:20 +02:00
71afdd8b36 JS Tree enhancement
* extends compensation state forms to match the new logic
* adds minor changes for tests
2022-05-11 10:16:34 +02:00
a334fff54d WIP: JS Tree
* simplifies js for single-select radio tree
2022-05-11 08:41:37 +02:00
b65dae5b95 WIP: JS Tree improvements
* adds optional short_name rendering for selectable codes
* refactors autocomplete field for compensation state into custom js tree widget
* adds single select (radio) alternative to tree widget templates
2022-05-10 16:41:46 +02:00
bb399571b1 Visual enhancement for custom JS tree widget
* adds proper css behaviour for collapsed icon
* adds minor js comments
2022-05-10 15:07:21 +02:00
92211445e7 Merge pull request '#156 Parcel WFS as geojson' (#157) from 156_Parcel_WFS_as_geojson into master
Reviewed-on: SGD-Nord/konova#157
2022-04-27 12:18:47 +02:00
73c61e96f5 #156 Parcel WFS as geojson
* refactors fetching of parcels via wfs from xml to json for easier and faster processing
2022-04-27 12:12:56 +02:00
339f074681 HOTFIX: API
* hardens atom_id input to be integer or string compatible
2022-04-25 13:47:07 +02:00
989d256521 HOTFIX: EGON sending via API
* adds EGON message triggering on API payment changes
2022-04-25 13:28:51 +02:00
623c29f827 Merge pull request '#149 Send on changes' (#154) from 149_EGON_sending into master
Reviewed-on: SGD-Nord/konova#154
2022-04-25 11:17:34 +02:00
eb975cd3c5 #149 Send on changes
* changes trigger for sending data to EGON: on each new payment, edited payment or deleted payment action, the data will be sent to EGON instead only once on "recording"
2022-04-25 11:16:51 +02:00
5a8765f638 Merge pull request '151_Parcel_table_infinite_scroll' (#153) from 151_Parcel_table_infinite_scroll into master
Reviewed-on: SGD-Nord/konova#153
2022-04-21 14:37:38 +02:00
9c2bdcdacf #151 Parcel table infinite scroll
* refactors button for further loading to infinite scroll
* adds code documentation
2022-04-21 14:36:55 +02:00
48e3e84b4c #151 Dynamic parcel table
* refactors parcel table into a dynamic table, which does not show all content at once but rather supports pagination and a button which triggers loading of more content
* adds translation
2022-04-21 14:19:35 +02:00
db884baa09 #138 config.json
* adds some layers and reorganizes config.json for NETGIS client
2022-04-20 14:32:28 +02:00
253b509122 #138 WIP Validity
* adds geometry validity checks for SimpleGeomForm is_valid()
    * shows validity problems on the form if a feature is invalid
* optimizes merging of different features into one MultiPolygon
* further enhances tests
* adds as_feature_collection() method on Geometry model for converting geom MultiPolygon attribute into FeatureCollection json holding each polygon as an own feature -> makes each polygon selectable in new netgis map client
2022-04-20 13:52:52 +02:00
c60afb0391 #138 WIP Improvements
* adds geom back writing to form field in case of invalid geometry, so the invalid geometry will be shown again
* updates tests
* fixes bug where race condition of celery workers could lead to duplicates in parcels (needs migration)
2022-04-20 11:55:03 +02:00
49c14a67b6 #138 WIP NETGIS Map client
* adds functionality for address search widget
    * drops default proxy.php (replaced by own python call)
* reduces maxZoom in config.json
2022-04-20 09:23:24 +02:00
d13c3e8094 #138 WIP First draft
* adds first working draft of netgis map client
2022-04-19 17:22:06 +02:00
c6e784e6d4 Merge branch 'master' into 138_New_map_client 2022-04-19 14:08:20 +02:00
940aa38154 Merge pull request '140_Improve_check-record_reset' (#152) from 140_Improve_check-record_reset into master
Reviewed-on: SGD-Nord/konova#152
2022-04-19 14:06:42 +02:00
887a3552b4 #140 Tests
* adds workflow tests for major datatypes
2022-04-19 14:04:20 +02:00
3b36193566 #140 Enhancements
* fixes InterventionAutocomplete bug, where team-shared entries would not pop up as valid option
* fixes bug where form opening for new compensation without direct intervention link resulted in 404
* adds intervention-recorded check on deduction forms: Form is invalid if intervention is currently recorded and therefore blocked for any editing
* extends basic check_for_recorded_instance() method to let some forms pass, e.g. deduction related forms on ecoaccounts which only have a reason to be rendered IF the entry is recorded
* adds/updates translations
2022-04-19 13:37:29 +02:00
090f6faa4e #140 Block edit on recorded
* adds new modal form content template recorded_no_edit.html
* adds modal content change, such that no data can be edited on any form as long as the entry is recorded -> instead, users are informed on the form, that the recording state prohibits editing
* adds translations
2022-04-19 09:43:36 +02:00
d2ec3d9c08 Merge pull request '146_Minor_improvements' (#150) from 146_Minor_improvements into master
Reviewed-on: SGD-Nord/konova#150
2022-04-14 14:13:14 +02:00
8165540c00 #146 Record-unshare with default
* adds automatic unsharing with default-only users if entry is recorded
2022-04-14 08:37:43 +02:00
87fae51144 #146 Team leave
* adds button and functionality for leaving a team
   * if the admin leaves the team, another user will be chosen as new admin automatically
* improves Team (django) admin backend
   * better control over user adding-removing
   * only added team members are selectable as admin
2022-04-13 15:52:41 +02:00
bf1c0e2078 #146 Clickable QR codes
* refactors QR codes on report views to be clickable as well (even supported through saved pdf)
2022-04-13 14:57:05 +02:00
b85e33dc22 #146 Share with fix
* fixes bug where editable icon on overview table would not glow if user has only team based shared access
2022-04-13 14:18:32 +02:00
83d70b6d59 #146 (Parcel) table
* set default rpp for overview tables from 5 to 10
* improves loading speed of parcel table
2022-04-13 14:07:01 +02:00
60e23d15fc #146 Admins and update_all_parcels.py
* extends admin backend
    * adds found_in_codelists to KonovaCodeAdmin to see where a KonovaCode can be found in
    * improves rendering of after_states and before_states for all AbstractCompensationAdmins
    * adds geometry_id to all major datatype admin backends
    * adds st_area like calculation to geometry admin backend
* update_all_parcels
    * orders geometries by size (small to big) to process smaller geometries first and bigger later
    * adds more output to command for a better overview of what is just going on
2022-04-13 11:42:04 +02:00
fb1dce9d3c Merge pull request '#144 Report improved' (#145) from 144_Improve_report into master
Reviewed-on: SGD-Nord/konova#145
2022-04-12 10:46:06 +02:00
6ecbd74b93 #144 Report improved
* fixes bug in egon_export.py where missing payment date would result in non writing of gml
* fixes bug in egon_export.py which occured due to extension of parcel data fetching
* updates unavailable.html report content, such that users will understand why a recorded entry might not be visible, yet
2022-04-12 10:33:03 +02:00
a6551534dc Merge pull request '#142 Localized date improved' (#143) from 142_Localized_date_format into master
Reviewed-on: SGD-Nord/konova#143
2022-04-12 09:06:05 +02:00
6060f1c1bd #142 Localized date improved
* fixes bug where created timestamp has been displayed on modified attribute on detail views
* enhances localized date and datetime rendering
* reorders sub menus in user's profile hub
2022-04-12 09:05:33 +02:00
59ff1c79a8 Merge pull request '139_Improve_parcel_reference' (#141) from 139_Improve_parcel_reference into master
Reviewed-on: SGD-Nord/konova#141
2022-04-11 12:21:41 +02:00
64d0a3bd12 # 139 Doc update
* updates doc
2022-04-11 10:55:15 +02:00
a34a0b4d8a #139 Parcel filter improved
* improves frontend filtering for district, municipal, ..., so keys can be used for a lookup as well
2022-04-11 10:51:15 +02:00
1be77e8b22 # 139 Parcel reference improved
* improves frontend layout to display more details on district, municipal and parce group
* improves ordering of parcels
* refactors parcel related models
* improves parcel fetching
* extends and simplifies sanitize_db parcel related code
2022-04-11 10:23:28 +02:00
f7b074ab23 #138 WIP
* minor changes for dev purposes
2022-04-11 08:02:48 +02:00
ac4dacefe0 #138 Map client to views
* adds netgis map client to all detail and report views
* adds netgis map client to new object forms
* WIP: needs functionality server-client
2022-04-04 12:27:45 +02:00
fc31ad4ae0 Merge pull request '131_EGON_connection' (#135) from 131_EGON_connection into master
Reviewed-on: SGD-Nord/konova#135
2022-03-21 12:20:55 +01:00
7689e0b80d #131 EGON export
* finishes egon compatible (tested) data export
* moves egon export into celery process
* adds export of data in case of intervention recording
* adds _RABBITMQ_ settings for intervention/settings.py
* adds new dependency for requirements.txt
2022-03-21 12:14:55 +01:00
17c954e844 #131 EGON exporter
* enhances EGON exporter code structure
2022-03-09 08:34:26 +01:00
06ad0fdc2d #131 WIP: EGON exporter
* adds incomplete WIP implementation of an EGON exporter
2022-03-08 11:54:26 +01:00
98a1a70a69 Merge pull request '129_Handler_code' (#133) from 129_Handler_code into master
Reviewed-on: SGD-Nord/konova#133
2022-03-04 13:23:11 +01:00
22a3339157 # 129 Handler code renaming
* renames handler code list
* improves missing handler data rendering on detail view
2022-03-03 12:09:09 +01:00
c98f41c9a8 # 129 Handler code
* adds handler code list usage to forms and models
* updates tests
* extends API for handler code handling
2022-03-03 12:05:22 +01:00
f7dbf428ac Merge pull request '121_Deferred_parcel_loading' (#128) from 121_Deferred_parcel_loading into master
Reviewed-on: SGD-Nord/konova#128
2022-02-21 15:57:38 +01:00
fe409605aa #121 Deferred parcels test
* adds test for htmx-parcel fetching
2022-02-21 15:53:09 +01:00
a4f6519601 #121 Fancy spatial reference
* visual enhancement for spatial reference rendering
2022-02-21 15:38:41 +01:00
0b9587f17c #121 Deferred parcels
* improves filtering by gmrkng and krs
* implements deferred loading of parcels on spatial referenced data objects
* adds HTMX to project
* improves detail view layout (mainly interesting for smaller displays/mobile)
2022-02-21 15:18:15 +01:00
9794688f20 Merge pull request '125_Form_titles_wrong' (#127) from 125_Form_titles_wrong into master
Reviewed-on: SGD-Nord/konova#127
2022-02-21 09:25:21 +01:00
310f7d124c # 126 Report change
* removes handler from report rendering
2022-02-21 09:19:24 +01:00
cecceba8b2 #125 Edit form titles
* changes form titles for all new EditForms
2022-02-21 09:04:46 +01:00
ce7d207a6d HOTFIX
* fixes bug where new teams button would not open modal form
2022-02-18 15:28:15 +01:00
25d04006d8 Merge pull request '101_Team_based_sharing' (#122) from 101_Team_based_sharing into master
Reviewed-on: SGD-Nord/konova#122
2022-02-18 15:20:34 +01:00
fe29035158 #101 Team mails
* adds mail templates for shared data actions
* fixes bug where deleted compensations would be used for checking
2022-02-18 15:19:37 +01:00
f6c39304ab #101 Datatype team migrations
* adds migration files for ShareableObject data models
2022-02-18 14:09:45 +01:00
f05bb6ff5c #101 Team data view
* adds overview of shared teams on object detail view
* adds team data view if button is clicked
2022-02-18 14:07:44 +01:00
aa675aa046 #101 Team sharing tests
* adds tests for team sharing
* extends the API for team sharing support
* adds shared_teams property shortcut for ShareableObjectMixin
* adds full support for team-based sharing to all views and functions
* simplifies ShareModalForm
* adds/updates translations
2022-02-18 13:52:27 +01:00
6dac847d22 #101 Team sharing form
* adds team sharing field to share form
* splits sharing logic into user based and teams based
* adds TeamAdmin for admin backend
* adds validity check on Team name -> only unused names shall be valid
2022-02-18 11:02:40 +01:00
02a6b815ea Merge branch 'master' into 101_Team_based_sharing 2022-02-18 09:27:00 +01:00
262f1a6298 Missing migration
* adds a migration which has not been checked in from another branch
2022-02-18 09:26:29 +01:00
b831a63db6 WIP: #101 Team sharing form
* adds form for sharing via team
2022-02-17 15:07:25 +01:00
3e2c5b7e47 #101 Team enhancements
* visual enhancement for team index rendering
* adds validity check for admin-membership of a team
2022-02-17 13:44:32 +01:00
e8fae7a6f4 #101 Team settings
* adds first implementation for team managing
2022-02-17 13:13:32 +01:00
7a760332fa Merge pull request '#114 Unshared Account Deductions' (#120) from 114_Deduct_from_unshared_EcoAccount into master
Reviewed-on: SGD-Nord/konova#120
2022-02-16 12:35:44 +01:00
7091b3d707 #114 Unshared Account Deductions
* enables deducting from unshared eco accounts
   * account must be recorded and not deleted, so users can use it for deductions
2022-02-16 12:35:19 +01:00
a58532322e Merge pull request '118_API_update' (#119) from 118_API_update into master
Reviewed-on: SGD-Nord/konova#119
2022-02-16 11:39:16 +01:00
cf43a4351e #118 API pagination
* adds pagination and related parameters to GET apis
* updates api GET test
2022-02-16 11:38:24 +01:00
e65b7ec45c #118 API ActionTypes
* adds support for multiple action_type entries on one CompensationAction
2022-02-16 09:44:56 +01:00
a1d3fafc61 Merge pull request '110_Biotope_codelists' (#117) from 110_Biotope_codelists into master
Reviewed-on: SGD-Nord/konova#117
2022-02-16 09:10:28 +01:00
767285112d # 110 Biotope codes
* removes list 974 from update_codelist.py command
* adds migration for existing biotope states to be changed into proper list
2022-02-16 09:08:11 +01:00
b62113df8b Merge pull request 'master' (#116) from master into 110_Biotope_codelists
Reviewed-on: SGD-Nord/konova#116
2022-02-16 08:50:40 +01:00
0658a2f6f1 Merge pull request '112_Restructure_CompensationAction' (#115) from 112_Restructure_CompensationAction into master
Reviewed-on: SGD-Nord/konova#115
2022-02-16 08:32:06 +01:00
117f0eaeb6 #112 Tree filter case insensitive search
* adds case insensitive search for TreeWidget
2022-02-16 08:31:18 +01:00
46ff1d2bc5 #112 Order enhancement
* enhances ordering for action details and biotope details
2022-02-16 08:26:24 +01:00
ec69556b1c #112 Autocomplete enhancements
* enhances filtering for Autocomplete -> parent_parent will be searched for match as well
* adds rendering of parent_parent group for BiotopeAutocomplete
* enhances ordering of registration office autocomplete
2022-02-15 15:33:25 +01:00
eb22dcf9b4 # 112 Search input for TreeWidget
* adds search input field for js-filtering by input
2022-02-15 14:35:49 +01:00
30d1e4033d #112 TreeWidget JS
* adds visual support on (de-)selecting checkboxes
* adds same support on initialization of checked checkboxes e.g. on edit forms
2022-02-15 13:23:15 +01:00
b34aa44d44 #112 CompensationAction explanation
* updates the help_text for action_type on NewActionModalForm to give a better explanation
2022-02-15 11:32:20 +01:00
f3a837a8a6 #112 AbstractCompensation rendering enhancements
* minor changes to detail view rendering of EMA, Compensation and EcoAccount
2022-02-15 10:56:49 +01:00
f36f219d2e #112 CompensationAction Tree
* implements generic HTML based and Django compatible TreeView
* enhances listing of CompensationActions on DetailView
2022-02-15 10:48:01 +01:00
3df605376c WIP: #112 Restructure CompensationAction 2022-02-11 16:21:44 +01:00
23790bca8d WIP: CompensationAction using jstree 2022-02-11 14:13:42 +01:00
925d5f5070 #112 Migration enhance
* fixes bug in migration
* adds check to migration which raises error before data loss might happen
2022-02-11 08:43:21 +01:00
3814c2749a #112 WIP: Restructure CompensationAction
* changes action_type from ForeignKey into M2M
* adds migration
* changes form widget
* WIP: changes rendering on detail view of compensation
* TEST NOT CHECKED YET!
2022-02-10 14:45:00 +01:00
d0991ea832 # 110 Biotope codelists
* adds codelist 654 to settings
* adds codelist to update_codelist command
2022-02-10 14:01:59 +01:00
68b8ff07e9 Translation update
* updates translations
2022-02-10 13:31:51 +01:00
f8dedc6df1 Merge pull request '86_User_suggestions_and_feedback' (#111) from 86_User_suggestions_and_feedback into master
Reviewed-on: SGD-Nord/konova#111
2022-02-10 13:31:55 +01:00
fe29b7874e #86 Edit deadlines EcoAccount
* adds support for editing of deadlines in EcoAccount
* adds buttons and urls
2022-02-10 12:49:30 +01:00
e3c7a1a274 #86 Edit deadline Compensation
* adds support for editing of deadlines
* adds buttons and urls
* adds w-10 as base css-class for all action columns
2022-02-10 12:42:41 +01:00
3f7a6d416d #86 Edit deadlines EMA
* adds support for editing of EMA deadlines
* adds buttons and urls
2022-02-10 12:33:22 +01:00
cba174b762 #86 Edit view tests
* extends view tests
2022-02-10 11:45:55 +01:00
8d573e7390 #86 Edit actions compensation
* adds support for editing of CompensationAction for compensation
* adds buttons and urls
2022-02-10 11:31:13 +01:00
06f81d89c4 #86 Edit actions EMA
* adds support for editing of CompensationAction
* adds buttons and urls for EMA
2022-02-10 11:24:20 +01:00
a16e0af751 #86 Edit states EMA/EcoAccount
* adds support for editing of states for EMA and EcoAccount
* adds buttons and urls
2022-02-10 11:15:01 +01:00
0479f54a4d #86 Edit states compensation
* adds support for editing of states
* adds buttons and urls for compensation
2022-02-10 11:02:30 +01:00
aa616db1f0 #86 Edit document EcoAccount
* adds support for editing of documents
* adds buttons and urls for ecoaccount
2022-02-10 10:51:52 +01:00
a9bd92c57c #86 Edit document Compensation
* adds support for editing of documents
* adds buttons and urls for compensation
* simplifies getter for all documents
2022-02-10 10:44:44 +01:00
fce85690b7 #86 Edit document EMA
* adds buttons and urls for ema
2022-02-10 10:28:41 +01:00
a385420c57 #86 Edit document
* adds support for editing of documents
* adds buttons for intervention
2022-02-10 10:21:18 +01:00
9915e6a450 #86 Revocation edit
* adds support for revocation edit
    * revocation document files will be replaced on an edit
2022-02-09 16:02:28 +01:00
59f28fbf12 #86 Edit deductions
* adds support for editing deductions
* adds tests
* improves major base test logic
2022-02-09 14:49:56 +01:00
78b4dce64d #86 Edit payment
* adds button for payment editing
* adds new edit form payment editing
* adds tests for views and workflow
2022-02-09 10:29:34 +01:00
cb6a2d4d91 #86 District column simplification
* simplifies the fetching of districts for district column
2022-02-09 09:30:37 +01:00
591e35a0e2 #86 Parcel-Geometry improvement
* improves the way parcel-geometry relations are stored on the DB
    * instead of a numerical sequence we switched to UUID, so no sequence will run out at anytime (new model: ParcelIntersection)
    * instead of dropping all M2M relations between parcel and geometry on each calculation, we keep the ones that still exist, drop the ones that do not exist and add new ones (if new ones exist)
2022-02-09 09:18:35 +01:00
e3fbe60fce # 86 LANIS link fix
* simplifies creation of LANIS link by refactoring into super class
2022-02-08 17:14:23 +01:00
83531c5f77 #86 Autocomplete enhancement
* adds support for title lookup on EcoAccounts
* adds support for title lookup on Interventions
* adds support for email lookup on User
2022-02-08 17:08:03 +01:00
0afb4d34c3 #86 Parcel district column for all
* adds parcel district column for all major data objects
* adds warning about intervention-revocation on index view of compensations
* adds warning about intervention-revocation on detail view of related compensations
2022-02-08 15:25:44 +01:00
a759eb2453 #86 Revocation rendering if needed
* renders revocation warning on the index view if a revocation exists
2022-02-08 15:07:05 +01:00
fd3fe17953 #86 Parcel districts instead of revocation
* drops revocation column in favour of a parcel district column
2022-02-08 14:51:53 +01:00
58e5b47b07 #86 Email enhancement
* adds object titles to email sending
2022-02-08 14:31:11 +01:00
a56f202e7f Remove form renaming
* renames new remove modal forms to match a more coherent style
2022-02-08 13:31:40 +01:00
51a1652baa Test enhancements
* adds more view tests to intervention tests
2022-02-08 13:27:42 +01:00
6cdf355063 #86 Log detail enhancements
* restructures removing of related data into separate sub-delete forms for easier logic handling
2022-02-08 13:16:20 +01:00
13fd3e1fcb Further tests
* adds tests for intervention workflow
2022-02-08 12:07:49 +01:00
00c1bb67ca Further tests ecoaccount
* adds ecoaccount workflow tests
2022-02-08 11:58:43 +01:00
8d47c9576b Further tests
* restructures compensation/tests into subtests for ecoaccount and compensation
* adds tests for ema workflow
* improved test data setup
2022-02-08 09:27:28 +01:00
a147626174 # 86 Deadline removal log entry
* adds log entries if deadline is removed
2022-02-07 09:56:37 +01:00
34d167a3eb #86 Logs
* adds log detail support for compensation state and action
2022-02-04 16:56:08 +01:00
80b78d3e0d Merge pull request '# 108' (#109) from 108_Deleted_compensation_checked into master
Reviewed-on: SGD-Nord/konova#109
2022-02-04 16:00:20 +01:00
6525f24121 # 108
* fixes bug
2022-02-04 15:59:53 +01:00
16505c79e7 Merge pull request '86_User_suggestions_and_feedback' (#106) from 86_User_suggestions_and_feedback into master
Reviewed-on: SGD-Nord/konova#106
2022-02-04 14:47:23 +01:00
fd04c314cd # 86 Minor html/css tweaks
* improves minor things like display related breakpoints for certain html elements
* improves css for select2 for better group-result distinction
2022-02-04 13:55:50 +01:00
fb8b338950 # 86 More log details Documents
* adds more log details on adding/removing documents
* fixes bug in admin backend where restoring of non-compensation entries led to an error
* fixes bug where deleting of revocation without an attached file would lead to an error
2022-02-04 09:18:46 +01:00
6c80480d0d #86 Userlogs Compensation
* adds log details for adding/removing of compensations for intervention
* adds handy restore-deleted function for admin backend for alls BaseObject derivatives
* adds/updates translations
2022-02-03 15:29:22 +01:00
e5cd5a2312 #86 Userlogs Revocation
* reworks user logs for adding/removing revocations with more detail on log history
* enhances css to display neat shadow on select2-results
2022-02-03 12:10:23 +01:00
1eff3687e8 CSS enhancement
* improves css on select2 width, which lead to strange viewport sizing on creation of new major datasets
2022-02-03 11:58:33 +01:00
a9215511ac # 86 Proper log detail
* adds support for payment adding/deleting to intervention log
* adds support for deduction adding/deleting to intervention/ecoaccount log
* improves code snippets
* drops add_deduction() methods for ecoaccount and intervention in favor of simpler creation in NewDeductionModalForm
* adds messages
* adds/updates translations
2022-02-02 15:16:25 +01:00
f224bbb5bd # 86 HTML simplification
* simplifies rendering of detail attributes for CompensationState and CompensationAction -> takes up less space
2022-02-02 14:26:39 +01:00
874b266352 # 86 Visual improvements
* moves message rendering directly below navigation menu for a more closed look
* reworks message rendering on before_states and after_states for all compensation related datatypes
* reworks layout of action column on all related data card tables
* resizes certain attribute layouts on related data card tables
* reworks layout of details on CompensationState and CompensationAction rendering from own column into subgrouped placement of main type info
* drops align-middle placement for all related data card table contents
2022-02-02 14:18:44 +01:00
77b59db56f # 86 Comment field length
* removes comment field length limit
* adds improvements for rendering large comments
2022-02-02 12:54:45 +01:00
d5e23b420e # 86 Viewport jump EcoAccount/EMA
* adds direct jump of viewport on related-data action (create/delete)
2022-02-02 11:26:02 +01:00
299923ef45 # 86 Viewport jump Compensation
* adds direct jump of viewport on related-data action (create/delete)
* adds comment field to log.html as 'details'
2022-02-02 10:17:59 +01:00
7028672b93 # 86 Viewport jump Intervention
* adds direct jump of viewport on related-data action (create/delete)
2022-02-02 09:32:34 +01:00
258 changed files with 69499 additions and 2895 deletions

View File

@@ -3,11 +3,11 @@
{% block body %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<h3>{% trans 'Evaluation report' %} {{office.long_name}}</h3>
<h5>{% trans 'From' %} {{report.date_from.date}} {% trans 'to' %} {{report.date_to.date}}</h5>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="d-flex justify-content-end">
<div class="dropdown">
<div class="btn btn" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">

View File

@@ -6,6 +6,7 @@
"title": "Test_compensation",
"is_cef": false,
"is_coherence_keeping": false,
"is_pik": false,
"intervention": "MUST_BE_SET_IN_TEST",
"before_states": [
],

View File

@@ -5,10 +5,14 @@
"properties": {
"title": "Test_ecoaccount",
"deductable_surface": 10000.0,
"is_pik": false,
"responsible": {
"conservation_office": null,
"conservation_file_number": null,
"handler": null
"handler": {
"type": null,
"detail": "Someone"
}
},
"legal": {
"agreement_date": null

View File

@@ -4,10 +4,14 @@
],
"properties": {
"title": "Test_ema",
"is_pik": false,
"responsible": {
"conservation_office": null,
"conservation_file_number": null,
"handler": null
"handler": {
"type": null,
"detail": "Someone"
}
},
"before_states": [
],

View File

@@ -9,7 +9,10 @@
"registration_file_number": null,
"conservation_office": null,
"conservation_file_number": null,
"handler": null
"handler": {
"type": null,
"detail": "Someone"
}
},
"legal": {
"registration_date": null,

View File

@@ -109,8 +109,8 @@ class APIV1CreateTestCase(BaseAPIV1TestCase):
Returns:
"""
self.intervention.share_with(self.superuser)
self.eco_account.share_with(self.superuser)
self.intervention.share_with_user(self.superuser)
self.eco_account.share_with_user(self.superuser)
url = reverse("api:v1:deduction")
json_file_path = "api/tests/v1/create/deduction_create_post_body.json"

View File

@@ -57,7 +57,7 @@ class APIV1DeleteTestCase(BaseAPIV1TestCase):
"""
test_intervention = self.create_dummy_intervention()
test_intervention.share_with(self.superuser)
test_intervention.share_with_user(self.superuser)
url = reverse("api:v1:intervention", args=(str(test_intervention.id),))
self._test_delete_object(test_intervention, url)
@@ -68,7 +68,7 @@ class APIV1DeleteTestCase(BaseAPIV1TestCase):
"""
test_comp = self.create_dummy_compensation()
test_comp.share_with(self.superuser)
test_comp.share_with_user(self.superuser)
url = reverse("api:v1:compensation", args=(str(test_comp.id),))
self._test_delete_object(test_comp, url)
@@ -79,7 +79,7 @@ class APIV1DeleteTestCase(BaseAPIV1TestCase):
"""
test_acc = self.create_dummy_eco_account()
test_acc.share_with(self.superuser)
test_acc.share_with_user(self.superuser)
url = reverse("api:v1:ecoaccount", args=(str(test_acc.id),))
self._test_delete_object(test_acc, url)
@@ -90,7 +90,7 @@ class APIV1DeleteTestCase(BaseAPIV1TestCase):
"""
test_ema = self.create_dummy_ema()
test_ema.share_with(self.superuser)
test_ema.share_with_user(self.superuser)
url = reverse("api:v1:ema", args=(str(test_ema.id),))
self._test_delete_object(test_ema, url)
@@ -101,7 +101,7 @@ class APIV1DeleteTestCase(BaseAPIV1TestCase):
"""
test_deduction = self.create_dummy_deduction()
test_deduction.intervention.share_with(self.superuser)
test_deduction.intervention.share_with_user(self.superuser)
url = reverse("api:v1:deduction", args=(str(test_deduction.id),))
response = self._run_delete_request(url)

View File

@@ -36,7 +36,12 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
"""
response = self._run_get_request(url)
content = json.loads(response.content)
geojson = content[str(obj.id)]
self.assertIn("rpp", content)
self.assertIn("p", content)
self.assertIn("next", content)
self.assertIn("results", content)
paginated_content = content["results"]
geojson = paginated_content[str(obj.id)]
self.assertEqual(response.status_code, 200, msg=response.content)
return geojson
@@ -59,7 +64,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
Returns:
"""
self.intervention.share_with(self.superuser)
self.intervention.share_with_user(self.superuser)
url = reverse("api:v1:intervention", args=(str(self.intervention.id),))
geojson = self._test_get_object(self.intervention, url)
self._assert_geojson_format(geojson)
@@ -80,13 +85,33 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
except KeyError as e:
self.fail(e)
def test_get_shared(self):
""" Tests api GET on shared info of the intervention
Returns:
"""
self.intervention.share_with_user(self.superuser)
self.intervention.share_with_team(self.team)
url = reverse("api:v1:intervention-share", args=(str(self.intervention.id),))
response = self._run_get_request(url)
content = json.loads(response.content)
self.assertIn("users", content)
self.assertIn(self.superuser.username, content["users"])
self.assertEqual(1, len(content["users"]))
self.assertIn("teams", content)
self.assertEqual(1, len(content["teams"]))
for team in content["teams"]:
self.assertEqual(team["id"], str(self.team.id))
self.assertEqual(team["name"], self.team.name)
def test_get_compensation(self):
""" Tests api GET
Returns:
"""
self.intervention.share_with(self.superuser)
self.intervention.share_with_user(self.superuser)
self.compensation.intervention = self.intervention
self.compensation.save()
@@ -97,6 +122,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
props = geojson["properties"]
props["is_cef"]
props["is_coherence_keeping"]
props["is_pik"]
props["intervention"]
props["intervention"]["id"]
props["intervention"]["identifier"]
@@ -114,7 +140,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
Returns:
"""
self.eco_account.share_with(self.superuser)
self.eco_account.share_with_user(self.superuser)
url = reverse("api:v1:ecoaccount", args=(str(self.eco_account.id),))
geojson = self._test_get_object(self.eco_account, url)
@@ -143,7 +169,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
Returns:
"""
self.ema.share_with(self.superuser)
self.ema.share_with_user(self.superuser)
url = reverse("api:v1:ema", args=(str(self.ema.id),))
geojson = self._test_get_object(self.ema, url)
@@ -167,7 +193,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
Returns:
"""
self.deduction.intervention.share_with(self.superuser)
self.deduction.intervention.share_with_user(self.superuser)
url = reverse("api:v1:deduction", args=(str(self.deduction.id),))
_json = self._test_get_object(self.deduction, url)

View File

@@ -12,15 +12,17 @@ class BaseAPIV1TestCase(BaseTestCase):
def setUpTestData(cls):
super().setUpTestData()
cls.superuser.get_API_token()
cls.superuser.api_token.is_active = True
cls.superuser.api_token.save()
default_group = cls.groups.get(name=DEFAULT_GROUP)
cls.superuser.groups.add(default_group)
def setUp(self) -> None:
super().setUp()
self.superuser.get_API_token()
self.superuser.api_token.is_active = True
self.superuser.api_token.save()
default_group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.add(default_group)
cls.header_data = {
"HTTP_ksptoken": cls.superuser.api_token.token,
"HTTP_kspuser": cls.superuser.username,
self.header_data = {
"HTTP_ksptoken": self.superuser.api_token.token,
"HTTP_kspuser": self.superuser.username,
}

View File

@@ -46,6 +46,7 @@
"title": "TEST_compensation_CHANGED",
"is_cef": true,
"is_coherence_keeping": true,
"is_pik": true,
"intervention": "CHANGE_BEFORE_RUN!!!",
"before_states": [],
"after_states": [],

View File

@@ -45,10 +45,14 @@
"properties": {
"title": "TEST_account_CHANGED",
"deductable_surface": "100000.0",
"is_pik": true,
"responsible": {
"conservation_office": null,
"conservation_file_number": "123-TEST",
"handler": "TEST_HANDLER_CHANGED"
"handler": {
"type": null,
"detail": "TEST HANDLER CHANGED"
}
},
"legal": {
"agreement_date": "2022-01-11"

View File

@@ -47,8 +47,12 @@
"responsible": {
"conservation_office": null,
"conservation_file_number": "TEST_CHANGED",
"handler": "TEST_HANDLER_CHANGED"
"handler": {
"type": null,
"detail": "TEST_HANDLER_CHANGED"
}
},
"is_pik": true,
"before_states": [],
"after_states": [],
"actions": [],

View File

@@ -0,0 +1,8 @@
{
"users": [
"CHANGE_ME"
],
"teams": [
"CHANGE_ME"
]
}

View File

@@ -49,7 +49,10 @@
"registration_file_number": "CHANGED",
"conservation_office": null,
"conservation_file_number": "CHANGED",
"handler": null
"handler": {
"type": null,
"detail": "TEST_HANDLER_CHANGED"
}
},
"legal": {
"registration_date": "2022-02-01",

View File

@@ -52,7 +52,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
Returns:
"""
self.intervention.share_with(self.superuser)
self.intervention.share_with_user(self.superuser)
modified_on = self.intervention.modified
url = reverse("api:v1:intervention", args=(str(self.intervention.id),))
json_file_path = "api/tests/v1/update/intervention_update_put_body.json"
@@ -79,7 +79,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
"""
self.compensation.intervention = self.intervention
self.compensation.save()
self.intervention.share_with(self.superuser)
self.intervention.share_with_user(self.superuser)
modified_on = self.compensation.modified
url = reverse("api:v1:compensation", args=(str(self.compensation.id),))
@@ -97,6 +97,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
self.assertNotEqual(modified_on, self.compensation.modified)
self.assertEqual(put_props["is_cef"], self.compensation.is_cef)
self.assertEqual(put_props["is_coherence_keeping"], self.compensation.is_coherence_keeping)
self.assertEqual(put_props["is_pik"], self.compensation.is_pik)
self.assertEqual(len(put_props["actions"]), self.compensation.actions.count())
self.assertEqual(len(put_props["before_states"]), self.compensation.before_states.count())
self.assertEqual(len(put_props["after_states"]), self.compensation.after_states.count())
@@ -108,7 +109,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
Returns:
"""
self.eco_account.share_with(self.superuser)
self.eco_account.share_with_user(self.superuser)
modified_on = self.eco_account.modified
url = reverse("api:v1:ecoaccount", args=(str(self.eco_account.id),))
@@ -126,7 +127,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
self.assertEqual(put_props["deductable_surface"], str(self.eco_account.deductable_surface))
self.assertEqual(put_props["responsible"]["conservation_office"], self.eco_account.responsible.conservation_office)
self.assertEqual(put_props["responsible"]["conservation_file_number"], self.eco_account.responsible.conservation_file_number)
self.assertEqual(put_props["responsible"]["handler"], self.eco_account.responsible.handler)
self.assertEqual(put_props["responsible"]["handler"]["detail"], self.eco_account.responsible.handler.detail)
self.assertEqual(put_props["legal"]["agreement_date"], str(self.eco_account.legal.registration_date))
self.assertEqual(len(put_props["actions"]), self.eco_account.actions.count())
self.assertEqual(len(put_props["before_states"]), self.eco_account.before_states.count())
@@ -139,7 +140,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
Returns:
"""
self.ema.share_with(self.superuser)
self.ema.share_with_user(self.superuser)
modified_on = self.ema.modified
url = reverse("api:v1:ema", args=(str(self.ema.id),))
@@ -156,7 +157,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
self.assertNotEqual(modified_on, self.ema.modified)
self.assertEqual(put_props["responsible"]["conservation_office"], self.ema.responsible.conservation_office)
self.assertEqual(put_props["responsible"]["conservation_file_number"], self.ema.responsible.conservation_file_number)
self.assertEqual(put_props["responsible"]["handler"], self.ema.responsible.handler)
self.assertEqual(put_props["responsible"]["handler"]["detail"], self.ema.responsible.handler.detail)
self.assertEqual(len(put_props["actions"]), self.ema.actions.count())
self.assertEqual(len(put_props["before_states"]), self.ema.before_states.count())
self.assertEqual(len(put_props["after_states"]), self.ema.after_states.count())
@@ -168,8 +169,8 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
Returns:
"""
self.deduction.intervention.share_with(self.superuser)
self.deduction.account.share_with(self.superuser)
self.deduction.intervention.share_with_user(self.superuser)
self.deduction.account.share_with_user(self.superuser)
url = reverse("api:v1:deduction", args=(str(self.deduction.id),))
json_file_path = "api/tests/v1/update/deduction_update_put_body.json"
@@ -184,3 +185,24 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
self.assertEqual(put_body["intervention"], str(self.deduction.intervention.id))
self.assertEqual(put_body["eco_account"], str(self.deduction.account.id))
self.assertEqual(put_body["surface"], self.deduction.surface)
def test_update_share_intervention(self):
self.intervention.share_with_user(self.superuser)
url = reverse("api:v1:intervention-share", args=(str(self.intervention.id),))
json_file_path = "api/tests/v1/update/intervention_share_update_put_body.json"
with open(json_file_path) as json_file:
put_body = json.load(fp=json_file)
put_body["users"] = [self.user.username]
put_body["teams"] = [self.team.name]
self.assertFalse(self.intervention.is_shared_with(self.user))
self.assertEqual(0, self.intervention.shared_teams.count())
response = self._run_update_request(url, put_body)
self.assertEqual(response.status_code, 200, msg=response.content)
self.intervention.refresh_from_db()
self.assertEqual(1, self.intervention.shared_teams.count())
self.assertEqual(2, self.intervention.shared_users.count())
self.assertEqual(self.team.name, self.intervention.shared_teams.first().name)
self.assertTrue(self.intervention.is_shared_with(self.user))

View File

@@ -10,6 +10,7 @@ from abc import abstractmethod
from django.contrib.gis import geos
from django.contrib.gis.geos import GEOSGeometry
from django.core.paginator import Paginator
from konova.utils.message_templates import DATA_UNSHARED
@@ -19,6 +20,10 @@ class AbstractModelAPISerializer:
lookup = None
properties_data = None
rpp = None
page_number = None
paginator = None
class Meta:
abstract = True
@@ -80,9 +85,12 @@ class AbstractModelAPISerializer:
Returns:
serialized_data (dict)
"""
entries = self.model.objects.filter(**self.lookup)
entries = self.model.objects.filter(**self.lookup).order_by("id")
self.paginator = Paginator(entries, self.rpp)
requested_entries = self.paginator.page(self.page_number)
serialized_data = {}
for entry in entries:
for entry in requested_entries.object_list:
serialized_data[str(entry.id)] = self._model_to_geo_json(entry)
return serialized_data

View File

@@ -34,6 +34,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
def _extend_properties_data(self, entry):
self.properties_data["is_cef"] = entry.is_cef
self.properties_data["is_coherence_keeping"] = entry.is_coherence_keeping
self.properties_data["is_pik"] = entry.is_pik
self.properties_data["intervention"] = self.intervention_to_json(entry.intervention)
self.properties_data["before_states"] = self._compensation_state_to_json(entry.before_states.all())
self.properties_data["after_states"] = self._compensation_state_to_json(entry.after_states.all())
@@ -113,6 +114,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
obj.title = properties["title"]
obj.is_cef = properties["is_cef"]
obj.is_coherence_keeping = properties["is_coherence_keeping"]
obj.is_pik = properties.get("is_pik", False)
obj = self.set_intervention(obj, properties["intervention"], user)
obj.geometry.save()
@@ -149,6 +151,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
obj.title = properties["title"]
obj.is_cef = properties["is_cef"]
obj.is_coherence_keeping = properties["is_coherence_keeping"]
obj.is_pik = properties.get("is_pik", False)
obj.modified = update_action
obj.geometry.geom = self._create_geometry_from_json(json_model)
obj.geometry.modified = update_action

View File

@@ -9,9 +9,9 @@ from django.db import transaction
from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin, \
LegalAPISerializerV1Mixin, ResponsibilityAPISerializerV1Mixin, DeductableAPISerializerV1Mixin
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID
from compensation.models import EcoAccount
from intervention.models import Legal, Responsibility
from intervention.models import Legal, Responsibility, Handler
from konova.models import Geometry
from konova.tasks import celery_update_parcels
from user.models import UserActionLogEntry
@@ -25,6 +25,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
model = EcoAccount
def _extend_properties_data(self, entry):
self.properties_data["is_pik"] = entry.is_pik
self.properties_data["deductable_surface"] = entry.deductable_surface
self.properties_data["deductable_surface_available"] = entry.deductable_surface - entry.get_deductions_surface()
self.properties_data["responsible"] = self._responsible_to_json(entry.responsible)
@@ -44,7 +45,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
return {
"conservation_office": self._konova_code_to_json(responsible.conservation_office),
"conservation_file_number": responsible.conservation_file_number,
"handler": responsible.handler,
"handler": self._handler_to_json(responsible.handler),
}
def _set_responsibility(self, obj, responsibility_data: dict):
@@ -64,7 +65,11 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
CODELIST_CONSERVATION_OFFICE_ID,
)
obj.responsible.conservation_file_number = responsibility_data["conservation_file_number"]
obj.responsible.handler = responsibility_data["handler"]
obj.responsible.handler.type = self._konova_code_from_json(
responsibility_data["handler"]["type"],
CODELIST_HANDLER_ID,
)
obj.responsible.handler.detail = responsibility_data["handler"]["detail"]
return obj
def _set_legal(self, obj, legal_data):
@@ -92,7 +97,9 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
# Create linked objects
obj = EcoAccount()
obj.responsible = Responsibility()
obj.responsible = Responsibility(
handler=Handler()
)
obj.legal = Legal()
created = create_action
obj.created = created
@@ -116,6 +123,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
properties = json_model["properties"]
obj.identifier = obj.generate_new_identifier()
obj.title = properties["title"]
obj.is_pik = properties.get("is_pik", False)
try:
obj.deductable_surface = float(properties["deductable_surface"])
@@ -128,6 +136,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
obj = self._set_legal(obj, properties["legal"])
obj.geometry.save()
obj.responsible.handler.save()
obj.responsible.save()
obj.legal.save()
obj.save()
@@ -162,6 +171,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
# Fill in data to objects
properties = json_model["properties"]
obj.title = properties["title"]
obj.is_pik = properties.get("is_pik", False)
obj.deductable_surface = float(properties["deductable_surface"])
obj.modified = update_action
obj.geometry.geom = self._create_geometry_from_json(json_model)
@@ -170,6 +180,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
obj = self._set_legal(obj, properties["legal"])
obj.geometry.save()
obj.responsible.handler.save()
obj.responsible.save()
obj.legal.save()
obj.save()

View File

@@ -9,9 +9,9 @@ from django.db import transaction
from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin, \
ResponsibilityAPISerializerV1Mixin
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID
from ema.models import Ema
from intervention.models import Responsibility
from intervention.models import Responsibility, Handler
from konova.models import Geometry
from konova.tasks import celery_update_parcels
from user.models import UserActionLogEntry
@@ -21,6 +21,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
model = Ema
def _extend_properties_data(self, entry):
self.properties_data["is_pik"] = entry.is_pik
self.properties_data["responsible"] = self._responsible_to_json(entry.responsible)
self.properties_data["before_states"] = self._compensation_state_to_json(entry.before_states.all())
self.properties_data["after_states"] = self._compensation_state_to_json(entry.after_states.all())
@@ -31,7 +32,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
return {
"conservation_office": self._konova_code_to_json(responsible.conservation_office),
"conservation_file_number": responsible.conservation_file_number,
"handler": responsible.handler,
"handler": self._handler_to_json(responsible.handler),
}
def _set_responsibility(self, obj, responsibility_data: dict):
@@ -51,7 +52,11 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
CODELIST_CONSERVATION_OFFICE_ID,
)
obj.responsible.conservation_file_number = responsibility_data["conservation_file_number"]
obj.responsible.handler = responsibility_data["handler"]
obj.responsible.handler.type = self._konova_code_from_json(
responsibility_data["handler"]["type"],
CODELIST_HANDLER_ID,
)
obj.responsible.handler.detail = responsibility_data["handler"]["detail"]
return obj
def _initialize_objects(self, json_model, user):
@@ -75,7 +80,9 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
# Create linked objects
obj = Ema()
obj.responsible = Responsibility()
obj.responsible = Responsibility(
handler=Handler()
)
created = create_action
obj.created = created
obj.geometry = geometry
@@ -98,9 +105,11 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
properties = json_model["properties"]
obj.identifier = obj.generate_new_identifier()
obj.title = properties["title"]
obj.is_pik = properties.get("is_pik", False)
obj = self._set_responsibility(obj, properties["responsible"])
obj.geometry.save()
obj.responsible.handler.save()
obj.responsible.save()
obj.save()
@@ -134,12 +143,14 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
# Fill in data to objects
properties = json_model["properties"]
obj.title = properties["title"]
obj.is_pik = properties.get("is_pik", False)
obj.modified = update_action
obj.geometry.geom = self._create_geometry_from_json(json_model)
obj.geometry.modified = update_action
obj = self._set_responsibility(obj, properties["responsible"])
obj.geometry.save()
obj.responsible.handler.save()
obj.responsible.save()
obj.save()

View File

@@ -11,7 +11,7 @@ from django.db.models import QuerySet
from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, \
ResponsibilityAPISerializerV1Mixin, LegalAPISerializerV1Mixin, DeductableAPISerializerV1Mixin
from compensation.models import Payment
from intervention.models import Intervention, Responsibility, Legal
from intervention.models import Intervention, Responsibility, Legal, Handler
from konova.models import Geometry
from konova.tasks import celery_update_parcels
from user.models import UserActionLogEntry
@@ -69,7 +69,9 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
# Create linked objects
obj = Intervention()
resp = Responsibility()
resp = Responsibility(
handler=Handler()
)
legal = Legal()
created = create_action
obj.legal = legal
@@ -130,6 +132,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
id__in=payments
)
obj.payments.set(payments)
obj.send_data_to_egon()
return obj
def create_model_from_json(self, json_model, user):
@@ -152,6 +155,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
self._set_responsibility(obj, properties["responsible"])
self._set_legal(obj, properties["legal"])
obj.responsible.handler.save()
obj.responsible.save()
obj.geometry.save()
obj.legal.save()
@@ -188,12 +192,13 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
obj.geometry.geom = self._create_geometry_from_json(json_model)
obj.geometry.modified = update_action
obj.responsible.handler.save()
obj.responsible.save()
obj.geometry.save()
obj.legal.save()
obj.save()
obj.mark_as_edited(user)
obj.mark_as_edited(user, edit_comment="API update")
celery_update_parcels.delay(obj.geometry.id)

View File

@@ -15,9 +15,9 @@ from api.utils.serializer.serializer import AbstractModelAPISerializer
from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_PROCESS_TYPE_ID, \
CODELIST_LAW_ID, CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, \
CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, CODELIST_HANDLER_ID
from compensation.models import CompensationAction, UnitChoices, CompensationState
from intervention.models import Responsibility, Legal
from intervention.models import Responsibility, Legal, Handler
from konova.models import Deadline, DeadlineType
from konova.utils.message_templates import DATA_UNSHARED
@@ -75,7 +75,10 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer):
Returns:
"""
if json_str is None or len(json_str) == 0:
if json_str is None:
return None
json_str = str(json_str)
if len(json_str) == 0:
return None
code = KonovaCode.objects.get(
atom_id=json_str,
@@ -176,6 +179,12 @@ class ResponsibilityAPISerializerV1Mixin:
class Meta:
abstract = True
def _handler_to_json(self, handler: Handler):
return {
"type": self._konova_code_to_json(handler.type),
"detail": handler.detail
}
def _responsible_to_json(self, responsible: Responsibility):
""" Serializes Responsibility model into json
@@ -190,7 +199,7 @@ class ResponsibilityAPISerializerV1Mixin:
"registration_file_number": responsible.registration_file_number,
"conservation_office": self._konova_code_to_json(responsible.conservation_office),
"conservation_file_number": responsible.conservation_file_number,
"handler": responsible.handler,
"handler": self._handler_to_json(responsible.handler),
}
def _set_responsibility(self, obj, responsibility_data: dict):
@@ -215,7 +224,11 @@ class ResponsibilityAPISerializerV1Mixin:
CODELIST_CONSERVATION_OFFICE_ID,
)
obj.responsible.conservation_file_number = responsibility_data["conservation_file_number"]
obj.responsible.handler = responsibility_data["handler"]
obj.responsible.handler.type = self._konova_code_from_json(
responsibility_data["handler"]["type"],
CODELIST_HANDLER_ID,
)
obj.responsible.handler.detail = responsibility_data["handler"]["detail"]
return obj
@@ -367,7 +380,9 @@ class AbstractCompensationAPISerializerV1Mixin:
"""
actions = []
for entry in actions_data:
action = entry["action"]
action_types = [
self._konova_code_from_json(e, CODELIST_COMPENSATION_ACTION_ID) for e in entry["action_types"]
]
action_details = [
self._konova_code_from_json(e, CODELIST_COMPENSATION_ACTION_DETAIL_ID) for e in entry["action_details"]
]
@@ -384,7 +399,7 @@ class AbstractCompensationAPISerializerV1Mixin:
# If this exact data is already existing, we do not create it new. Instead put it's id in the list of
# entries, we will use to set the new actions
action_entry = obj.actions.filter(
action_type__atom_id=action,
action_type__in=action_types,
amount=amount,
unit=unit,
comment=comment,
@@ -396,13 +411,13 @@ class AbstractCompensationAPISerializerV1Mixin:
else:
# Create and add id to list
action_entry = CompensationAction.objects.create(
action_type=self._konova_code_from_json(action, CODELIST_COMPENSATION_ACTION_ID),
amount=amount,
unit=unit,
comment=comment,
)
actions.append(action_entry.id)
action_entry.action_type.set(action_types)
action_entry.action_type_details.set(action_details)
obj.actions.set(actions)
return obj
@@ -438,7 +453,9 @@ class AbstractCompensationAPISerializerV1Mixin:
"""
return [
{
"action": self._konova_code_to_json(entry.action_type),
"action_types": [
self._konova_code_to_json(action) for action in entry.action_type.all()
],
"action_details": [
self._konova_code_to_json(detail) for detail in entry.action_type_details.all()
],

View File

@@ -21,7 +21,6 @@ class AbstractAPIViewV1(AbstractAPIView):
""" Holds general serialization functions for API v1
"""
serializer = None
def __init__(self, *args, **kwargs):
self.lookup = {
@@ -45,11 +44,17 @@ class AbstractAPIViewV1(AbstractAPIView):
response (JsonResponse)
"""
try:
self.rpp = int(request.GET.get("rpp", self.rpp))
self.page_number = int(request.GET.get("p", self.page_number))
self.serializer.rpp = self.rpp
self.serializer.page_number = self.page_number
self.serializer.prepare_lookup(id, self.user)
data = self.serializer.fetch_and_serialize()
except Exception as e:
return self.return_error_response(e, 500)
return JsonResponse(data)
return self._return_error_response(e, 500)
return self._return_response(request, data)
def post(self, request: HttpRequest):
""" Handles the POST request
@@ -67,7 +72,7 @@ class AbstractAPIViewV1(AbstractAPIView):
body = json.loads(body)
created_id = self.serializer.create_model_from_json(body, self.user)
except Exception as e:
return self.return_error_response(e, 500)
return self._return_error_response(e, 500)
return JsonResponse({"id": created_id})
def put(self, request: HttpRequest, id=None):
@@ -87,7 +92,7 @@ class AbstractAPIViewV1(AbstractAPIView):
body = json.loads(body)
updated_id = self.serializer.update_model_from_json(id, body, self.user)
except Exception as e:
return self.return_error_response(e, 500)
return self._return_error_response(e, 500)
return JsonResponse({"id": updated_id})
def delete(self, request: HttpRequest, id=None):
@@ -104,7 +109,7 @@ class AbstractAPIViewV1(AbstractAPIView):
try:
success = self.serializer.delete_entry(id, self.user)
except Exception as e:
return self.return_error_response(e, 500)
return self._return_error_response(e, 500)
return JsonResponse(
{
"success": success,

View File

@@ -19,7 +19,7 @@ from ema.models import Ema
from intervention.models import Intervention
from konova.utils.message_templates import DATA_UNSHARED
from konova.utils.user_checks import is_default_group_only
from user.models import User
from user.models import User, Team
class AbstractAPIView(View):
@@ -31,10 +31,22 @@ class AbstractAPIView(View):
"""
user = None
serializer = None
rpp = 5 # Results per page default
page_number = 1 # Page number default
class Meta:
abstract = True
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.response_body_base = {
"rpp": None,
"p": None,
"next": None,
"results": None
}
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
try:
@@ -42,13 +54,14 @@ class AbstractAPIView(View):
ksp_token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)
ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None)
self.user = APIUserToken.get_user_from_token(ksp_token, ksp_user)
request.user = self.user
if not self.user.is_default_user():
raise PermissionError("Default permissions required")
except PermissionError as e:
return self.return_error_response(e, 403)
return self._return_error_response(e, 403)
return super().dispatch(request, *args, **kwargs)
def return_error_response(self, error, status_code=500):
def _return_error_response(self, error, status_code=500):
""" Returns an error as JsonReponse
Args:
@@ -68,6 +81,31 @@ class AbstractAPIView(View):
status=status_code
)
def _return_response(self, request: HttpRequest, data):
""" Returns all important data into a response object
Args:
request (HttpRequest): The incoming request
data (dict): The serialized data
Returns:
response (JsonResponse): The response to be returned
"""
response = self.response_body_base
next_page = self.page_number + 1
next_page = next_page if next_page in self.serializer.paginator.page_range else None
if next_page is not None:
next_url = request.build_absolute_uri(
request.path + f"?rpp={self.rpp}&p={next_page}"
)
else:
next_url = None
response["rpp"] = self.rpp
response["p"] = self.page_number
response["next"] = next_url
response["results"] = data
return JsonResponse(response)
class InterventionCheckAPIView(AbstractAPIView):
@@ -82,14 +120,14 @@ class InterventionCheckAPIView(AbstractAPIView):
response (JsonResponse)
"""
if not self.user.is_zb_user():
return self.return_error_response("Permission not granted", 403)
return self._return_error_response("Permission not granted", 403)
try:
obj = Intervention.objects.get(
id=id,
users__in=[self.user]
)
except Exception as e:
return self.return_error_response(e)
return self._return_error_response(e)
all_valid, check_details = self.run_quality_checks(obj)
@@ -160,13 +198,21 @@ class AbstractModelShareAPIView(AbstractAPIView):
"""
try:
users = self._get_shared_users_of_object(id)
teams = self._get_shared_teams_of_object(id)
except Exception as e:
return self.return_error_response(e)
return self._return_error_response(e)
data = {
"users": [
user.username for user in users
]
],
"teams": [
{
"id": team.id,
"name": team.name,
}
for team in teams
],
}
return JsonResponse(data)
@@ -185,7 +231,7 @@ class AbstractModelShareAPIView(AbstractAPIView):
try:
success = self._process_put_body(request.body, id)
except Exception as e:
return self.return_error_response(e)
return self._return_error_response(e)
data = {
"success": success,
}
@@ -220,6 +266,22 @@ class AbstractModelShareAPIView(AbstractAPIView):
users = obj.shared_users
return users
def _get_shared_teams_of_object(self, id) -> QuerySet:
""" Check permissions and get the teams
Args:
id (str): The object's id
Returns:
users (QuerySet)
"""
obj = self.model.objects.get(
id=id
)
self._check_user_has_shared_access(obj)
teams = obj.shared_teams
return teams
def _process_put_body(self, body: bytes, id: str):
""" Reads the body data, performs validity checks and sets the new users
@@ -233,19 +295,26 @@ class AbstractModelShareAPIView(AbstractAPIView):
obj = self.model.objects.get(id=id)
self._check_user_has_shared_access(obj)
new_users = json.loads(body.decode("utf-8"))
new_users = new_users.get("users", [])
content = json.loads(body.decode("utf-8"))
new_users = content.get("users", [])
if len(new_users) == 0:
raise ValueError("Shared user list must not be empty!")
new_teams = content.get("teams", [])
# Eliminate duplicates
new_users = list(dict.fromkeys(new_users))
new_teams = list(dict.fromkeys(new_teams))
# Make sure each of these names exist as a user
new_users_objs = []
for user in new_users:
new_users_objs.append(User.objects.get(username=user))
# Make sure each of these names exist as a user
new_teams_objs = []
for team_name in new_teams:
new_teams_objs.append(Team.objects.get(name=team_name))
if is_default_group_only(self.user):
# Default only users are not allowed to remove other users from having access. They can only add new ones!
new_users_to_be_added = User.objects.filter(
@@ -254,7 +323,16 @@ class AbstractModelShareAPIView(AbstractAPIView):
id__in=obj.shared_users
)
new_users_objs = obj.shared_users.union(new_users_to_be_added)
obj.share_with_list(new_users_objs)
new_teams_to_be_added = Team.objects.filter(
name__in=new_teams
).exclude(
id__in=obj.shared_teams
)
new_teams_objs = obj.shared_teams.union(new_teams_to_be_added)
obj.share_with_user_list(new_users_objs)
obj.share_with_team_list(new_teams_objs)
return True

View File

@@ -33,6 +33,7 @@ class KonovaCodeAdmin(admin.ModelAdmin):
"is_selectable",
"is_leaf",
"parent",
"found_in_codelists",
]
search_fields = [
@@ -42,6 +43,12 @@ class KonovaCodeAdmin(admin.ModelAdmin):
"short_name",
]
def found_in_codelists(self, obj):
codelists = KonovaCodeList.objects.filter(
codes__in=[obj]
).values_list("id", flat=True)
codelists = "\n".join(str(x) for x in codelists)
return codelists
#admin.site.register(KonovaCodeList, KonovaCodeListAdmin)
admin.site.register(KonovaCode, KonovaCodeAdmin)

View File

@@ -0,0 +1,165 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 31.05.22
"""
from django.db import transaction
from codelist.models import KonovaCode
from compensation.models import CompensationAction, CompensationState
from intervention.models import Legal, Handler, Responsibility
from konova.management.commands.setup import BaseKonovaCommand
class Command(BaseKonovaCommand):
help = "Updates internal codelist by external API"
def handle(self, *args, **options):
try:
with transaction.atomic():
self.sync_codelist()
except KeyboardInterrupt:
self._break_line()
exit(-1)
def __get_newest_code(self, code):
code = KonovaCode.objects.filter(
atom_id=code.atom_id,
parent=code.parent,
code_lists__in=code.code_lists.all(),
).order_by(
"-id"
).first()
return code
def __migrate_compensation_action_codes(self):
all_actions = CompensationAction.objects.all()
used_codes = []
for action in all_actions:
stored_codes = action.action_type.all()
codes = []
for code in stored_codes:
codes.append(self.__get_newest_code(code))
action.action_type.set(codes)
used_codes += codes
stored_codes = action.action_type_details.all()
codes = []
for code in stored_codes:
codes.append(self.__get_newest_code(code))
action.action_type_details.set(codes)
used_codes += codes
action.save()
return used_codes
def __migrate_compensation_state_codes(self):
all_states = CompensationState.objects.all()
used_codes = []
for state in all_states:
code = state.biotope_type
if code is not None:
new_code = self.__get_newest_code(code)
state.biotope_type = new_code
used_codes.append(new_code)
stored_codes = state.biotope_type_details.all()
codes = []
for code in stored_codes:
codes.append(self.__get_newest_code(code))
state.biotope_type_details.set(codes)
used_codes += codes
state.save()
return used_codes
def __migrate_legal_codes(self):
all_legal = Legal.objects.all()
used_codes = []
for legal in all_legal:
code = legal.process_type
if code is not None:
new_code = self.__get_newest_code(code)
legal.process_type = new_code
used_codes.append(new_code)
stored_codes = legal.laws.all()
codes = []
for code in stored_codes:
codes.append(self.__get_newest_code(code))
legal.laws.set(codes)
used_codes += codes
legal.save()
return used_codes
def __migrate_handler_codes(apps):
all_handlers = Handler.objects.all()
used_codes = []
for handler in all_handlers:
code = handler.type
if code is None:
continue
new_code = apps.__get_newest_code(code)
handler.type = new_code
used_codes.append(new_code)
handler.save()
return used_codes
def __migrate_responsibility_codes(apps):
all_resps = Responsibility.objects.all()
used_codes = []
for responsibility in all_resps:
code = responsibility.registration_office
if code is not None:
new_code = apps.__get_newest_code(code)
responsibility.registration_office = new_code
used_codes.append(new_code)
code = responsibility.conservation_office
if code is not None:
new_code = apps.__get_newest_code(code)
responsibility.conservation_office = new_code
used_codes.append(new_code)
responsibility.save()
return used_codes
def sync_codelist(self):
""" Due to issues on the external codelist app there can be multiple entries of the same code
(atom_id, parent, list) but with different identifiers.
These issues have been resolved but already
Returns:
"""
self._write_warning("Sync codes in usage and replace by newest entries...")
used_codes = []
used_codes += self.__migrate_compensation_action_codes()
used_codes += self.__migrate_compensation_state_codes()
used_codes += self.__migrate_legal_codes()
used_codes += self.__migrate_handler_codes()
used_codes += self.__migrate_responsibility_codes()
self._write_success(f"Synced {len(used_codes)} code usages!")
all_codes = KonovaCode.objects.all()
newest_code_ids = []
for code in all_codes:
newest_code = self.__get_newest_code(code)
newest_code_ids.append(newest_code.id)
code_ids_to_keep = set(newest_code_ids)
self._write_warning(f"Of {all_codes.count()} KonovaCodes there are {len(code_ids_to_keep)} to keep as newest versions...")
deletable_codes = KonovaCode.objects.all().exclude(
id__in=code_ids_to_keep
)
deletable_codes_count = deletable_codes.count()
self._write_warning(f"{deletable_codes_count} found which are obsolet...")
if deletable_codes_count > 0:
deletable_codes.delete()
self._write_success("Obsolete codes deleted!")

View File

@@ -6,12 +6,11 @@ Created on: 23.08.21
"""
import requests
from django.core.management import BaseCommand
from xml.etree import ElementTree as etree
from codelist.models import KonovaCode, KonovaCodeList
from codelist.settings import CODELIST_INTERVENTION_HANDLER_ID, CODELIST_CONSERVATION_OFFICE_ID, \
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_BIOTOPES_ID, CODELIST_LAW_ID, CODELIST_COMPENSATION_HANDLER_ID, \
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_BIOTOPES_ID, CODELIST_LAW_ID, CODELIST_HANDLER_ID, \
CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_CLASS_ID, CODELIST_COMPENSATION_ADDITIONAL_TYPE_ID, \
CODELIST_BASE_URL, CODELIST_PROCESS_TYPE_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, \
CODELIST_COMPENSATION_ACTION_DETAIL_ID
@@ -36,7 +35,7 @@ class Command(BaseKonovaCommand):
CODELIST_BIOTOPES_ID,
CODELIST_BIOTOPES_EXTRA_CODES_ID,
CODELIST_LAW_ID,
CODELIST_COMPENSATION_HANDLER_ID,
CODELIST_HANDLER_ID,
CODELIST_COMPENSATION_ACTION_ID,
CODELIST_COMPENSATION_ACTION_CLASS_ID,
CODELIST_COMPENSATION_ACTION_DETAIL_ID,

View File

@@ -1,9 +1,13 @@
# Generated by Django 3.1.3 on 2022-01-14 08:36
from django.core.management import call_command
from django.db import migrations, models
import django.db.models.deletion
def load_initial_codes(apps, schema_editor):
call_command('update_codelist')
class Migration(migrations.Migration):
initial = True
@@ -32,4 +36,5 @@ class Migration(migrations.Migration):
('codes', models.ManyToManyField(blank=True, help_text='Codes for this list', related_name='code_lists', to='codelist.KonovaCode')),
],
),
migrations.RunPython(load_initial_codes),
]

View File

@@ -65,6 +65,25 @@ class KonovaCode(models.Model):
ret_val += ", " + self.parent.long_name
return ret_val
def add_children(self, order_by: str = "long_name"):
""" Adds all children (resurcively until leaf) as .children to the KonovaCode
Returns:
code (KonovaCode): The manipulated KonovaCode instance
"""
if self.is_leaf:
return self
children = KonovaCode.objects.filter(
parent=self
).order_by(
order_by
)
self.children = children
for child in children:
child.add_children(order_by)
return self
class KonovaCodeList(models.Model):
"""

View File

@@ -13,12 +13,13 @@ CODELIST_BASE_URL = "https://codelisten.naturschutz.rlp.de/repository/referenzli
CODELIST_INTERVENTION_HANDLER_ID = 903 # CLMassnahmeträger
CODELIST_CONSERVATION_OFFICE_ID = 907 # CLNaturschutzbehörden
CODELIST_REGISTRATION_OFFICE_ID = 1053 # CLZulassungsbehörden
CODELIST_BIOTOPES_ID = 974 # CL_EIV_Biotoptypen
CODELIST_BIOTOPES_ID = 654 # CL_Biotoptypen
CODELIST_AFTER_STATE_BIOTOPES_ID = 974 # CL-KSP_ZielBiotoptypen - USAGE HAS BEEN DROPPED IN 2022 IN FAVOR OF 654
CODELIST_BIOTOPES_EXTRA_CODES_ID = 975 # CLZusatzbezeichnung
CODELIST_LAW_ID = 1048 # CLVerfahrensrecht
CODELIST_PROCESS_TYPE_ID = 44382 # CLVerfahrenstyp
CODELIST_COMPENSATION_HANDLER_ID = 1052 # CLEingreifer
CODELIST_HANDLER_ID = 1052 # CLEingreifer
CODELIST_COMPENSATION_ACTION_ID = 1026 # CLMassnahmedetail
CODELIST_COMPENSATION_ACTION_DETAIL_ID = 1035 # CLZusatzmerkmal
CODELIST_COMPENSATION_ACTION_CLASS_ID = 1034 # CLMassnahmeklasse

View File

@@ -3,6 +3,8 @@ from django.contrib import admin
from compensation.models import Compensation, CompensationAction, CompensationState, Payment, \
EcoAccountDeduction, EcoAccount
from konova.admin import BaseObjectAdmin, BaseResourceAdmin
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
from user.models import UserAction
class AbstractCompensationAdmin(BaseObjectAdmin):
@@ -19,16 +21,30 @@ class AbstractCompensationAdmin(BaseObjectAdmin):
"identifier",
"title",
"comment",
"after_states",
"before_states",
"list_after_states",
"list_before_states",
"geometry",
]
def get_readonly_fields(self, request, obj=None):
return super().get_readonly_fields(request, obj) + [
"after_states",
"before_states",
"list_after_states",
"list_before_states",
"geometry",
]
def list_after_states(self, obj):
states = obj.after_states.all()
states = [str(state) for state in states]
states = "\n".join(states)
return states
def list_before_states(self, obj):
states = obj.before_states.all()
states = [str(state) for state in states]
states = "\n".join(states)
return states
class CompensationAdmin(AbstractCompensationAdmin):
autocomplete_fields = [
@@ -39,9 +55,21 @@ class CompensationAdmin(AbstractCompensationAdmin):
return super().get_fields(request, obj) + [
"is_cef",
"is_coherence_keeping",
"is_pik",
"intervention",
]
def restore_deleted_data(self, request, queryset):
super().restore_deleted_data(request, queryset)
for entry in queryset:
# Remove delete log entry from related intervention log history
logs = entry.intervention.log.filter(
action=UserAction.EDITED,
comment=COMPENSATION_REMOVED_TEMPLATE.format(entry.identifier)
)
logs.delete()
class EcoAccountAdmin(AbstractCompensationAdmin):
list_display = [

View File

@@ -59,8 +59,9 @@ class CheckboxCompensationTableFilter(CheckboxTableFilter):
"""
if not value:
return queryset.filter(
intervention__users__in=[self.user], # requesting user has access
)
Q(intervention__users__in=[self.user]) | # requesting user has access
Q(intervention__teams__in=self.user.shared_teams)
).distinct()
else:
return queryset
@@ -127,24 +128,6 @@ class CheckboxEcoAccountTableFilter(CheckboxTableFilter):
)
)
def filter_show_all(self, queryset, name, value) -> QuerySet:
""" Filters queryset depending on value of 'show_all' setting
Args:
queryset ():
name ():
value ():
Returns:
"""
if not value:
return queryset.filter(
users__in=[self.user], # requesting user has access
)
else:
return queryset
def filter_only_show_unrecorded(self, queryset, name, value) -> QuerySet:
""" Filters queryset depending on value of 'show_recorded' setting

View File

@@ -13,12 +13,12 @@ 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
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
from intervention.models import Intervention, Responsibility, Legal, Handler
from konova.forms import BaseForm, SimpleGeomForm
from konova.utils.message_templates import EDITED_GENERAL_DATA
from konova.utils.message_templates import EDITED_GENERAL_DATA, COMPENSATION_ADDED_TEMPLATE
from user.models import UserActionLogEntry
@@ -101,12 +101,30 @@ class CompensationResponsibleFormMixin(forms.Form):
}
)
)
handler = forms.CharField(
label=_("Eco-account handler"),
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=_("Who handles the eco-account"),
help_text=_("Detail input on the handler"),
widget=forms.TextInput(
attrs={
"placeholder": _("Company Mustermann"),
@@ -142,7 +160,23 @@ class CoherenceCompensationFormMixin(forms.Form):
)
class NewCompensationForm(AbstractCompensationForm, CEFCompensationFormMixin, CoherenceCompensationFormMixin):
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.
@@ -173,6 +207,7 @@ class NewCompensationForm(AbstractCompensationForm, CEFCompensationFormMixin, Co
"identifier",
"title",
"intervention",
"is_pik",
"is_cef",
"is_coherence_keeping",
"comment",
@@ -200,35 +235,51 @@ class NewCompensationForm(AbstractCompensationForm, CEFCompensationFormMixin, Co
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():
# 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)
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,
geometry=geometry,
comment=comment,
)
# Add the log entry to the main objects log list
comp.log.add(action)
comp = self.__create_comp(user, geom_form)
comp.intervention.mark_as_edited(user, edit_comment=COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
return comp
@@ -249,6 +300,7 @@ class EditCompensationForm(NewCompensationForm):
"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 = []
@@ -265,6 +317,7 @@ class EditCompensationForm(NewCompensationForm):
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
@@ -281,6 +334,7 @@ class EditCompensationForm(NewCompensationForm):
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()
@@ -290,7 +344,7 @@ class EditCompensationForm(NewCompensationForm):
return self.instance
class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMixin, PikCompensationFormMixin):
""" Form for creating eco accounts
Inherits from basic AbstractCompensationForm and further form fields from CompensationResponsibleFormMixin
@@ -331,7 +385,9 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
"registration_date",
"surface",
"conservation_file_number",
"handler",
"is_pik",
"handler_type",
"handler_detail",
"comment",
]
@@ -339,13 +395,13 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
super().__init__(*args, **kwargs)
self.form_title = _("New Eco-Account")
self.action_url = reverse("compensation:acc-new")
self.cancel_redirect = reverse("compensation:acc-index")
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["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):
@@ -354,10 +410,12 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
registration_date = self.cleaned_data.get("registration_date", None)
handler = self.cleaned_data.get("handler", 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
@@ -365,6 +423,11 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
# 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,
@@ -384,9 +447,10 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
created=action,
geometry=geometry,
comment=comment,
is_pik=is_pik,
legal=legal
)
acc.share_with(user)
acc.share_with_user(user)
# Add the log entry to the main objects log list
acc.log.add(action)
@@ -402,21 +466,24 @@ class EditEcoAccountForm(NewEcoAccountForm):
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,))
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": self.instance.responsible.handler,
"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 = []
@@ -431,11 +498,13 @@ class EditEcoAccountForm(NewEcoAccountForm):
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
registration_date = self.cleaned_data.get("registration_date", None)
handler = self.cleaned_data.get("handler", 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)
@@ -444,7 +513,9 @@ class EditEcoAccountForm(NewEcoAccountForm):
geometry = geom_form.save(action)
# Update responsible data
self.instance.responsible.handler = handler
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()
@@ -459,6 +530,7 @@ class EditEcoAccountForm(NewEcoAccountForm):
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()

View File

@@ -17,11 +17,13 @@ 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 import BaseModalForm, NewDocumentForm
from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm
from konova.models import DeadlineType
from konova.utils.message_templates import FORM_INVALID, ADDED_COMPENSATION_STATE, ADDED_DEADLINE, \
ADDED_COMPENSATION_ACTION
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):
@@ -100,10 +102,54 @@ class NewPaymentForm(BaseModalForm):
def save(self):
pay = self.instance.add_payment(self)
self.instance.mark_as_edited(self.user, self.request)
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
@@ -111,22 +157,12 @@ class NewStateModalForm(BaseModalForm):
What has been on this area before changes/compensations have been applied and what will be the result ('after')?
"""
biotope_type = forms.ModelChoiceField(
biotope_type = forms.ChoiceField(
label=_("Biotope Type"),
label_suffix="",
required=True,
help_text=_("Select the biotope type"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_BIOTOPES_ID],
),
widget=autocomplete.ModelSelect2(
url="codes-biotope-autocomplete",
attrs={
"data-placeholder": _("Biotope Type"),
}
),
widget=CompensationStateTreeRadioSelect(),
)
biotope_extra = forms.ModelMultipleChoiceField(
label=_("Biotope additional type"),
@@ -164,6 +200,16 @@ class NewStateModalForm(BaseModalForm):
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)
@@ -219,6 +265,66 @@ class NewStateModalForm(BaseModalForm):
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
@@ -271,7 +377,30 @@ class NewDeadlineModalForm(BaseModalForm):
def save(self):
deadline = self.instance.add_deadline(self)
self.instance.mark_as_edited(self.user, self.request, ADDED_DEADLINE)
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
@@ -284,22 +413,13 @@ class NewActionModalForm(BaseModalForm):
"""
from compensation.models import UnitChoices
action_type = forms.ModelChoiceField(
action_type = forms.MultipleChoiceField(
label=_("Action Type"),
label_suffix="",
required=True,
help_text=_("Select the action type"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_COMPENSATION_ACTION_ID],
),
widget=autocomplete.ModelSelect2(
url="codes-compensation-action-autocomplete",
attrs={
"data-placeholder": _("Action"),
}
),
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"),
@@ -346,10 +466,9 @@ class NewActionModalForm(BaseModalForm):
)
comment = forms.CharField(
required=False,
max_length=200,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment, maximum {} letters").format(200),
help_text=_("Additional comment"),
widget=forms.Textarea(
attrs={
"rows": 5,
@@ -362,6 +481,16 @@ class NewActionModalForm(BaseModalForm):
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)
@@ -369,9 +498,37 @@ class NewActionModalForm(BaseModalForm):
return action
class NewCompensationDocumentForm(NewDocumentForm):
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 NewEcoAccountDocumentForm(NewDocumentForm):
class NewEcoAccountDocumentModalForm(NewDocumentModalForm):
document_model = EcoAccountDocument

View File

@@ -8,17 +8,6 @@ Created on: 14.10.21
from django.db import models
class CompensationActionManager(models.Manager):
""" Holds default db fetch setting for this model type
"""
def get_queryset(self):
return super().get_queryset().select_related(
"action_type",
"action_type__parent"
)
class CompensationStateManager(models.Manager):
""" Holds default db fetch setting for this model type

View File

@@ -0,0 +1,42 @@
# Generated by Django 3.1.3 on 2022-02-10 13:02
from django.db import migrations, models
def migrate_actions(apps, schema_editor):
CompensationAction = apps.get_model('compensation', 'CompensationAction')
actions = CompensationAction.objects.all()
for action in actions:
action_type = action.action_type or []
action.action_type_tmp.set([action_type])
action.save()
if not action.action_type_tmp.count() > 0:
raise ValueError("Migration of actions did not work! Stoped before data loss!")
class Migration(migrations.Migration):
dependencies = [
('codelist', '0001_initial'),
('compensation', '0003_auto_20220202_0846'),
]
operations = [
migrations.AddField(
model_name='compensationaction',
name='action_type_tmp',
field=models.ManyToManyField(blank=True, limit_choices_to={'code_lists__in': [1026], 'is_archived': False, 'is_selectable': True}, related_name='_compensationaction_action_type_+', to='codelist.KonovaCode'),
),
migrations.RunPython(migrate_actions),
migrations.RemoveField(
model_name='compensationaction',
name='action_type',
),
migrations.RenameField(
model_name='compensationaction',
old_name='action_type_tmp',
new_name='action_type',
)
]

View File

@@ -0,0 +1,46 @@
# Generated by Django 3.1.3 on 2022-02-18 08:17
from django.db import migrations, models, transaction
import django.db.models.deletion
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_AFTER_STATE_BIOTOPES_ID
def migrate_entries_974_to_654(apps, schema_editor):
CompensationState = apps.get_model("compensation", "CompensationState")
KonovaCode = apps.get_model("codelist", "KonovaCode")
all_states = CompensationState.objects.all()
with transaction.atomic():
for state in all_states:
code_from_654 = KonovaCode.objects.get(
short_name=state.biotope_type.short_name,
code_lists__in=[CODELIST_BIOTOPES_ID],
is_archived=False,
is_leaf=True,
)
state.biotope_type = code_from_654
state.save()
old_list_states = CompensationState.objects.filter(
biotope_type__code_lists__in=[CODELIST_AFTER_STATE_BIOTOPES_ID]
)
if old_list_states.count() > 0:
raise Exception("Still unmigrated values!")
class Migration(migrations.Migration):
dependencies = [
('codelist', '0001_initial'),
('compensation', '0004_auto_20220210_1402'),
]
operations = [
migrations.RunPython(migrate_entries_974_to_654),
migrations.AlterField(
model_name='compensationstate',
name='biotope_type',
field=models.ForeignKey(blank=True, limit_choices_to={'code_lists__in': [654], 'is_archived': False, 'is_selectable': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='codelist.konovacode'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.1.3 on 2022-02-18 09:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0003_team'),
('compensation', '0005_auto_20220218_0917'),
]
operations = [
migrations.AddField(
model_name='ecoaccount',
name='teams',
field=models.ManyToManyField(help_text='Teams having access (data shared with)', to='user.Team'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.1.3 on 2022-05-31 10:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('compensation', '0006_ecoaccount_teams'),
]
operations = [
migrations.AddField(
model_name='compensation',
name='is_pik',
field=models.BooleanField(blank=True, default=False, help_text="Flag if compensation is a 'Produktonsintegrierte Kompensation'", null=True),
),
migrations.AddField(
model_name='ecoaccount',
name='is_pik',
field=models.BooleanField(blank=True, default=False, help_text="Flag if compensation is a 'Produktonsintegrierte Kompensation'", null=True),
),
]

View File

@@ -10,7 +10,6 @@ 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 compensation.managers import CompensationActionManager
from konova.models import BaseResource
@@ -30,10 +29,8 @@ class CompensationAction(BaseResource):
"""
Compensations include actions like planting trees, refreshing rivers and so on.
"""
action_type = models.ForeignKey(
action_type = models.ManyToManyField(
KonovaCode,
on_delete=models.SET_NULL,
null=True,
blank=True,
limit_choices_to={
"code_lists__in": [CODELIST_COMPENSATION_ACTION_ID],
@@ -56,10 +53,8 @@ class CompensationAction(BaseResource):
unit = models.CharField(max_length=100, null=True, blank=True, choices=UnitChoices.choices)
comment = models.TextField(blank=True, null=True, help_text="Additional comment")
objects = CompensationActionManager()
def __str__(self):
return f"{self.action_type} | {self.amount} {self.unit}"
return f"{self.action_type.all()} | {self.amount} {self.unit}"
@property
def unit_humanize(self):

View File

@@ -8,7 +8,9 @@ Created on: 16.11.21
import shutil
from django.contrib import messages
from user.models import User
from codelist.models import KonovaCode
from user.models import User, Team
from django.db import models, transaction
from django.db.models import QuerySet, Sum
from django.http import HttpRequest
@@ -20,7 +22,9 @@ from compensation.utils.quality import CompensationQualityChecker
from konova.models import BaseObject, AbstractDocument, Deadline, generate_document_file_upload_path, \
GeoReferencedMixin
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, COMPENSATION_REMOVED_TEMPLATE, \
DOCUMENT_REMOVED_TEMPLATE, COMPENSATION_EDITED_TEMPLATE, DEADLINE_REMOVED, ADDED_DEADLINE, \
COMPENSATION_ACTION_REMOVED, COMPENSATION_STATE_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE
from user.models import UserActionLogEntry
@@ -60,7 +64,6 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin):
user = form.user
with transaction.atomic():
created_action = UserActionLogEntry.get_created_action(user)
edited_action = UserActionLogEntry.get_edited_action(user, _("Added deadline"))
deadline = Deadline.objects.create(
type=form_data["type"],
@@ -69,12 +72,26 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin):
created=created_action,
)
self.modified = edited_action
self.save()
self.log.add(edited_action)
self.deadlines.add(deadline)
self.mark_as_edited(user, edit_comment=ADDED_DEADLINE)
return deadline
def remove_deadline(self, form):
""" Removes a deadline from the abstract compensation
Args:
form (RemoveDeadlineModalForm): The form holding all relevant data
Returns:
"""
deadline = form.deadline
user = form.user
with transaction.atomic():
deadline.delete()
self.mark_as_edited(user, edit_comment=DEADLINE_REMOVED)
def add_action(self, form) -> CompensationAction:
""" Adds a new action to the compensation
@@ -89,17 +106,32 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin):
with transaction.atomic():
user_action = UserActionLogEntry.get_created_action(user)
comp_action = CompensationAction.objects.create(
action_type=form_data["action_type"],
amount=form_data["amount"],
unit=form_data["unit"],
comment=form_data["comment"],
created=user_action,
)
comp_action.action_type.set(form_data.get("action_type", []))
comp_action_details = form_data["action_type_details"]
comp_action.action_type_details.set(comp_action_details)
self.actions.add(comp_action)
return comp_action
def remove_action(self, form):
""" Removes a CompensationAction from the abstract compensation
Args:
form (RemoveCompensationActionModalForm): The form holding all relevant data
Returns:
"""
action = form.action
user = form.user
with transaction.atomic():
action.delete()
self.mark_as_edited(user, edit_comment=COMPENSATION_ACTION_REMOVED)
def add_state(self, form, is_before_state: bool) -> CompensationState:
""" Adds a new compensation state to the compensation
@@ -112,8 +144,10 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin):
"""
form_data = form.cleaned_data
with transaction.atomic():
biotope_type_id = form_data["biotope_type"]
code = KonovaCode.objects.get(id=biotope_type_id)
state = CompensationState.objects.create(
biotope_type=form_data["biotope_type"],
biotope_type=code,
surface=form_data["surface"],
)
state_additional_types = form_data["biotope_extra"]
@@ -124,6 +158,21 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin):
self.after_states.add(state)
return state
def remove_state(self, form):
""" Removes a CompensationState from the abstract compensation
Args:
form (RemoveCompensationStateModalForm): The form holding all relevant data
Returns:
"""
state = form.state
user = form.user
with transaction.atomic():
state.delete()
self.mark_as_edited(user, edit_comment=COMPENSATION_STATE_REMOVED)
def get_surface_after_states(self) -> float:
""" Calculates the compensation's/account's surface
@@ -208,7 +257,22 @@ class CoherenceMixin(models.Model):
abstract = True
class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
class PikMixin(models.Model):
""" Provides PIK flag as Mixin
"""
is_pik = models.BooleanField(
blank=True,
null=True,
default=False,
help_text="Flag if compensation is a 'Produktonsintegrierte Kompensation'"
)
class Meta:
abstract = True
class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
"""
Regular compensation, linked to an intervention
"""
@@ -235,6 +299,11 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
self.identifier = self.generate_new_identifier()
super().save(*args, **kwargs)
def mark_as_deleted(self, user, send_mail: bool = True):
super().mark_as_deleted(user, send_mail)
if user is not None:
self.intervention.mark_as_edited(user, edit_comment=COMPENSATION_REMOVED_TEMPLATE.format(self.identifier))
def is_shared_with(self, user: User):
""" Access check
@@ -249,7 +318,7 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
# Compensations inherit their shared state from the interventions
return self.intervention.is_shared_with(user)
def share_with(self, user: User):
def share_with_user(self, user: User):
""" Adds user to list of shared access users
Args:
@@ -258,10 +327,9 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
Returns:
"""
if not self.intervention.is_shared_with(user):
self.intervention.users.add(user)
self.intervention.users.add(user)
def share_with_list(self, user_list: list):
def share_with_user_list(self, user_list: list):
""" Sets the list of shared access users
Args:
@@ -272,6 +340,28 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
"""
self.intervention.users.set(user_list)
def share_with_team(self, team: Team):
""" Adds team to list of shared access teams
Args:
team (Team): The team to be added to the object
Returns:
"""
self.intervention.teams.add(team)
def share_with_team_list(self, team_list: list):
""" Sets the list of shared access teams
Args:
team_list (list): The teams to be added to the object
Returns:
"""
self.intervention.teams.set(team_list)
@property
def shared_users(self) -> QuerySet:
""" Shortcut for fetching the users which have shared access on this object
@@ -281,27 +371,14 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
"""
return self.intervention.users.all()
def get_LANIS_link(self) -> str:
""" Generates a link for LANIS depending on the geometry
@property
def shared_teams(self) -> QuerySet:
""" Shortcut for fetching the teams which have shared access on this object
Returns:
users (QuerySet)
"""
try:
geom = self.geometry.geom.transform(DEFAULT_SRID_RLP, clone=True)
x = geom.centroid.x
y = geom.centroid.y
zoom_lvl = 16
except AttributeError:
# If no geometry has been added, yet.
x = 1
y = 1
zoom_lvl = 6
return LANIS_LINK_TEMPLATE.format(
zoom_lvl,
x,
y,
)
return self.intervention.teams.all()
def get_documents(self) -> QuerySet:
""" Getter for all documents of a compensation
@@ -326,7 +403,9 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
Returns:
"""
self.intervention.mark_as_edited(user, request, edit_comment, reset_recorded)
self.intervention.unrecord(user, request)
action = super().mark_as_edited(user, edit_comment=edit_comment)
return action
def is_ready_for_publish(self) -> bool:
""" Not inherited by RecordableObjectMixin
@@ -338,6 +417,38 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
"""
return self.intervention.is_ready_for_publish()
def set_status_messages(self, request: HttpRequest):
""" Setter for different information that need to be rendered
Adds messages to the given HttpRequest
Args:
request (HttpRequest): The incoming request
Returns:
request (HttpRequest): The modified request
"""
if self.intervention.legal.revocations.exists():
messages.error(
request,
INTERVENTION_HAS_REVOCATIONS_TEMPLATE.format(self.intervention.legal.revocations.count()),
extra_tags="danger",
)
super().set_status_messages(request)
return request
@property
def is_recorded(self):
""" Getter for record status as property
Since compensations inherit their record status from their intervention, the intervention's status is being
returned
Returns:
"""
return self.intervention.is_recorded
class CompensationDocument(AbstractDocument):
"""
@@ -353,7 +464,7 @@ class CompensationDocument(AbstractDocument):
max_length=1000,
)
def delete(self, *args, **kwargs):
def delete(self, user=None, *args, **kwargs):
"""
Custom delete functionality for CompensationDocuments.
Removes the folder from the file system if there are no further documents for this entry.
@@ -375,6 +486,9 @@ class CompensationDocument(AbstractDocument):
folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path)
if user:
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))
# Remove the file itself
super().delete(*args, **kwargs)

View File

@@ -7,23 +7,23 @@ Created on: 16.11.21
"""
import shutil
from user.models import User
from django.urls import reverse
from konova.utils.message_templates import DEDUCTION_REMOVED, DOCUMENT_REMOVED_TEMPLATE
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models, transaction
from django.db import models
from django.db.models import Sum, QuerySet
from django.utils.translation import gettext_lazy as _
from compensation.managers import EcoAccountManager, EcoAccountDeductionManager
from compensation.models.compensation import AbstractCompensation
from compensation.models.compensation import AbstractCompensation, PikMixin
from compensation.utils.quality import EcoAccountQualityChecker
from konova.models import ShareableObjectMixin, RecordableObjectMixin, AbstractDocument, BaseResource, \
generate_document_file_upload_path
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
from user.models import UserActionLogEntry
class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin):
class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, PikMixin):
"""
An eco account is a kind of 'prepaid' compensation. It can be compared to an account that already has been filled
with some kind of currency. From this account one is able to deduct currency for current projects.
@@ -122,28 +122,6 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
return ret_val_total, ret_val_relative
def get_LANIS_link(self) -> str:
""" Generates a link for LANIS depending on the geometry
Returns:
"""
try:
geom = self.geometry.geom.transform(DEFAULT_SRID_RLP, clone=True)
x = geom.centroid.x
y = geom.centroid.y
zoom_lvl = 16
except AttributeError:
# If no geometry has been added, yet.
x = 1
y = 1
zoom_lvl = 6
return LANIS_LINK_TEMPLATE.format(
zoom_lvl,
x,
y,
)
def quality_check(self) -> EcoAccountQualityChecker:
""" Quality check
@@ -165,34 +143,6 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
)
return docs
def add_deduction(self, form):
""" Adds a new deduction to the intervention
Args:
form (NewDeductionModalForm): The form holding the data
Returns:
"""
form_data = form.cleaned_data
user = form.user
with transaction.atomic():
# Create log entry
user_action_create = UserActionLogEntry.get_created_action(user)
user_action_edit = UserActionLogEntry.get_edited_action(user)
self.log.add(user_action_edit)
self.modified = user_action_edit
self.save()
deduction = EcoAccountDeduction.objects.create(
intervention=form_data["intervention"],
account=self,
surface=form_data["surface"],
created=user_action_create,
)
return deduction
def is_ready_for_publish(self) -> bool:
""" Checks whether the data passes all constraints for being publishable
@@ -203,6 +153,14 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
is_ready = is_recorded
return is_ready
def get_share_link(self):
""" Returns the share url for the object
Returns:
"""
return reverse("compensation:acc:share", args=(self.id, self.access_token))
class EcoAccountDocument(AbstractDocument):
"""
@@ -218,7 +176,7 @@ class EcoAccountDocument(AbstractDocument):
max_length=1000,
)
def delete(self, *args, **kwargs):
def delete(self, user=None, *args, **kwargs):
"""
Custom delete functionality for EcoAccountDocuments.
Removes the folder from the file system if there are no further documents for this entry.
@@ -240,6 +198,9 @@ class EcoAccountDocument(AbstractDocument):
folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path)
if user:
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))
# Remove the file itself
super().delete(*args, **kwargs)
@@ -285,3 +246,9 @@ class EcoAccountDeduction(BaseResource):
def __str__(self):
return "{} of {}".format(self.surface, self.account)
def delete(self, user=None, *args, **kwargs):
if user is not None:
self.intervention.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED)
self.account.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED)
super().delete(*args, **kwargs)

View File

@@ -10,6 +10,8 @@ from django.db import models
from intervention.models import Intervention
from konova.models import BaseResource
from konova.utils.message_templates import PAYMENT_REMOVED
from user.models import UserActionLogEntry
class Payment(BaseResource):

View File

@@ -6,6 +6,7 @@ Created on: 16.11.21
"""
from django.db import models
from django.db.models import Q
from codelist.models import KonovaCode
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID

View File

@@ -5,17 +5,15 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 01.12.20
"""
from user.models import User
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.timezone import localtime
from django.utils.translation import gettext_lazy as _
from compensation.filters import CompensationTableFilter, EcoAccountTableFilter
from compensation.models import Compensation, EcoAccount
from konova.sub_settings.django_settings import DEFAULT_DATE_TIME_FORMAT
from konova.utils.tables import BaseTable, TableRenderMixin
import django_tables2 as tables
@@ -31,6 +29,11 @@ class CompensationTable(BaseTable, TableRenderMixin):
orderable=True,
accessor="title",
)
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=True,
accessor="geometry",
)
c = tables.Column(
verbose_name=_("Checked"),
orderable=True,
@@ -80,14 +83,17 @@ class CompensationTable(BaseTable, TableRenderMixin):
Returns:
"""
html = ""
html += self.render_link(
tooltip=_("Open {}").format(_("Compensation")),
href=reverse("compensation:detail", args=(record.id,)),
txt=value,
new_tab=False,
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 format_html(html)
return html
def render_c(self, value, record: Compensation):
""" Renders the checked column for a compensation
@@ -103,18 +109,45 @@ class CompensationTable(BaseTable, TableRenderMixin):
"""
html = ""
checked = value is not None
tooltip = _("Not checked yet")
tooltip = DATA_IS_UNCHECKED
previously_checked = record.intervention.get_last_checked_action()
if checked:
value = value.timestamp
value = localtime(value)
checked_on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
tooltip = _("Checked on {} by {}").format(checked_on, record.intervention.checked.user)
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
@@ -129,9 +162,7 @@ class CompensationTable(BaseTable, TableRenderMixin):
recorded = value is not None
tooltip = _("Not recorded yet")
if recorded:
value = value.timestamp
value = localtime(value)
on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
on = value.get_timestamp_str_formatted()
tooltip = _("Recorded on {} by {}").format(on, record.intervention.recorded.user)
html += self.render_bookmark(
tooltip=tooltip,
@@ -149,11 +180,7 @@ class CompensationTable(BaseTable, TableRenderMixin):
Returns:
"""
if value is None:
value = User.objects.none()
has_access = value.filter(
id=self.user.id
).exists()
has_access = record.is_shared_with(self.user)
html = self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),
@@ -173,10 +200,20 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
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"),
@@ -201,7 +238,7 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
def __init__(self, request: HttpRequest, *args, **kwargs):
self.title = _("Eco Accounts")
self.add_new_url = reverse("compensation:acc-new")
self.add_new_url = reverse("compensation:acc:new")
qs = kwargs.get("queryset", None)
self.filter = EcoAccountTableFilter(
user=request.user,
@@ -224,7 +261,7 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
html = ""
html += self.render_link(
tooltip=_("Open {}").format(_("Eco-account")),
href=reverse("compensation:acc-detail", args=(record.id,)),
href=reverse("compensation:acc:detail", args=(record.id,)),
txt=value,
new_tab=False,
)
@@ -244,6 +281,28 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
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
@@ -258,9 +317,7 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
checked = value is not None
tooltip = _("Not recorded yet. Can not be used for deductions, yet.")
if checked:
value = value.timestamp
value = localtime(value)
on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
on = value.get_timestamp_str_formatted()
tooltip = _("Recorded on {} by {}").format(on, record.recorded.user)
html += self.render_bookmark(
tooltip=tooltip,
@@ -281,7 +338,7 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
html = ""
# Do not use value in here, since value does use unprefetched 'users' manager, where record has already
# prefetched users data
has_access = self.user in record.users.all()
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",

View File

@@ -1,4 +1,5 @@
{% load i18n l10n fontawesome_5 humanize %}
{% load i18n l10n fontawesome_5 humanize ksp_filters %}
<div id="actions" class="card">
<div class="card-header rlp-r">
<div class="row">
@@ -20,16 +21,13 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th class="w-25" scope="col">
<th class="" scope="col">
{% trans 'Action type' %}
</th>
<th class="w-25" scope="col">
{% trans 'Action type details' %}
</th>
<th scope="col">
{% trans 'Amount' context 'Compensation' %}
</th>
@@ -37,8 +35,10 @@
{% trans 'Comment' %}
</th>
{% if is_default_member and has_access %}
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
{% endif %}
</tr>
@@ -46,18 +46,28 @@
<tbody>
{% for action in actions %}
<tr>
<td class="align-middle">
{{ action.action_type }}
</td>
<td class="align-middle">
<td class="">
{% for type in action.action_type.all %}
<div> {{type.parent.parent.long_name}} {% fa5_icon 'angle-right' %} {{type.parent.long_name}} {% fa5_icon 'angle-right' %} {{type.long_name}} </div>
<hr>
{% endfor %}
{% for detail in action.action_type_details.all %}
<div class="mb-2" title="{{detail}}">{{detail.long_name}}</div>
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
{% endfor %}
</td>
<td class="align-middle">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
<td class="align-middle">{{ action.comment|default_if_none:"" }}</td>
<td class="align-middle">
<td class="">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
<td class="">
<div class="scroll-150">
{{ action.comment }}
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:action-remove' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove action' %}">
{% fa5_icon 'trash' %}
</button>

View File

@@ -20,7 +20,7 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
@@ -34,8 +34,10 @@
{% trans 'Comment' %}
</th>
{% if is_default_member and has_access %}
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
{% endif %}
</tr>
@@ -47,10 +49,17 @@
{% trans deadline.type_humanized %}
</td>
<td class="align-middle">{{ deadline.date|default_if_none:"---" }}</td>
<td class="align-middle">{{ deadline.comment }}</td>
<td>
<td class="align-middle">
<div class="scroll-150">
{{ deadline.comment }}
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'deadline-remove' deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove deadline' %}">
<button data-form-url="{% url 'compensation:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:deadline-remove' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove deadline' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -20,19 +20,24 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">
{% trans 'Title' %}
</th>
<th scope="col">
{% trans 'Created on' %}
</th>
<th scope="col">
{% trans 'Comment' %}
</th>
{% if is_default_member and has_access %}
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
{% endif %}
</tr>
@@ -41,14 +46,24 @@
{% for doc in obj.documents.all %}
<tr>
<td class="align-middle">
<a href="{% url 'compensation:get-doc' doc.id %}">
<a href="{% url 'compensation:get-doc' obj.id doc.id %}">
{{ doc.title }}
</a>
</td>
<td class="align-middle">{{ doc.comment }}</td>
<td>
<td class="align-middle">
{{ doc.date_of_creation }}
</td>
<td class="align-middle">
<div class="scroll-150">
{{ doc.comment }}
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:remove-doc' doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
<button data-form-url="{% url 'compensation:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:remove-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -20,27 +20,26 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
{% if sum_before_states > sum_after_states %}
<div class="row alert alert-danger">
{% trans 'Missing surfaces according to states before: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
{% if sum_before_states > sum_after_states %}
<div class="alert alert-danger mb-0">
{% trans 'Missing surfaces according to states before: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th class="w-25" scope="col">
<th class="w-50" scope="col">
{% trans 'Biotope type' %}
</th>
<th class="w-25" scope="col">
{% trans 'Biotope additional type' %}
</th>
<th scope="col">
{% trans 'Surface' %}
</th>
{% if is_default_member and has_access %}
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
{% endif %}
</tr>
@@ -48,19 +47,21 @@
<tbody>
{% for state in after_states %}
<tr>
<td class="align-middle">
{{ state.biotope_type }}
</td>
<td class="align-middle">
{% for biotope_extra in state.biotope_type_details.all %}
<div class="mb-2" title="{{ biotope_extra }}">
{{ biotope_extra.long_name }}
</div>
<td>
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
{% endfor %}
</td>
<td class="align-middle">{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle">
<td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %}
</button>

View File

@@ -20,27 +20,26 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
{% if sum_before_states < sum_after_states %}
<div class="row alert alert-danger">
{% trans 'Missing surfaces according to states after: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
{% if sum_before_states < sum_after_states %}
<div class="alert alert-danger mb-0">
{% trans 'Missing surfaces according to states after: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th class="w-25" scope="col">
<th class="w-50" scope="col">
{% trans 'Biotope type' %}
</th>
<th class="w-25" scope="col">
{% trans 'Biotope additional type' %}
</th>
<th scope="col">
{% trans 'Surface' %}
</th>
{% if is_default_member and has_access %}
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
{% endif %}
</tr>
@@ -48,19 +47,21 @@
<tbody>
{% for state in before_states %}
<tr>
<td class="align-middle">
{{ state.biotope_type }}
</td>
<td class="align-middle">
{% for biotope_extra in state.biotope_type_details.all %}
<div class="mb-2" title="{{ biotope_extra }}">
{{ biotope_extra.long_name }}
</div>
<td>
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
{% endfor %}
</td>
<td class="align-middle">{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle">
<td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %}
</button>

View File

@@ -2,6 +2,7 @@
{% load i18n l10n static fontawesome_5 humanize ksp_filters %}
{% block head %}
{% comment %}
dal documentation (django-autocomplete-light) states using form.media for adding needed scripts.
This does not work properly with modal forms, as the scripts are not loaded properly inside the modal.
@@ -14,16 +15,16 @@
{% block body %}
<div id="detail-header" class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<h3>{% trans 'Compensation' %}<br> {{obj.identifier}}</h3>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/compensation/includes/controls.html' %}
</div>
</div>
<hr>
<div id="data" class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="table-container">
<table class="table table-hover">
<tr>
@@ -38,6 +39,16 @@
</a>
</td>
</tr>
<tr>
<th scope="row">{% trans 'Is PIK' %}</th>
<td class="align-middle">
{% if obj.is_pik %}
{% trans 'Yes' %}
{% else %}
{% trans 'No' %}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans 'Is CEF compensation' %}</th>
<td class="align-middle">
@@ -65,6 +76,11 @@
<span>
{% fa5_icon 'star' 'far' %}
</span>
{% if last_checked %}
<span class="rlp-gd-inv" title="{{last_checked_tooltip}}">
{% fa5_icon 'star' 'fas' %}
</span>
{% endif %}
{% else %}
<span class="check-star" title="{% trans 'Checked on '%} {{obj.intervention.checked.timestamp}} {% trans 'by' %} {{obj.intervention.checked.user}}">
{% fa5_icon 'star' %}
@@ -89,14 +105,24 @@
<tr>
<th scope="row">{% trans 'Last modified' %}</th>
<td class="align-middle">
{{obj.modified.timestamp|default_if_none:""|naturalday}}
<br>
{{obj.modified.user.username}}
{% if obj.modified %}
{{obj.modified.timestamp|default_if_none:""}}
<br>
{{obj.modified.user.username}}
{% else %}
{{obj.created.timestamp|default_if_none:""}}
<br>
{{obj.created.user.username}}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans 'Shared with' %}</th>
<td class="align-middle">
{% for team in obj.intervention.shared_teams %}
{% include 'user/includes/team_data_modal_button.html' %}
{% endfor %}
<hr>
{% for user in obj.intervention.users.all %}
{% include 'user/includes/contact_modal_button.html' %}
{% endfor %}
@@ -105,39 +131,44 @@
</table>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="row">
{% include 'map/geom_form.html' %}
</div>
<div class="row">
{% include 'konova/includes/parcels.html' %}
</div>
<div class="row">
{% include 'konova/includes/comment_card.html' %}
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="col">
<div class="row">
<div class="col-sm-12">
{% include 'map/geom_form.html' %}
</div>
</div>
<div class="row">
{% include 'konova/includes/parcels/parcels.html' %}
</div>
<div class="row">
{% include 'konova/includes/comment_card.html' %}
</div>
</div>
</div>
</div>
<hr>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/compensation/includes/states-before.html' %}
<div id="related_data">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/compensation/includes/states-before.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/compensation/includes/states-after.html' %}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/compensation/includes/states-after.html' %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/compensation/includes/actions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/compensation/includes/deadlines.html' %}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/compensation/includes/actions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/compensation/includes/deadlines.html' %}
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/compensation/includes/documents.html' %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/compensation/includes/documents.html' %}
</div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
{% load i18n l10n fontawesome_5 humanize %}
{% load i18n l10n fontawesome_5 humanize ksp_filters %}
<div id="actions" class="card">
<div class="card-header rlp-r">
<div class="row">
@@ -11,7 +11,7 @@
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc-new-action' obj.id %}" title="{% trans 'Add new action' %}">
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-action' obj.id %}" title="{% trans 'Add new action' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'seedling' %}
</button>
@@ -20,25 +20,24 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th class="w-25" scope="col">
<th class="" scope="col">
{% trans 'Action type' %}
</th>
<th class="w-25" scope="col">
{% trans 'Action type details' %}
</th>
<th scope="col">
{% trans 'Amount' context 'Compensation' %}
</th>
<th scope="col">
{% trans 'Comment' %}
</th>
{% if default_member and has_access %}
<th scope="col">
{% trans 'Action' %}
{% if is_default_member and has_access %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
{% endif %}
</tr>
@@ -46,19 +45,29 @@
<tbody>
{% for action in actions %}
<tr>
<td class="align-middle">
{{ action.action_type }}
</td>
<td class="align-middle">
<td class="">
{% for type in action.action_type.all %}
<div> {{type.parent.parent.long_name}} {% fa5_icon 'angle-right' %} {{type.parent.long_name}} {% fa5_icon 'angle-right' %} {{type.long_name}} </div>
<hr>
{% endfor %}
{% for detail in action.action_type_details.all %}
<div class="mb-2" title="{{detail}}">{{detail.long_name}}</div>
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
{% endfor %}
</td>
<td class="align-middle">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
<td class="align-middle">{{ action.comment|default_if_none:"" }}</td>
<td class="align-middle">
<td class="">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
<td class="">
<div class="scroll-150">
{{ action.comment }}
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc-action-remove' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove action' %}">
<button data-form-url="{% url 'compensation:acc:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:acc:action-remove' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove action' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -6,36 +6,36 @@
LANIS
</button>
</a>
<a href="{% url 'compensation:acc-report' obj.id %}" target="_blank" class="mr-2">
<a href="{% url 'compensation:acc:report' obj.id %}" target="_blank" class="mr-2">
<button class="btn btn-default" title="{% trans 'Public report' %}">
{% fa5_icon 'file-alt' %}
</button>
</a>
{% if has_access %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'compensation:share-create' obj.id %}">
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'compensation:acc:share-create' obj.id %}">
{% fa5_icon 'share-alt' %}
</button>
{% if is_ets_member %}
{% if obj.recorded %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Unrecord' %}" data-form-url="{% url 'compensation:acc-record' obj.id %}">
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Unrecord' %}" data-form-url="{% url 'compensation:acc:record' obj.id %}">
{% fa5_icon 'bookmark' 'far' %}
</button>
{% else %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Record' %}" data-form-url="{% url 'compensation:acc-record' obj.id %}">
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Record' %}" data-form-url="{% url 'compensation:acc:record' obj.id %}">
{% fa5_icon 'bookmark' %}
</button>
{% endif %}
{% endif %}
{% if is_default_member %}
<a href="{% url 'compensation:acc-edit' obj.id %}" class="mr-2">
<a href="{% url 'compensation:acc:edit' obj.id %}" class="mr-2">
<button class="btn btn-default" title="{% trans 'Edit' %}">
{% fa5_icon 'edit' %}
</button>
</a>
<button class="btn btn-default btn-modal mr-2" data-form-url="{% url 'compensation:acc-log' obj.id %}" title="{% trans 'Show log' %}">
<button class="btn btn-default btn-modal mr-2" data-form-url="{% url 'compensation:acc:log' obj.id %}" title="{% trans 'Show log' %}">
{% fa5_icon 'history' %}
</button>
<button class="btn btn-default btn-modal" data-form-url="{% url 'compensation:acc-remove' obj.id %}" title="{% trans 'Delete' %}">
<button class="btn btn-default btn-modal" data-form-url="{% url 'compensation:acc:remove' obj.id %}" title="{% trans 'Delete' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -11,7 +11,7 @@
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc-new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'calendar-check' %}
</button>
@@ -20,7 +20,7 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
@@ -33,8 +33,10 @@
<th scope="col">
{% trans 'Comment' %}
</th>
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
</tr>
</thead>
@@ -45,10 +47,17 @@
{% trans deadline.type_humanized %}
</td>
<td class="align-middle">{{ deadline.date|default_if_none:"---" }}</td>
<td class="align-middle">{{ deadline.comment }}</td>
<td>
<td class="align-middle">
<div class="scroll-150">
{{ deadline.comment }}
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'deadline-remove' deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove deadline' %}">
<button data-form-url="{% url 'compensation:acc:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:acc:deadline-remove' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove deadline' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -10,8 +10,8 @@
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc-new-deduction' obj.id %}" title="{% trans 'Add new deduction' %}">
{% if is_default_member and obj.recorded %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-deduction' obj.id %}" title="{% trans 'Add new deduction' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'tree' %}
</button>
@@ -20,7 +20,7 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
@@ -36,8 +36,10 @@
<th scope="col">
{% trans 'Created' %}
</th>
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
</tr>
</thead>
@@ -58,9 +60,12 @@
</td>
<td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td>
<td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td>
<td>
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc-remove-deduction' deduction.account.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove Deduction' %}">
<td class="align-middle float-right">
{% if is_default_member and has_access or is_default_member and user in deduction.intervention.shared_users %}
<button data-form-url="{% url 'compensation:acc:edit-deduction' deduction.account.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:acc:remove-deduction' deduction.account.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove Deduction' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -11,7 +11,7 @@
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc-new-doc' obj.id %}" title="{% trans 'Add new document' %}">
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'file' %}
</button>
@@ -20,7 +20,7 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
@@ -28,10 +28,15 @@
{% trans 'Title' %}
</th>
<th scope="col">
{% trans 'Comment' %}
{% trans 'Created on' %}
</th>
<th scope="col">
{% trans 'Action' %}
{% trans 'Comment' %}
</th>
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
</tr>
</thead>
@@ -39,14 +44,24 @@
{% for doc in obj.documents.all %}
<tr>
<td class="align-middle">
<a href="{% url 'compensation:acc-get-doc' doc.id %}">
<a href="{% url 'compensation:acc:get-doc' obj.id doc.id %}">
{{ doc.title }}
</a>
</td>
<td class="align-middle">{{ doc.comment }}</td>
<td>
<td class="align-middle">
{{ doc.date_of_creation }}
</td>
<td class="align-middle">
<div class="scroll-150">
{{ doc.comment }}
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc-remove-doc' doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
<button data-form-url="{% url 'compensation:acc:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:acc:remove-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -11,7 +11,7 @@
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc-new-state' obj.id %}" title="{% trans 'Add new state after' %}">
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'layer-group' %}
</button>
@@ -20,27 +20,26 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
{% if sum_before_states > sum_after_states %}
<div class="row alert alert-danger">
{% trans 'Missing surfaces according to states before: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
{% if sum_before_states > sum_after_states %}
<div class="alert alert-danger mb-0">
{% trans 'Missing surfaces according to states before: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th class="w-25" scope="col">
<th class="w-50" scope="col">
{% trans 'Biotope type' %}
</th>
<th class="w-25" scope="col">
{% trans 'Biotope additional type' %}
</th>
<th scope="col">
{% trans 'Surface' %}
</th>
{% if is_default_member and has_access %}
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
{% endif %}
</tr>
@@ -48,20 +47,22 @@
<tbody>
{% for state in after_states %}
<tr>
<td class="align-middle">
{{ state.biotope_type }}
</td>
<td class="align-middle">
{% for biotope_extra in state.biotope_type_details.all %}
<div class="mb-2" title="{{ biotope_extra }}">
{{ biotope_extra.long_name }}
</div>
<td>
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
{% endfor %}
</td>
<td class="align-middle">{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle">
<td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc-state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
<button data-form-url="{% url 'compensation:acc:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:acc:state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -11,7 +11,7 @@
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc-new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'layer-group' %}
</button>
@@ -20,27 +20,26 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
{% if sum_before_states < sum_after_states %}
<div class="row alert alert-danger">
{% trans 'Missing surfaces according to states after: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
{% if sum_before_states < sum_after_states %}
<div class="alert alert-danger mb-0">
{% trans 'Missing surfaces according to states after: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th class="w-25" scope="col">
<th class="w-50" scope="col">
{% trans 'Biotope type' %}
</th>
<th class="w-25" scope="col">
{% trans 'Biotope additional type' %}
</th>
<th scope="col">
{% trans 'Surface' %}
</th>
{% if is_default_member and has_access %}
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
{% endif %}
</tr>
@@ -48,20 +47,22 @@
<tbody>
{% for state in before_states %}
<tr>
<td class="align-middle">
{{ state.biotope_type }}
</td>
<td class="align-middle">
{% for biotope_extra in state.biotope_type_details.all %}
<div class="mb-2" title="{{ biotope_extra }}">
{{ biotope_extra.long_name }}
</div>
<td>
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
{% endfor %}
</td>
<td class="align-middle">{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle">
<td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc-state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
<button data-form-url="{% url 'compensation:acc:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'compensation:acc:state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -2,6 +2,7 @@
{% load i18n l10n static fontawesome_5 humanize %}
{% block head %}
{% comment %}
dal documentation (django-autocomplete-light) states using form.media for adding needed scripts.
This does not work properly with modal forms, as the scripts are not loaded properly inside the modal.
@@ -14,16 +15,16 @@
{% block body %}
<div id="detail-header" class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<h3>{% trans 'Eco-account' %}<br> {{obj.identifier}}</h3>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/eco_account/includes/controls.html' %}
</div>
</div>
<hr>
<div id="data" class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="table-container">
<table class="table table-hover">
<tr>
@@ -69,17 +70,37 @@
<th scope="row">{% trans 'Action handler' %}</th>
<td class="align-middle">{{obj.responsible.handler|default_if_none:""}}</td>
</tr>
<tr>
<th scope="row">{% trans 'Is PIK' %}</th>
<td class="align-middle">
{% if obj.is_pik %}
{% trans 'Yes' %}
{% else %}
{% trans 'No' %}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans 'Last modified' %}</th>
<td class="align-middle">
{{obj.modified.timestamp|default_if_none:""|naturalday}}
<br>
{{obj.modified.user.username}}
{% if obj.modified %}
{{obj.modified.timestamp|default_if_none:""}}
<br>
{{obj.modified.user.username}}
{% else %}
{{obj.created.timestamp|default_if_none:""}}
<br>
{{obj.created.user.username}}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans 'Shared with' %}</th>
<td class="align-middle">
{% for team in obj.shared_teams %}
{% include 'user/includes/team_data_modal_button.html' %}
{% endfor %}
<hr>
{% for user in obj.users.all %}
{% include 'user/includes/contact_modal_button.html' %}
{% endfor %}
@@ -88,12 +109,14 @@
</table>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="row">
{% include 'map/geom_form.html' %}
<div class="col-sm-12">
{% include 'map/geom_form.html' %}
</div>
</div>
<div class="row">
{% include 'konova/includes/parcels.html' %}
{% include 'konova/includes/parcels/parcels.html' %}
</div>
<div class="row">
{% include 'konova/includes/comment_card.html' %}
@@ -102,28 +125,30 @@
</div>
<hr>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/eco_account/includes/states-before.html' %}
<div id="related_data">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/eco_account/includes/states-before.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/eco_account/includes/states-after.html' %}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/eco_account/includes/states-after.html' %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/eco_account/includes/actions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/eco_account/includes/deadlines.html' %}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/eco_account/includes/actions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/eco_account/includes/deadlines.html' %}
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/eco_account/includes/documents.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/eco_account/includes/deductions.html' %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/eco_account/includes/documents.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'compensation/detail/eco_account/includes/deductions.html' %}
</div>
</div>
</div>

View File

@@ -3,7 +3,7 @@
{% block body %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<h3>{% trans 'Report' %}</h3>
<h4>{{obj.identifier}}</h4>
<div class="table-container">
@@ -20,6 +20,36 @@
</a>
</td>
</tr>
<tr>
<th scope="row">{% trans 'Is PIK' %}</th>
<td class="align-middle">
{% if obj.is_pik %}
{% trans 'Yes' %}
{% else %}
{% trans 'No' %}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans 'Is CEF' %}</th>
<td class="align-middle">
{% if obj.is_cef %}
{% trans 'Yes' %}
{% else %}
{% trans 'No' %}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans 'Is coherence keeping' %}</th>
<td class="align-middle">
{% if obj.is_coherence_keeping %}
{% trans 'Yes' %}
{% else %}
{% trans 'No' %}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans 'Last modified' %}</th>
<td class="align-middle">
@@ -33,22 +63,17 @@
{% include 'compensation/detail/compensation/includes/states-after.html' %}
{% include 'compensation/detail/compensation/includes/actions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="row">
{% include 'map/geom_form.html' %}
<div class="col-sm-12">
{% include 'map/geom_form.html' %}
</div>
</div>
<div class="row">
{% include 'konova/includes/parcels.html' %}
{% include 'konova/includes/parcels/parcels.html' %}
</div>
<div class="row">
<div class="col-sm-6 col-md-6 col-lg-6">
<h4>{% trans 'Open in browser' %}</h4>
{{ qrcode|safe }}
</div>
<div class="col-sm-6 col-md-6 col-lg-6">
<h4>{% trans 'View in LANIS' %}</h4>
{{ qrcode_lanis|safe }}
</div>
{% include 'konova/includes/report/qrcodes.html' %}
</div>
</div>

View File

@@ -3,7 +3,7 @@
{% block body %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<h3>{% trans 'Report' %}</h3>
<h4>{{obj.identifier}}</h4>
<div class="table-container">
@@ -21,8 +21,14 @@
<td class="align-middle">{{obj.responsible.conservation_file_number|default_if_none:""}}</td>
</tr>
<tr>
<th scope="row">{% trans 'Action handler' %}</th>
<td class="align-middle">{{obj.responsible.handler|default_if_none:""}}</td>
<th scope="row">{% trans 'Is PIK' %}</th>
<td class="align-middle">
{% if obj.is_pik %}
{% trans 'Yes' %}
{% else %}
{% trans 'No' %}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans 'Deductions for' %}</th>
@@ -50,22 +56,17 @@
{% include 'compensation/detail/compensation/includes/states-after.html' %}
{% include 'compensation/detail/compensation/includes/actions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="row">
{% include 'map/geom_form.html' %}
<div class="col-sm-12">
{% include 'map/geom_form.html' %}
</div>
</div>
<div class="row">
{% include 'konova/includes/parcels.html' %}
{% include 'konova/includes/parcels/parcels.html' %}
</div>
<div class="row">
<div class="col-sm-6 col-md-6 col-lg-6">
<h4>{% trans 'Open in browser' %}</h4>
{{ qrcode|safe }}
</div>
<div class="col-sm-6 col-md-6 col-lg-6">
<h4>{% trans 'View in LANIS' %}</h4>
{{ qrcode_lanis|safe }}
</div>
{% include 'konova/includes/report/qrcodes.html' %}
</div>
</div>

View File

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

View File

@@ -0,0 +1,248 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 07.02.22
"""
from django.test.client import Client
from django.urls import reverse
from konova.models import Deadline, DeadlineType
from konova.settings import DEFAULT_GROUP
from konova.tests.test_views import BaseViewTestCase
class CompensationViewTestCase(BaseViewTestCase):
"""
These tests focus on proper returned views depending on the user's groups privileges and login status
"""
@classmethod
def setUpTestData(cls) -> None:
super().setUpTestData()
def setUp(self) -> None:
super().setUp()
state = self.create_dummy_states()
self.compensation.before_states.set([state])
self.compensation.after_states.set([state])
action = self.create_dummy_action()
self.compensation.actions.set([action])
self.deadline = Deadline.objects.get_or_create(
type=DeadlineType.FINISHED,
date="2020-01-01",
comment="TESTDEADDLINECOMMENT"
)[0]
self.compensation.deadlines.add(self.deadline)
# Prepare urls
self.index_url = reverse("compensation:index", args=())
self.new_url = reverse("compensation:new", args=(self.intervention.id,))
self.new_id_url = reverse("compensation:new-id", args=())
self.detail_url = reverse("compensation:detail", args=(self.compensation.id,))
self.log_url = reverse("compensation:log", args=(self.compensation.id,))
self.edit_url = reverse("compensation:edit", args=(self.compensation.id,))
self.remove_url = reverse("compensation:remove", args=(self.compensation.id,))
self.report_url = reverse("compensation:report", args=(self.compensation.id,))
self.state_new_url = reverse("compensation:new-state", args=(self.compensation.id,))
self.action_new_url = reverse("compensation:new-action", args=(self.compensation.id,))
self.deadline_new_url = reverse("compensation:new-deadline", args=(self.compensation.id,))
self.deadline_edit_url = reverse("compensation:deadline-edit", args=(self.compensation.id, self.deadline.id))
self.deadline_remove_url = reverse("compensation:deadline-remove", args=(self.compensation.id, self.deadline.id))
self.new_doc_url = reverse("compensation:new-doc", args=(self.compensation.id,))
self.state_remove_url = reverse("compensation:state-remove", args=(self.compensation.id, self.comp_state.id,))
self.action_remove_url = reverse("compensation:action-remove", args=(self.compensation.id, self.comp_action.id,))
def test_anonymous_user(self):
""" Check correct status code for all requests
Assumption: User not logged in
Returns:
"""
client = Client()
success_urls = [
self.report_url,
]
fail_urls = [
self.index_url,
self.detail_url,
self.new_url,
self.new_id_url,
self.log_url,
self.edit_url,
self.remove_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_no_groups_shared(self):
""" Check correct status code for all requests
Assumption: User logged in and has no groups and data is shared
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
self.intervention.share_with_user_list([self.superuser])
# Since the user has no groups, it does not matter that data has been shared. There SHOULD not be any difference
# to a user without access, since the important permissions are missing
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
]
fail_urls = [
self.new_url,
self.new_id_url,
self.log_url,
self.edit_url,
self.remove_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_no_groups_unshared(self):
""" Check correct status code for all requests
Assumption: User logged in and has no groups and data is not shared
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.intervention.share_with_user_list([])
# Since the user has no groups, it does not matter that data is unshared. There SHOULD not be any difference
# to a user having shared access, since all important permissions are missing
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
]
fail_urls = [
self.new_url,
self.new_id_url,
self.log_url,
self.edit_url,
self.remove_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_default_group_shared(self):
""" Check correct status code for all requests
Assumption: User logged in, is default group member and data is shared
--> Default group necessary since all base functionalities depend on this group membership
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([group])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.intervention.share_with_user_list([self.superuser])
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
self.new_url,
self.new_id_url,
self.edit_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
self.log_url,
self.remove_url,
]
self.assert_url_success(client, success_urls)
def test_logged_in_default_group_unshared(self):
""" Check correct status code for all requests
Assumption: User logged in, is default group member and data is NOT shared
--> Default group necessary since all base functionalities depend on this group membership
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([group])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.intervention.share_with_user_list([])
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
self.new_id_url,
]
fail_urls = [
self.new_url,
self.edit_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
self.log_url,
self.remove_url,
]
self.assert_url_fail(client, fail_urls)
self.assert_url_success(client, success_urls)

View File

@@ -2,7 +2,7 @@
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 11.11.21
Created on: 07.02.22
"""
import datetime
@@ -11,7 +11,7 @@ from django.contrib.gis.geos import MultiPolygon
from django.urls import reverse
from compensation.models import Compensation
from konova.settings import ETS_GROUP, ZB_GROUP
from konova.settings import ZB_GROUP, ETS_GROUP
from konova.tests.test_views import BaseWorkflowTestCase
from user.models import UserAction
@@ -21,17 +21,18 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
def setUpTestData(cls):
super().setUpTestData()
# Give the user shared access to the dummy intervention -> inherits the access to the compensation
cls.intervention.share_with(cls.superuser)
# Make sure the intervention itself would be fine with valid data
cls.intervention = cls.fill_out_intervention(cls.intervention)
# Make sure the compensation is linked to the intervention
cls.intervention.compensations.set([cls.compensation])
def setUp(self) -> None:
super().setUp()
# Give the user shared access to the dummy intervention -> inherits the access to the compensation
self.intervention.share_with_user(self.superuser)
# Make sure the intervention itself would be fine with valid data
self.intervention = self.fill_out_intervention(self.intervention)
# Make sure the compensation is linked to the intervention
self.intervention.compensations.set([self.compensation])
# Delete all existing compensations, which might be created by tests
Compensation.objects.all().delete()
@@ -49,23 +50,33 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
test_id = self.create_dummy_string()
test_title = self.create_dummy_string()
test_geom = self.create_dummy_geometry()
geom_json = self.create_geojson(test_geom)
post_data = {
"identifier": test_id,
"title": test_title,
"geom": test_geom.geojson,
"geom": geom_json,
"intervention": self.intervention.id,
}
pre_creation_intervention_log_count = self.intervention.log.count()
# Preserve the current number of intervention's compensations
num_compensations = self.intervention.compensations.count()
self.client_user.post(new_url, post_data)
response = self.client_user.post(new_url, post_data)
self.assertEqual(302, response.status_code)
self.intervention.refresh_from_db()
self.assertEqual(num_compensations + 1, self.intervention.compensations.count())
new_compensation = self.intervention.compensations.get(identifier=test_id)
self.assertEqual(new_compensation.identifier, test_id)
self.assertEqual(new_compensation.title, test_title)
self.assert_equal_geometries(new_compensation.geometry.geom, test_geom)
self.assertEqual(new_compensation.log.count(), 1)
# Expect logs to be set
self.assertEqual(pre_creation_intervention_log_count + 1, self.intervention.log.count())
self.assertEqual(new_compensation.log.count(), 1)
self.assertEqual(self.intervention.log.first().action, UserAction.EDITED)
self.assertEqual(new_compensation.log.first().action, UserAction.CREATED)
def test_new_from_intervention(self):
""" Test the creation of a compensation from a given intervention
@@ -78,11 +89,13 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
test_id = self.create_dummy_string()
test_title = self.create_dummy_string()
test_geom = self.create_dummy_geometry()
geom_json = self.create_geojson(test_geom)
post_data = {
"identifier": test_id,
"title": test_title,
"geom": test_geom.geojson,
"geom": geom_json,
}
pre_creation_intervention_log_count = self.intervention.log.count()
# Preserve the current number of intervention's compensations
num_compensations = self.intervention.compensations.count()
@@ -95,6 +108,12 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
self.assertEqual(new_compensation.title, test_title)
self.assert_equal_geometries(new_compensation.geometry.geom, test_geom)
# Expect logs to be set
self.assertEqual(new_compensation.log.count(), 1)
self.assertEqual(new_compensation.log.first().action, UserAction.CREATED)
self.assertEqual(pre_creation_intervention_log_count + 1, self.intervention.log.count())
self.assertEqual(self.intervention.log.first().action, UserAction.EDITED)
def test_edit(self):
""" Checks that the editing of a compensation works
@@ -103,11 +122,13 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
"""
url = reverse("compensation:edit", args=(self.compensation.id,))
self.compensation = self.fill_out_compensation(self.compensation)
pre_edit_log_count = self.compensation.log.count()
new_title = self.create_dummy_string()
new_identifier = self.create_dummy_string()
new_comment = self.create_dummy_string()
new_geometry = MultiPolygon(srid=4326) # Create an empty geometry
geojson = self.create_geojson(new_geometry)
check_on_elements = {
self.compensation.title: new_title,
@@ -122,7 +143,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
"title": new_title,
"intervention": self.intervention.id, # just keep the intervention as it is
"comment": new_comment,
"geom": new_geometry.geojson,
"geom": geojson,
}
self.client_user.post(url, post_data)
self.compensation.refresh_from_db()
@@ -138,6 +159,10 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
self.assert_equal_geometries(self.compensation.geometry.geom, new_geometry)
# Expect logs to be set
self.assertEqual(pre_edit_log_count + 1, self.compensation.log.count())
self.assertEqual(self.compensation.log.first().action, UserAction.EDITED)
def test_checkability(self):
"""
This tests if the checkability of the compensation (which is defined by the linked intervention's checked
@@ -152,6 +177,8 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
# Add proper privilege for the user
self.superuser.groups.add(self.groups.get(name=ZB_GROUP))
pre_check_log_count = self.compensation.log.count()
# Prepare url and form data
url = reverse("intervention:check", args=(self.intervention.id,))
post_data = {
@@ -186,6 +213,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
# Expect the user action to be in the log
self.assertIn(checked, self.compensation.log.all())
self.assertEqual(pre_check_log_count + 1, self.compensation.log.count())
def test_recordability(self):
"""
@@ -200,6 +228,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
"""
# Add proper privilege for the user
self.superuser.groups.add(self.groups.get(name=ETS_GROUP))
pre_record_log_count = self.compensation.log.count()
# Prepare url and form data
record_url = reverse("intervention:record", args=(self.intervention.id,))
@@ -234,62 +263,28 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
# Expect the user action to be in the log
self.assertIn(recorded, self.compensation.log.all())
self.assertEqual(pre_record_log_count + 1, self.compensation.log.count())
def test_non_editable_after_recording(self):
""" Tests that the compensation can not be edited after being recorded
class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()
# Add user to conservation office group and give shared access to the account
cls.superuser.groups.add(cls.groups.get(name=ETS_GROUP))
cls.eco_account.share_with_list([cls.superuser])
def test_deductability(self):
"""
This tests the deductability of an eco account.
An eco account should only be deductible if it is recorded.
User must be redirected to another page
Returns:
"""
# Give user shared access to the dummy intervention, which will be needed here
self.intervention.share_with(self.superuser)
# Prepare data for deduction creation
deduct_url = reverse("compensation:acc-new-deduction", args=(self.eco_account.id,))
test_surface = 10.00
post_data = {
"surface": test_surface,
"account": self.id,
"intervention": self.intervention.id,
}
# Perform request --> expect to fail
self.client_user.post(deduct_url, post_data)
# Expect that no deduction has been created
self.assertEqual(0, self.eco_account.deductions.count())
self.assertEqual(0, self.intervention.deductions.count())
# Now mock the eco account as it would be recorded (with invalid data)
# Make sure the deductible surface is high enough 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.save()
self.assertIsNotNone(self.eco_account.recorded)
self.assertGreater(self.eco_account.deductable_surface, test_surface)
# Rerun the request
self.client_user.post(deduct_url, post_data)
# Expect that the deduction has been created
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(deduction.account, self.eco_account)
self.assertEqual(deduction.intervention, self.intervention)
self.assertIsNotNone(self.compensation)
self.assertFalse(self.compensation.is_recorded)
edit_url = reverse("compensation:edit", args=(self.compensation.id,))
response = self.client_user.get(edit_url)
has_redirect = response.status_code == 302
self.assertFalse(has_redirect)
self.compensation.intervention.set_recorded(self.user)
self.assertTrue(self.compensation.is_recorded)
edit_url = reverse("compensation:edit", args=(self.compensation.id,))
response = self.client_user.get(edit_url)
has_redirect = response.status_code == 302
self.assertTrue(has_redirect)
self.compensation.intervention.set_unrecorded(self.user)

View File

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

View File

@@ -0,0 +1,229 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 27.10.21
"""
from django.urls import reverse
from django.test import Client
from compensation.tests.compensation.test_views import CompensationViewTestCase
from konova.models import DeadlineType, Deadline
from konova.settings import DEFAULT_GROUP
class EcoAccountViewTestCase(CompensationViewTestCase):
"""
These tests focus on proper returned views depending on the user's groups privileges and login status
EcoAccounts can inherit the same tests used for compensations.
"""
comp_state = None
comp_action = None
@classmethod
def setUpTestData(cls) -> None:
super().setUpTestData()
def setUp(self) -> None:
super().setUp()
state = self.create_dummy_states()
self.eco_account.before_states.set([state])
self.eco_account.after_states.set([state])
action = self.create_dummy_action()
self.eco_account.actions.set([action])
# Prepare urls
self.index_url = reverse("compensation:acc:index", args=())
self.new_url = reverse("compensation:acc:new", args=())
self.new_id_url = reverse("compensation:acc:new-id", args=())
self.detail_url = reverse("compensation:acc:detail", args=(self.eco_account.id,))
self.log_url = reverse("compensation:acc:log", args=(self.eco_account.id,))
self.edit_url = reverse("compensation:acc:edit", args=(self.eco_account.id,))
self.remove_url = reverse("compensation:acc:remove", args=(self.eco_account.id,))
self.report_url = reverse("compensation:acc:report", args=(self.eco_account.id,))
self.state_new_url = reverse("compensation:acc:new-state", args=(self.eco_account.id,))
self.state_edit_url = reverse("compensation:acc:state-edit", args=(self.eco_account.id, self.comp_state.id))
self.state_remove_url = reverse("compensation:acc:state-remove", args=(self.eco_account.id, self.comp_state.id,))
self.action_new_url = reverse("compensation:acc:new-action", args=(self.eco_account.id,))
self.action_edit_url = reverse("compensation:acc:action-edit", args=(self.eco_account.id, self.comp_action.id))
self.action_remove_url = reverse("compensation:acc:action-remove", args=(self.eco_account.id, self.comp_action.id,))
self.deadline = Deadline.objects.get_or_create(
type=DeadlineType.FINISHED,
date="2020-01-01",
comment="DEADLINE COMMENT"
)[0]
self.eco_account.deadlines.add(self.deadline)
self.deadline_new_url = reverse("compensation:acc:new-deadline", args=(self.eco_account.id,))
self.deadline_edit_url = reverse("compensation:acc:deadline-edit", args=(self.eco_account.id, self.deadline.id))
self.deadline_remove_url = reverse("compensation:acc:deadline-remove", args=(self.eco_account.id, self.deadline.id))
self.new_doc_url = reverse("compensation:acc:new-doc", args=(self.eco_account.id,))
def test_logged_in_no_groups_shared(self):
""" Check correct status code for all requests
Assumption: User logged in and has no groups and data is shared
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
self.eco_account.share_with_user_list([self.superuser])
# Since the user has no groups, it does not matter that data has been shared. There SHOULD not be any difference
# to a user without access, since the important permissions are missing
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
]
fail_urls = [
self.new_url,
self.new_id_url,
self.log_url,
self.edit_url,
self.remove_url,
self.state_new_url,
self.state_edit_url,
self.state_remove_url,
self.action_new_url,
self.action_edit_url,
self.action_remove_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.new_doc_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_no_groups_unshared(self):
""" Check correct status code for all requests
Assumption: User logged in and has no groups and data is shared
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
self.eco_account.share_with_user_list([])
# Since the user has no groups, it does not matter that data is unshared. There SHOULD not be any difference
# to a user having shared access, since all important permissions are missing
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
]
fail_urls = [
self.new_url,
self.new_id_url,
self.log_url,
self.edit_url,
self.remove_url,
self.state_new_url,
self.state_edit_url,
self.state_remove_url,
self.action_new_url,
self.action_edit_url,
self.action_remove_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.new_doc_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_default_group_shared(self):
""" Check correct status code for all requests
Assumption: User logged in, is default group member and data is shared
--> Default group necessary since all base functionalities depend on this group membership
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([group])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.eco_account.share_with_user_list([self.superuser])
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
self.new_url,
self.new_id_url,
self.edit_url,
self.state_new_url,
self.state_edit_url,
self.state_remove_url,
self.action_new_url,
self.action_edit_url,
self.action_remove_url,
self.new_doc_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.log_url,
self.remove_url,
]
self.assert_url_success(client, success_urls)
def test_logged_in_default_group_unshared(self):
""" Check correct status code for all requests
Assumption: User logged in, is default group member and data is NOT shared
--> Default group necessary since all base functionalities depend on this group membership
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([group])
self.eco_account.share_with_user_list([])
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
self.new_id_url,
self.new_url,
]
fail_urls = [
self.edit_url,
self.state_new_url,
self.state_edit_url,
self.state_remove_url,
self.action_new_url,
self.action_edit_url,
self.action_remove_url,
self.new_doc_url,
self.log_url,
self.remove_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
]
self.assert_url_fail(client, fail_urls)
self.assert_url_success(client, success_urls)

View File

@@ -0,0 +1,329 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 11.11.21
"""
import datetime
from django.contrib.gis.geos import MultiPolygon
from django.core.exceptions import ObjectDoesNotExist
from django.urls import reverse
from compensation.models import EcoAccount, EcoAccountDeduction
from konova.settings import ETS_GROUP, DEFAULT_GROUP
from konova.tests.test_views import BaseWorkflowTestCase
from user.models import UserAction
class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()
def setUp(self) -> None:
super().setUp()
# Add user to conservation office group and give shared access to the account
self.superuser.groups.add(self.groups.get(name=DEFAULT_GROUP))
self.superuser.groups.add(self.groups.get(name=ETS_GROUP))
self.eco_account.share_with_user_list([self.superuser])
def test_new(self):
""" Test the creation of an EcoAccount
Returns:
"""
# Prepare url and form data to be posted
new_url = reverse("compensation:acc:new")
test_id = self.create_dummy_string()
test_title = self.create_dummy_string()
test_geom = self.create_dummy_geometry()
geom_json = self.create_geojson(test_geom)
test_deductable_surface = 1000
test_conservation_office = self.get_conservation_office_code()
post_data = {
"identifier": test_id,
"title": test_title,
"geom": geom_json,
"deductable_surface": test_deductable_surface,
"conservation_office": test_conservation_office.id
}
self.client_user.post(new_url, post_data)
try:
acc = EcoAccount.objects.get(
identifier=test_id
)
except ObjectDoesNotExist:
self.fail(msg="EcoAccount not created")
self.assertEqual(acc.identifier, test_id)
self.assertEqual(acc.title, test_title)
self.assert_equal_geometries(acc.geometry.geom, test_geom)
self.assertEqual(acc.log.count(), 1)
# Expect logs to be set
self.assertEqual(acc.log.count(), 1)
self.assertEqual(acc.log.first().action, UserAction.CREATED)
def test_edit(self):
""" Checks that the editing of an EcoAccount works
Returns:
"""
self.eco_account.share_with_user(self.superuser)
url = reverse("compensation:acc:edit", args=(self.eco_account.id,))
pre_edit_log_count = self.eco_account.log.count()
new_title = self.create_dummy_string()
new_identifier = self.create_dummy_string()
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
check_on_elements = {
self.eco_account.title: new_title,
self.eco_account.identifier: new_identifier,
self.eco_account.comment: new_comment,
self.eco_account.deductable_surface: test_deductable_surface,
}
for k, v in check_on_elements.items():
self.assertNotEqual(k, v)
post_data = {
"identifier": new_identifier,
"title": new_title,
"comment": new_comment,
"geom": new_geometry.geojson,
"surface": test_deductable_surface,
"conservation_office": test_conservation_office.id
}
self.client_user.post(url, post_data)
self.eco_account.refresh_from_db()
check_on_elements = {
self.eco_account.title: new_title,
self.eco_account.identifier: new_identifier,
self.eco_account.deductable_surface: test_deductable_surface,
self.eco_account.comment: new_comment,
}
for k, v in check_on_elements.items():
self.assertEqual(k, v)
self.assert_equal_geometries(self.eco_account.geometry.geom, new_geometry)
# Expect logs to be set
self.assertEqual(pre_edit_log_count + 1, self.eco_account.log.count())
self.assertEqual(self.eco_account.log.first().action, UserAction.EDITED)
def test_recordability(self):
"""
This tests if the recordability of the EcoAccount is triggered by the quality of it's data (e.g. not all fields filled)
Returns:
"""
# Add proper privilege for the user
self.eco_account.share_with_user(self.superuser)
pre_record_log_count = self.eco_account.log.count()
# Prepare url and form data
record_url = reverse("compensation:acc:record", args=(self.eco_account.id,))
post_data = {
"confirm": True,
}
self.eco_account.refresh_from_db()
# Make sure the account is not recorded
self.assertIsNone(self.eco_account.recorded)
# Run the request --> expect fail, since the account is not valid, yet
self.client_user.post(record_url, post_data)
# Check that the account is still not recorded
self.assertIsNone(self.eco_account.recorded)
# Now fill out the data for an ecoaccount
self.eco_account = self.fill_out_eco_account(self.eco_account)
# Rerun the request
self.client_user.post(record_url, post_data)
# Expect the EcoAccount now to be recorded
# Attention: We can only test the date part of the timestamp,
# since the delay in microseconds would lead to fail
self.eco_account.refresh_from_db()
recorded = self.eco_account.recorded
self.assertIsNotNone(recorded)
self.assertEqual(self.superuser, recorded.user)
self.assertEqual(UserAction.RECORDED, recorded.action)
self.assertEqual(datetime.date.today(), recorded.timestamp.date())
# Expect the user action to be in the log
self.assertIn(recorded, self.eco_account.log.all())
self.assertEqual(pre_record_log_count + 1, self.eco_account.log.count())
def test_new_deduction(self):
"""
This tests the deductability of an eco account.
An eco account should only be deductible if it is recorded.
Returns:
"""
# Give user shared access to the dummy intervention, which will be needed here
self.intervention.share_with_user(self.superuser)
pre_deduction_acc_log_count = self.eco_account.log.count()
pre_deduction_int_log_count = self.intervention.log.count()
# Prepare data for deduction creation
deduct_url = reverse("compensation:acc:new-deduction", args=(self.eco_account.id,))
test_surface = 10.00
post_data = {
"surface": test_surface,
"account": self.eco_account.id,
"intervention": self.intervention.id,
}
# Perform request --> expect to fail
self.client_user.post(deduct_url, post_data)
# Expect that no deduction has been created
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
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.save()
self.assertIsNotNone(self.eco_account.recorded)
self.assertGreater(self.eco_account.deductable_surface, test_surface)
# Expect the recorded entry in the log
self.assertEqual(pre_deduction_acc_log_count + 1, self.eco_account.log.count())
self.assertTrue(self.eco_account.log.first().action == UserAction.RECORDED)
# Rerun the request
self.client_user.post(deduct_url, post_data)
# Expect that the deduction has been created
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(deduction.account, self.eco_account)
self.assertEqual(deduction.intervention, self.intervention)
# Expect entries in the log
self.assertEqual(pre_deduction_acc_log_count + 2, self.eco_account.log.count())
self.assertTrue(self.eco_account.log.first().action == UserAction.EDITED)
self.assertEqual(pre_deduction_int_log_count + 1, self.intervention.log.count())
self.assertTrue(self.intervention.log.first().action == UserAction.EDITED)
def test_edit_deduction(self):
test_surface = self.eco_account.get_available_rest()[0]
self.eco_account.set_recorded(self.superuser)
self.intervention.share_with_user(self.superuser)
self.eco_account.refresh_from_db()
self.assertTrue(self.superuser, self.intervention.is_shared_with(self.superuser))
deduction = EcoAccountDeduction.objects.create(
intervention=self.intervention,
account=self.eco_account,
surface=0
)
self.assertEqual(1, self.intervention.deductions.count())
self.assertEqual(1, self.eco_account.deductions.count())
# Prepare url and form data to be posted
new_url = reverse("compensation:acc:edit-deduction", args=(self.eco_account.id, deduction.id))
post_data = {
"intervention": deduction.intervention.id,
"account": deduction.account.id,
"surface": test_surface,
}
pre_edit_intervention_log_count = self.intervention.log.count()
pre_edit_account_log_count = self.eco_account.log.count()
num_deductions_intervention = self.intervention.deductions.count()
num_deductions_account = self.eco_account.deductions.count()
self.client_user.post(new_url, post_data)
self.intervention.refresh_from_db()
self.eco_account.refresh_from_db()
deduction.refresh_from_db()
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)
# Expect logs to be set
self.assertEqual(pre_edit_intervention_log_count + 1, self.intervention.log.count())
self.assertEqual(pre_edit_account_log_count + 1, self.eco_account.log.count())
self.assertEqual(self.intervention.log.first().action, UserAction.EDITED)
self.assertEqual(self.eco_account.log.first().action, UserAction.EDITED)
def test_remove_deduction(self):
intervention = self.deduction.intervention
account = self.deduction.account
# Prepare url and form data to be posted
new_url = reverse("compensation:acc:remove-deduction", args=(account.id, self.deduction.id))
post_data = {
"confirm": True,
}
intervention.share_with_user(self.superuser)
account.share_with_user(self.superuser)
pre_edit_intervention_log_count = intervention.log.count()
pre_edit_account_log_count = account.log.count()
num_deductions_intervention = intervention.deductions.count()
num_deductions_account = account.deductions.count()
self.client_user.post(new_url, post_data)
intervention.refresh_from_db()
account.refresh_from_db()
self.assertEqual(num_deductions_intervention - 1, intervention.deductions.count())
self.assertEqual(num_deductions_account - 1, account.deductions.count())
# Expect logs to be set
self.assertEqual(pre_edit_intervention_log_count + 1, intervention.log.count())
self.assertEqual(pre_edit_account_log_count + 1, account.log.count())
self.assertEqual(intervention.log.first().action, UserAction.EDITED)
self.assertEqual(account.log.first().action, UserAction.EDITED)
def test_non_editable_after_recording(self):
""" Tests that the eco_account can not be edited after being recorded
User must be redirected to another page
Returns:
"""
self.assertIsNotNone(self.eco_account)
self.assertFalse(self.eco_account.is_recorded)
edit_url = reverse("compensation:acc:edit", args=(self.eco_account.id,))
response = self.client_user.get(edit_url)
has_redirect = response.status_code == 302
self.assertFalse(has_redirect)
self.eco_account.set_recorded(self.user)
self.assertTrue(self.eco_account.is_recorded)
edit_url = reverse("compensation:acc:edit", args=(self.eco_account.id,))
response = self.client_user.get(edit_url)
has_redirect = response.status_code == 302
self.assertTrue(has_redirect)
self.eco_account.set_unrecorded(self.user)

View File

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

View File

@@ -0,0 +1,156 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 09.02.22
"""
from django.urls import reverse
from django.test.client import Client
from compensation.models import Payment
from konova.settings import DEFAULT_GROUP
from konova.tests.test_views import BaseViewTestCase
class PaymentViewTestCase(BaseViewTestCase):
@classmethod
def setUpTestData(cls) -> None:
super().setUpTestData()
def setUp(self) -> None:
super().setUp()
self.payment = Payment.objects.get_or_create(
intervention=self.intervention,
amount=1,
due_on="2020-01-01",
comment="Testcomment"
)[0]
self.new_url = reverse("compensation:pay:new", args=(self.intervention.id,))
self.edit_url = reverse("compensation:pay:edit", args=(self.intervention.id, self.payment.id))
self.remove_url = reverse("compensation:pay:remove", args=(self.intervention.id, self.payment.id))
def test_anonymous_user(self):
""" Check correct status code for all requests
Assumption: User not logged in
Returns:
"""
client = Client()
success_urls = [
]
fail_urls = [
self.new_url,
self.edit_url,
self.remove_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_no_groups_shared(self):
""" Check correct status code for all requests
Assumption: User logged in and has no groups and data is shared
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
self.intervention.share_with_user_list([self.superuser])
# Since the user has no groups, it does not matter that data has been shared. There SHOULD not be any difference
# to a user without access, since the important permissions are missing
success_urls = [
]
fail_urls = [
self.new_url,
self.edit_url,
self.remove_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_no_groups_unshared(self):
""" Check correct status code for all requests
Assumption: User logged in and has no groups and data is not shared
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.intervention.share_with_user_list([])
# Since the user has no groups, it does not matter that data is unshared. There SHOULD not be any difference
# to a user having shared access, since all important permissions are missing
success_urls = [
]
fail_urls = [
self.new_url,
self.edit_url,
self.remove_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_default_group_shared(self):
""" Check correct status code for all requests
Assumption: User logged in, is default group member and data is shared
--> Default group necessary since all base functionalities depend on this group membership
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([group])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.intervention.share_with_user_list([self.superuser])
success_urls = [
self.new_url,
self.edit_url,
self.remove_url,
]
self.assert_url_success(client, success_urls)
def test_logged_in_default_group_unshared(self):
""" Check correct status code for all requests
Assumption: User logged in, is default group member and data is NOT shared
--> Default group necessary since all base functionalities depend on this group membership
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([group])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.intervention.share_with_user_list([])
success_urls = [
]
fail_urls = [
self.new_url,
self.edit_url,
self.remove_url,
]
self.assert_url_fail(client, fail_urls)
self.assert_url_success(client, success_urls)

View File

@@ -0,0 +1,127 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 09.02.22
"""
from django.core.exceptions import ObjectDoesNotExist
from django.urls import reverse
from compensation.models import Payment
from konova.tests.test_views import BaseWorkflowTestCase
from user.models import UserAction
class PaymentWorkflowTestCase(BaseWorkflowTestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()
def setUp(self) -> None:
super().setUp()
# Give the user shared access to the dummy intervention
self.intervention.share_with_user(self.superuser)
self.payment = Payment.objects.get_or_create(
intervention=self.intervention,
amount=1,
due_on="2020-01-01",
comment="Testcomment"
)[0]
def test_new(self):
""" Test the creation of a payment
Returns:
"""
# Prepare url and form data to be posted
new_url = reverse("compensation:pay:new", args=(self.intervention.id,))
test_amount = 12345
test_due_on = "1970-01-01"
test_comment = self.create_dummy_string()
post_data = {
"amount": test_amount,
"due": test_due_on,
"comment": test_comment,
}
pre_creation_intervention_log_count = self.intervention.log.count()
num_payments = self.intervention.payments.count()
self.client_user.post(new_url, post_data)
self.intervention.refresh_from_db()
self.assertEqual(num_payments + 1, self.intervention.payments.count())
new_payment = self.intervention.payments.get(amount=test_amount)
self.assertEqual(new_payment.amount, test_amount)
self.assertEqual(str(new_payment.due_on), test_due_on)
self.assertEqual(new_payment.comment, test_comment)
# Expect logs to be set
self.assertEqual(pre_creation_intervention_log_count + 1, self.intervention.log.count())
self.assertEqual(self.intervention.log.first().action, UserAction.EDITED)
def test_edit(self):
""" Test edit of a payment
Returns:
"""
# Prepare url and form data to be posted
new_url = reverse("compensation:pay:edit", args=(self.intervention.id, self.payment.id))
test_amount = self.payment.amount * 2
test_due_on = "1970-01-01"
test_comment = self.create_dummy_string()
post_data = {
"amount": test_amount,
"due": test_due_on,
"comment": test_comment,
}
pre_edit_intervention_log_count = self.intervention.log.count()
num_payments = self.intervention.payments.count()
self.client_user.post(new_url, post_data)
self.intervention.refresh_from_db()
self.payment.refresh_from_db()
self.assertEqual(num_payments, self.intervention.payments.count())
self.assertEqual(self.payment.amount, test_amount)
self.assertEqual(str(self.payment.due_on), test_due_on)
self.assertEqual(self.payment.comment, test_comment)
# Expect logs to be set
self.assertEqual(pre_edit_intervention_log_count + 1, self.intervention.log.count())
self.assertEqual(self.intervention.log.first().action, UserAction.EDITED)
def test_remove(self):
""" Test remove of a payment
Returns:
"""
# Prepare url and form data to be posted
new_url = reverse("compensation:pay:remove", args=(self.intervention.id, self.payment.id))
post_data = {
"confirm": True,
}
pre_remove_intervention_log_count = self.intervention.log.count()
num_payments = self.intervention.payments.count()
self.client_user.post(new_url, post_data)
self.intervention.refresh_from_db()
try:
self.payment.refresh_from_db()
self.fail(msg="Payment still exists after delete")
except ObjectDoesNotExist:
pass
self.assertEqual(num_payments - 1, self.intervention.payments.count())
# Expect logs to be set
self.assertEqual(pre_remove_intervention_log_count + 1, self.intervention.log.count())
self.assertEqual(self.intervention.log.first().action, UserAction.EDITED)

View File

@@ -1,406 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 27.10.21
"""
from django.urls import reverse
from django.test import Client
from konova.settings import DEFAULT_GROUP
from konova.tests.test_views import BaseViewTestCase
class CompensationViewTestCase(BaseViewTestCase):
"""
These tests focus on proper returned views depending on the user's groups privileges and login status
"""
@classmethod
def setUpTestData(cls) -> None:
super().setUpTestData()
state = cls.create_dummy_states()
cls.compensation.before_states.set([state])
cls.compensation.after_states.set([state])
action = cls.create_dummy_action()
cls.compensation.actions.set([action])
# Prepare urls
cls.index_url = reverse("compensation:index", args=())
cls.new_url = reverse("compensation:new", args=(cls.intervention.id,))
cls.new_id_url = reverse("compensation:new-id", args=())
cls.detail_url = reverse("compensation:detail", args=(cls.compensation.id,))
cls.log_url = reverse("compensation:log", args=(cls.compensation.id,))
cls.edit_url = reverse("compensation:edit", args=(cls.compensation.id,))
cls.remove_url = reverse("compensation:remove", args=(cls.compensation.id,))
cls.report_url = reverse("compensation:report", args=(cls.compensation.id,))
cls.state_new_url = reverse("compensation:new-state", args=(cls.compensation.id,))
cls.action_new_url = reverse("compensation:new-action", args=(cls.compensation.id,))
cls.deadline_new_url = reverse("compensation:new-deadline", args=(cls.compensation.id,))
cls.new_doc_url = reverse("compensation:new-doc", args=(cls.compensation.id,))
cls.state_remove_url = reverse("compensation:state-remove", args=(cls.compensation.id, cls.comp_state.id,))
cls.action_remove_url = reverse("compensation:action-remove", args=(cls.compensation.id, cls.comp_action.id,))
def test_anonymous_user(self):
""" Check correct status code for all requests
Assumption: User not logged in
Returns:
"""
client = Client()
success_urls = [
self.report_url,
]
fail_urls = [
self.index_url,
self.detail_url,
self.new_url,
self.new_id_url,
self.log_url,
self.edit_url,
self.remove_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_no_groups_shared(self):
""" Check correct status code for all requests
Assumption: User logged in and has no groups and data is shared
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
self.intervention.share_with_list([self.superuser])
# Since the user has no groups, it does not matter that data has been shared. There SHOULD not be any difference
# to a user without access, since the important permissions are missing
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
]
fail_urls = [
self.new_url,
self.new_id_url,
self.log_url,
self.edit_url,
self.remove_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_no_groups_unshared(self):
""" Check correct status code for all requests
Assumption: User logged in and has no groups and data is shared
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.intervention.share_with_list([])
# Since the user has no groups, it does not matter that data is unshared. There SHOULD not be any difference
# to a user having shared access, since all important permissions are missing
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
]
fail_urls = [
self.new_url,
self.new_id_url,
self.log_url,
self.edit_url,
self.remove_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_default_group_shared(self):
""" Check correct status code for all requests
Assumption: User logged in, is default group member and data is shared
--> Default group necessary since all base functionalities depend on this group membership
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([group])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.intervention.share_with_list([self.superuser])
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
self.new_url,
self.new_id_url,
self.edit_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
self.log_url,
self.remove_url,
]
self.assert_url_success(client, success_urls)
def test_logged_in_default_group_unshared(self):
""" Check correct status code for all requests
Assumption: User logged in, is default group member and data is NOT shared
--> Default group necessary since all base functionalities depend on this group membership
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([group])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.intervention.share_with_list([])
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
self.new_id_url,
]
fail_urls = [
self.new_url,
self.edit_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
self.log_url,
self.remove_url,
]
self.assert_url_fail(client, fail_urls)
self.assert_url_success(client, success_urls)
class EcoAccountViewTestCase(CompensationViewTestCase):
"""
These tests focus on proper returned views depending on the user's groups privileges and login status
EcoAccounts can inherit the same tests used for compensations.
"""
comp_state = None
comp_action = None
@classmethod
def setUpTestData(cls) -> None:
super().setUpTestData()
state = cls.create_dummy_states()
cls.eco_account.before_states.set([state])
cls.eco_account.after_states.set([state])
action = cls.create_dummy_action()
cls.eco_account.actions.set([action])
# Prepare urls
cls.index_url = reverse("compensation:acc-index", args=())
cls.new_url = reverse("compensation:acc-new", args=())
cls.new_id_url = reverse("compensation:acc-new-id", args=())
cls.detail_url = reverse("compensation:acc-detail", args=(cls.eco_account.id,))
cls.log_url = reverse("compensation:acc-log", args=(cls.eco_account.id,))
cls.edit_url = reverse("compensation:acc-edit", args=(cls.eco_account.id,))
cls.remove_url = reverse("compensation:acc-remove", args=(cls.eco_account.id,))
cls.report_url = reverse("compensation:acc-report", args=(cls.eco_account.id,))
cls.state_new_url = reverse("compensation:acc-new-state", args=(cls.eco_account.id,))
cls.action_new_url = reverse("compensation:acc-new-action", args=(cls.eco_account.id,))
cls.deadline_new_url = reverse("compensation:acc-new-deadline", args=(cls.eco_account.id,))
cls.new_doc_url = reverse("compensation:acc-new-doc", args=(cls.eco_account.id,))
cls.state_remove_url = reverse("compensation:acc-state-remove", args=(cls.eco_account.id, cls.comp_state.id,))
cls.action_remove_url = reverse("compensation:acc-action-remove", args=(cls.eco_account.id, cls.comp_action.id,))
def test_logged_in_no_groups_shared(self):
""" Check correct status code for all requests
Assumption: User logged in and has no groups and data is shared
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
self.eco_account.share_with_list([self.superuser])
# Since the user has no groups, it does not matter that data has been shared. There SHOULD not be any difference
# to a user without access, since the important permissions are missing
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
]
fail_urls = [
self.new_url,
self.new_id_url,
self.log_url,
self.edit_url,
self.remove_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_no_groups_unshared(self):
""" Check correct status code for all requests
Assumption: User logged in and has no groups and data is shared
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
self.eco_account.share_with_list([])
# Since the user has no groups, it does not matter that data is unshared. There SHOULD not be any difference
# to a user having shared access, since all important permissions are missing
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
]
fail_urls = [
self.new_url,
self.new_id_url,
self.log_url,
self.edit_url,
self.remove_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
]
self.assert_url_success(client, success_urls)
self.assert_url_fail(client, fail_urls)
def test_logged_in_default_group_shared(self):
""" Check correct status code for all requests
Assumption: User logged in, is default group member and data is shared
--> Default group necessary since all base functionalities depend on this group membership
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([group])
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.eco_account.share_with_list([self.superuser])
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
self.new_url,
self.new_id_url,
self.edit_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
self.log_url,
self.remove_url,
]
self.assert_url_success(client, success_urls)
def test_logged_in_default_group_unshared(self):
""" Check correct status code for all requests
Assumption: User logged in, is default group member and data is NOT shared
--> Default group necessary since all base functionalities depend on this group membership
Returns:
"""
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([group])
self.eco_account.share_with_list([])
success_urls = [
self.index_url,
self.detail_url,
self.report_url,
self.new_id_url,
self.new_url,
]
fail_urls = [
self.edit_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_remove_url,
self.action_remove_url,
self.new_doc_url,
self.log_url,
self.remove_url,
]
self.assert_url_fail(client, fail_urls)
self.assert_url_success(client, success_urls)

View File

@@ -18,16 +18,24 @@ urlpatterns = [
path('<id>/log', log_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>/action/new', action_new_view, name='new-action'),
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>/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>/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>/report', report_view, name='report'),
# Documents
path('<id>/document/new/', new_document_view, name='new-doc'),
path('document/<doc_id>', get_document_view, name='get-doc'),
path('document/<doc_id>/remove/', remove_document_view, name='remove-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'),
]

View File

@@ -8,31 +8,42 @@ Created on: 24.08.21
from django.urls import path
from compensation.views.eco_account import *
app_name = "acc"
urlpatterns = [
path("", index_view, name="acc-index"),
path('new/', new_view, name='acc-new'),
path('new/id', new_id_view, name='acc-new-id'),
path('<id>', detail_view, name='acc-detail'),
path('<id>/log', log_view, name='acc-log'),
path('<id>/record', record_view, name='acc-record'),
path('<id>/report', report_view, name='acc-report'),
path('<id>/edit', edit_view, name='acc-edit'),
path('<id>/remove', remove_view, name='acc-remove'),
path('<id>/state/new', state_new_view, name='acc-new-state'),
path('<id>/action/new', action_new_view, name='acc-new-action'),
path('<id>/state/<state_id>/remove', state_remove_view, name='acc-state-remove'),
path('<id>/action/<action_id>/remove', action_remove_view, name='acc-action-remove'),
path('<id>/deadline/new', deadline_new_view, name="acc-new-deadline"),
path("", index_view, name="index"),
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>/report', report_view, name='report'),
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>/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>/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>/share/<token>', share_view, name='share'),
path('<id>/share', create_share_view, name='share-create'),
# Documents
path('<id>/document/new/', new_document_view, name='acc-new-doc'),
path('document/<doc_id>', get_document_view, name='acc-get-doc'),
path('document/<doc_id>/remove/', remove_document_view, name='acc-remove-doc'),
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'),
# Eco-account deductions
path('<id>/remove/<deduction_id>', deduction_remove_view, name='acc-remove-deduction'),
path('<id>/deduct/new', new_deduction_view, name='acc-new-deduction'),
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'),
]

View File

@@ -8,7 +8,9 @@ Created on: 24.08.21
from django.urls import path
from compensation.views.payment import *
app_name = "pay"
urlpatterns = [
path('<intervention_id>/new', new_payment_view, name='pay-new'),
path('<id>/remove', payment_remove_view, name='pay-remove'),
path('<id>/new', new_payment_view, name='new'),
path('<id>/remove/<payment_id>', payment_remove_view, name='remove'),
path('<id>/edit/<payment_id>', payment_edit_view, name='edit'),
]

View File

@@ -10,6 +10,6 @@ from django.urls import path, include
app_name = "compensation"
urlpatterns = [
path("", include("compensation.urls.compensation")),
path("acc/", include("compensation.urls.eco_account")),
path("pay/", include("compensation.urls.payment")),
path("acc/", include("compensation.urls.eco_account", namespace="acc")),
path("pay/", include("compensation.urls.payment", namespace="pay")),
]

View File

@@ -1,4 +1,5 @@
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
@@ -6,18 +7,23 @@ from django.utils.translation import gettext_lazy as _
from compensation.forms.forms import NewCompensationForm, EditCompensationForm
from compensation.forms.modalForms import NewStateModalForm, NewDeadlineModalForm, NewActionModalForm, \
NewCompensationDocumentForm
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 import RemoveModalForm, SimpleGeomForm
from konova.forms import RemoveModalForm, SimpleGeomForm, RemoveDeadlineModalForm, EditDocumentModalForm
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
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
@@ -64,6 +70,19 @@ def new_view(request: HttpRequest, intervention_id: str = None):
"""
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":
@@ -78,7 +97,7 @@ def new_view(request: HttpRequest, intervention_id: str = None):
comp.identifier
)
)
messages.success(request, _("Compensation {} added").format(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",)
@@ -129,6 +148,13 @@ def edit_view(request: HttpRequest, id: str):
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)
@@ -191,8 +217,15 @@ def detail_view(request: HttpRequest, id: str):
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,
@@ -255,7 +288,7 @@ def remove_view(request: HttpRequest, id: str):
form = RemoveModalForm(request.POST or None, instance=comp, request=request)
return form.process_request(
request=request,
msg_success=_("Compensation removed"),
msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier),
redirect_url=reverse("compensation:index"),
)
@@ -273,54 +306,52 @@ def new_document_view(request: HttpRequest, id: str):
"""
comp = get_object_or_404(Compensation, id=id)
form = NewCompensationDocumentForm(request.POST or None, request.FILES or None, instance=comp, request=request)
form = NewCompensationDocumentModalForm(request.POST or None, request.FILES or None, instance=comp, request=request)
return form.process_request(
request,
msg_success=_("Document added")
msg_success=DOCUMENT_ADDED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
def get_document_view(request: HttpRequest, doc_id: str):
@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)
user = request.user
instance = doc.instance
# File download only possible if related instance is shared with user
if not instance.users.filter(id=user.id):
messages.info(
request,
DATA_UNSHARED
)
return redirect("compensation:detail", id=instance.id)
return get_document(doc)
@login_required
@default_group_required
def remove_document_view(request: HttpRequest, doc_id: str):
@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,
@@ -328,6 +359,32 @@ def remove_document_view(request: HttpRequest, doc_id: str):
)
@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")
@@ -345,7 +402,8 @@ def state_new_view(request: HttpRequest, id: str):
form = NewStateModalForm(request.POST or None, instance=comp, request=request)
return form.process_request(
request,
msg_success=_("State added")
msg_success=COMPENSATION_STATE_ADDED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
)
@@ -366,7 +424,32 @@ def action_new_view(request: HttpRequest, id: str):
form = NewActionModalForm(request.POST or None, instance=comp, request=request)
return form.process_request(
request,
msg_success=_("Action added")
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"
)
@@ -387,7 +470,56 @@ def deadline_new_view(request: HttpRequest, id: str):
form = NewDeadlineModalForm(request.POST or None, instance=comp, request=request)
return form.process_request(
request,
msg_success=_("Deadline added")
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"
)
@@ -405,11 +537,37 @@ def state_remove_view(request: HttpRequest, id: str, state_id: str):
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
state = get_object_or_404(CompensationState, id=state_id)
form = RemoveModalForm(request.POST or None, instance=state, request=request)
form = RemoveCompensationStateModalForm(request.POST or None, instance=comp, state=state, request=request)
return form.process_request(
request,
msg_success=_("State removed")
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"
)
@@ -427,11 +585,13 @@ def action_remove_view(request: HttpRequest, id: str, action_id: str):
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
action = get_object_or_404(CompensationAction, id=action_id)
form = RemoveModalForm(request.POST or None, instance=action, request=request)
form = RemoveCompensationActionModalForm(request.POST or None, instance=comp, action=action, request=request)
return form.process_request(
request,
msg_success=_("Action removed")
msg_success=COMPENSATION_ACTION_REMOVED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
)
@@ -464,14 +624,12 @@ def report_view(request: HttpRequest, id: str):
instance=comp
)
parcels = comp.get_underlying_parcels()
qrcode_img = generate_qr_code(
request.build_absolute_uri(reverse("compensation:report", args=(id,))),
10
)
qrcode_img_lanis = generate_qr_code(
comp.get_LANIS_link(),
7
)
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")
@@ -479,8 +637,14 @@ def report_view(request: HttpRequest, id: str):
context = {
"obj": comp,
"qrcode": qrcode_img,
"qrcode_lanis": qrcode_img_lanis,
"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,

View File

@@ -16,20 +16,27 @@ 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, \
NewEcoAccountDocumentForm
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
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 import RemoveModalForm, SimpleGeomForm, NewDocumentForm, RecordModalForm
from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentModalForm, RecordModalForm, \
RemoveDeadlineModalForm, EditDocumentModalForm
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, \
CANCEL_ACC_RECORDED_OR_DEDUCTED
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
@@ -89,7 +96,7 @@ def new_view(request: HttpRequest):
)
)
messages.success(request, _("Eco-Account {} added").format(acc.identifier))
return redirect("compensation:acc-detail", id=acc.id)
return redirect("compensation:acc:detail", id=acc.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
@@ -139,6 +146,13 @@ def edit_view(request: HttpRequest, id: str):
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)
@@ -147,7 +161,7 @@ def edit_view(request: HttpRequest, id: str):
# 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)
return redirect("compensation:acc:detail", id=acc.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
@@ -254,19 +268,18 @@ def remove_view(request: HttpRequest, id: str):
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)
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"),
redirect_url=reverse("compensation:acc:index"),
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def deduction_remove_view(request: HttpRequest, id: str, deduction_id: str):
""" Renders a modal view for removing deductions
@@ -281,13 +294,45 @@ def deduction_remove_view(request: HttpRequest, id: str, deduction_id: str):
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 = RemoveModalForm(request.POST or None, instance=eco_deduction, request=request)
form = RemoveEcoAccountDeductionModalForm(request.POST or None, instance=acc, deduction=eco_deduction, request=request)
return form.process_request(
request=request,
msg_success=_("Deduction removed")
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"
)
@@ -357,7 +402,8 @@ def state_new_view(request: HttpRequest, id: str):
form = NewStateModalForm(request.POST or None, instance=acc, request=request)
return form.process_request(
request,
msg_success=_("State added")
msg_success=COMPENSATION_STATE_ADDED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@@ -378,7 +424,8 @@ def action_new_view(request: HttpRequest, id: str):
form = NewActionModalForm(request.POST or None, instance=acc, request=request)
return form.process_request(
request,
msg_success=_("Action added")
msg_success=COMPENSATION_ACTION_ADDED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@@ -396,11 +443,37 @@ def state_remove_view(request: HttpRequest, id: str, state_id: str):
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
state = get_object_or_404(CompensationState, id=state_id)
form = RemoveModalForm(request.POST or None, instance=state, request=request)
form = RemoveCompensationStateModalForm(request.POST or None, instance=acc, state=state, request=request)
return form.process_request(
request,
msg_success=_("State removed")
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"
)
@@ -418,11 +491,85 @@ def action_remove_view(request: HttpRequest, id: str, action_id: str):
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
action = get_object_or_404(CompensationAction, id=action_id)
form = RemoveModalForm(request.POST or None, instance=action, request=request)
form = RemoveCompensationActionModalForm(request.POST or None, instance=acc, action=action, request=request)
return form.process_request(
request,
msg_success=_("Action removed")
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"
)
@@ -443,7 +590,8 @@ def deadline_new_view(request: HttpRequest, id: str):
form = NewDeadlineModalForm(request.POST or None, instance=acc, request=request)
return form.process_request(
request,
msg_success=_("Deadline added")
msg_success=DEADLINE_ADDED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@@ -460,55 +608,78 @@ def new_document_view(request: HttpRequest, id: str):
"""
acc = get_object_or_404(EcoAccount, id=id)
form = NewEcoAccountDocumentForm(request.POST or None, request.FILES or None, instance=acc, request=request)
form = NewEcoAccountDocumentModalForm(request.POST or None, request.FILES or None, instance=acc, request=request)
return form.process_request(
request,
msg_success=_("Document added")
msg_success=DOCUMENT_ADDED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data",
)
@login_required
@default_group_required
def get_document_view(request: HttpRequest, doc_id: str):
@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)
user = request.user
instance = doc.instance
# File download only possible if related instance is shared with user
if not instance.users.filter(id=user.id):
messages.info(
request,
DATA_UNSHARED
)
return redirect("compensation:acc-detail", id=instance.id)
return get_document(doc)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def remove_document_view(request: HttpRequest, doc_id: str):
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,
@@ -518,7 +689,6 @@ def remove_document_view(request: HttpRequest, doc_id: str):
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def new_deduction_view(request: HttpRequest, id: str):
""" Renders a modal form view for creating deductions
@@ -530,10 +700,13 @@ def new_deduction_view(request: HttpRequest, id: str):
"""
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")
msg_success=DEDUCTION_ADDED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
)
@@ -566,18 +739,16 @@ def report_view(request:HttpRequest, id: str):
instance=acc
)
parcels = acc.get_underlying_parcels()
qrcode_img = generate_qr_code(
request.build_absolute_uri(reverse("ema:report", args=(id,))),
10
)
qrcode_img_lanis = generate_qr_code(
acc.get_LANIS_link(),
7
)
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().select_related("action_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()\
@@ -587,8 +758,14 @@ def report_view(request:HttpRequest, id: str):
context = {
"obj": acc,
"qrcode": qrcode_img,
"qrcode_lanis": qrcode_img_lanis,
"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,
@@ -631,8 +808,8 @@ def share_view(request: HttpRequest, id: str, token: str):
request,
_("{} has been shared with you").format(obj.identifier)
)
obj.share_with(user)
return redirect("compensation:acc-detail", id=id)
obj.share_with_user(user)
return redirect("compensation:acc:detail", id=id)
else:
messages.error(
request,

View File

@@ -5,54 +5,86 @@ Contact: michel.peltriaux@sgdnord.rlp.de
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
from compensation.forms.modalForms import NewPaymentForm, RemovePaymentModalForm, EditPaymentModalForm
from compensation.models import Payment
from intervention.models import Intervention
from konova.decorators import default_group_required
from konova.decorators import default_group_required, shared_access_required
from konova.forms import RemoveModalForm
from konova.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED, PAYMENT_EDITED
@login_required
@default_group_required
def new_payment_view(request: HttpRequest, intervention_id: str):
@shared_access_required(Intervention, "id")
def new_payment_view(request: HttpRequest, id: str):
""" Renders a modal view for adding new payments
Args:
request (HttpRequest): The incoming request
intervention_id (str): The intervention's id for which a new payment shall be added
id (str): The intervention's id for which a new payment shall be added
Returns:
"""
intervention = get_object_or_404(Intervention, id=intervention_id)
intervention = get_object_or_404(Intervention, id=id)
form = NewPaymentForm(request.POST or None, instance=intervention, request=request)
return form.process_request(
request,
msg_success=_("Payment added")
msg_success=PAYMENT_ADDED,
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
def payment_remove_view(request: HttpRequest, id: str):
@shared_access_required(Intervention, "id")
def payment_remove_view(request: HttpRequest, id: str, payment_id: str):
""" Renders a modal view for removing payments
Args:
request (HttpRequest): The incoming request
id (str): The payment's id
id (str): The intervention's id
payment_id (str): The payment's id
Returns:
"""
payment = get_object_or_404(Payment, id=id)
form = RemoveModalForm(request.POST or None, instance=payment, request=request)
intervention = get_object_or_404(Intervention, id=id)
payment = get_object_or_404(Payment, id=payment_id)
form = RemovePaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request)
return form.process_request(
request=request,
msg_success=_("Payment removed"),
msg_success=PAYMENT_REMOVED,
redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data"
)
@login_required
@default_group_required
@shared_access_required(Intervention, "id")
def payment_edit_view(request: HttpRequest, id: str, payment_id: str):
""" Renders a modal view for editing payments
Args:
request (HttpRequest): The incoming request
id (str): The intervention's id
payment_id (str): The payment's id
Returns:
"""
intervention = get_object_or_404(Intervention, id=id)
payment = get_object_or_404(Payment, id=payment_id)
form = EditPaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request)
return form.process_request(
request=request,
msg_success=PAYMENT_EDITED,
redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data"
)

View File

@@ -12,14 +12,15 @@ 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
from compensation.forms.forms import AbstractCompensationForm, CompensationResponsibleFormMixin, \
PikCompensationFormMixin
from ema.models import Ema, EmaDocument
from intervention.models import Responsibility
from konova.forms import SimpleGeomForm, NewDocumentForm
from intervention.models import Responsibility, Handler
from konova.forms import SimpleGeomForm, NewDocumentModalForm
from user.models import UserActionLogEntry
class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin, PikCompensationFormMixin):
""" Form for creating new EMA objects.
Inherits basic form fields from AbstractCompensationForm and additional from CompensationResponsibleFormMixin.
@@ -31,7 +32,9 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
"title",
"conservation_office",
"conservation_file_number",
"handler",
"is_pik",
"handler_type",
"handler_detail",
"comment",
]
@@ -53,9 +56,11 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
handler = self.cleaned_data.get("handler", None)
handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", 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
@@ -63,6 +68,10 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
# 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,
@@ -77,10 +86,11 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
created=action,
geometry=geometry,
comment=comment,
is_pik=is_pik,
)
# Add the creating user to the list of shared users
acc.share_with(user)
acc.share_with_user(user)
# Add the log entry to the main objects log list
acc.log.add(action)
@@ -105,10 +115,12 @@ class EditEmaForm(NewEmaForm):
form_data = {
"identifier": self.instance.identifier,
"title": self.instance.title,
"handler": self.instance.responsible.handler,
"handler_type": self.instance.responsible.handler.type,
"handler_detail": self.instance.responsible.handler.detail,
"conservation_office": self.instance.responsible.conservation_office,
"conservation_file_number": self.instance.responsible.conservation_file_number,
"comment": self.instance.comment,
"is_pik": self.instance.is_pik,
}
disabled_fields = []
self.load_initial_data(
@@ -121,10 +133,12 @@ class EditEmaForm(NewEmaForm):
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
handler = self.cleaned_data.get("handler", None)
handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", 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)
@@ -132,7 +146,9 @@ class EditEmaForm(NewEmaForm):
geometry = geom_form.save(action)
# Update responsible data
self.instance.responsible.handler = handler
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()
@@ -142,6 +158,7 @@ class EditEmaForm(NewEmaForm):
self.instance.title = title
self.instance.geometry = geometry
self.instance.comment = comment
self.instance.is_pik = is_pik
self.instance.modified = action
self.instance.save()
@@ -150,5 +167,5 @@ class EditEmaForm(NewEmaForm):
return self.instance
class NewEmaDocumentForm(NewDocumentForm):
class NewEmaDocumentModalForm(NewDocumentModalForm):
document_model = EmaDocument

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.1.3 on 2022-02-18 09:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0003_team'),
('ema', '0002_auto_20220114_0936'),
]
operations = [
migrations.AddField(
model_name='ema',
name='teams',
field=models.ManyToManyField(help_text='Teams having access (data shared with)', to='user.Team'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.3 on 2022-05-31 10:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ema', '0003_ema_teams'),
]
operations = [
migrations.AddField(
model_name='ema',
name='is_pik',
field=models.BooleanField(blank=True, default=False, help_text="Flag if compensation is a 'Produktonsintegrierte Kompensation'", null=True),
),
]

View File

@@ -11,16 +11,16 @@ from django.contrib import messages
from django.db import models
from django.db.models import QuerySet
from django.http import HttpRequest
from django.urls import reverse
from compensation.models import AbstractCompensation
from compensation.models import AbstractCompensation, PikMixin
from ema.managers import EmaManager
from ema.utils.quality import EmaQualityChecker
from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableObjectMixin, ShareableObjectMixin
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DOCUMENT_REMOVED_TEMPLATE
class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin):
class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, PikMixin):
"""
EMA = Ersatzzahlungsmaßnahme
(compensation actions from payments)
@@ -50,28 +50,6 @@ class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin):
self.identifier = new_id
super().save(*args, **kwargs)
def get_LANIS_link(self) -> str:
""" Generates a link for LANIS depending on the geometry
Returns:
"""
try:
geom = self.geometry.geom.transform(DEFAULT_SRID_RLP, clone=True)
x = geom.centroid.x
y = geom.centroid.y
zoom_lvl = 16
except AttributeError:
# If no geometry has been added, yet.
x = 1
y = 1
zoom_lvl = 6
return LANIS_LINK_TEMPLATE.format(
zoom_lvl,
x,
y,
)
def quality_check(self) -> EmaQualityChecker:
""" Quality check
@@ -119,6 +97,14 @@ class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin):
is_ready = is_recorded
return is_ready
def get_share_link(self):
""" Returns the share url for the object
Returns:
"""
return reverse("ema:share", args=(self.id, self.access_token))
class EmaDocument(AbstractDocument):
"""
@@ -134,7 +120,7 @@ class EmaDocument(AbstractDocument):
max_length=1000,
)
def delete(self, *args, **kwargs):
def delete(self, user=None, *args, **kwargs):
"""
Custom delete functionality for EcoAccountDocuments.
Removes the folder from the file system if there are no further documents for this entry.
@@ -156,6 +142,9 @@ class EmaDocument(AbstractDocument):
folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path)
if user:
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))
# Remove the file itself
super().delete(*args, **kwargs)

View File

@@ -6,6 +6,7 @@ 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 _
@@ -34,6 +35,11 @@ class EmaTable(BaseTable, TableRenderMixin):
orderable=True,
accessor="title",
)
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=True,
accessor="geometry",
)
r = tables.Column(
verbose_name=_("Recorded"),
orderable=True,
@@ -87,6 +93,28 @@ 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
@@ -101,9 +129,7 @@ class EmaTable(BaseTable, TableRenderMixin):
recorded = value is not None
tooltip = _("Not recorded yet")
if recorded:
value = value.timestamp
value = localtime(value)
on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
on = value.get_timestamp_str_formatted()
tooltip = _("Recorded on {} by {}").format(on, record.recorded.user)
html += self.render_bookmark(
tooltip=tooltip,
@@ -122,9 +148,7 @@ class EmaTable(BaseTable, TableRenderMixin):
"""
html = ""
has_access = value.filter(
id=self.user.id
).exists()
has_access = record.is_shared_with(self.user)
html += self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),

View File

@@ -1,4 +1,4 @@
{% load i18n l10n fontawesome_5 humanize %}
{% load i18n l10n fontawesome_5 humanize ksp_filters %}
<div id="actions" class="card">
<div class="card-header rlp-r">
<div class="row">
@@ -20,42 +20,51 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th class="w-25" scope="col">
{% trans 'Action type' %}
</th>
<th class="w-25" scope="col">
{% trans 'Action type details' %}
</th>
<th scope="col">
{% trans 'Amount' context 'Compensation' %}
</th>
<th scope="col">
{% trans 'Comment' %}
</th>
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
</tr>
</thead>
<tbody>
{% for action in obj.actions.all %}
<tr>
<td class="align-middle">
{{ action.action_type }}
</td>
<td class="align-middle">
<td class="">
{% for type in action.action_type.all %}
<div> {{type.parent.parent.long_name}} {% fa5_icon 'angle-right' %} {{type.parent.long_name}} {% fa5_icon 'angle-right' %} {{type.long_name}} </div>
<hr>
{% endfor %}
{% for detail in action.action_type_details.all %}
<div class="mb-2" title="{{detail}}">{{detail.long_name}}</div>
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
{% endfor %}
</td>
<td class="align-middle">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
<td class="align-middle">{{ action.comment|default_if_none:"" }}</td>
<td class="align-middle">
<td class="">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
<td class="">
<div class="scroll-150">
{{ action.comment }}
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'ema:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'ema:action-remove' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove action' %}">
{% fa5_icon 'trash' %}
</button>

View File

@@ -20,7 +20,7 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
@@ -33,8 +33,10 @@
<th scope="col">
{% trans 'Comment' %}
</th>
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
</tr>
</thead>
@@ -45,10 +47,17 @@
{% trans deadline.type_humanized %}
</td>
<td class="align-middle">{{ deadline.date|default_if_none:"---" }}</td>
<td class="align-middle">{{ deadline.comment }}</td>
<td>
<td class="align-middle">
<div class="scroll-150">
{{ deadline.comment }}
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'deadline-remove' deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove deadline' %}">
<button data-form-url="{% url 'ema:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'ema:deadline-remove' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove deadline' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -20,7 +20,7 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
@@ -28,10 +28,15 @@
{% trans 'Title' %}
</th>
<th scope="col">
{% trans 'Comment' %}
{% trans 'Created on' %}
</th>
<th scope="col">
{% trans 'Action' %}
{% trans 'Comment' %}
</th>
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
</tr>
</thead>
@@ -39,14 +44,24 @@
{% for doc in obj.documents.all %}
<tr>
<td class="align-middle">
<a href="{% url 'ema:get-doc' doc.id %}">
<a href="{% url 'ema:get-doc' obj.id doc.id %}">
{{ doc.title }}
</a>
</td>
<td class="align-middle">{{ doc.comment }}</td>
<td>
<td class="align-middle">
{{ doc.date_of_creation }}
</td>
<td class="align-middle">
<div class="scroll-150">
{{ doc.comment }}
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'ema:remove-doc' doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
<button data-form-url="{% url 'ema:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'ema:remove-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@@ -20,45 +20,46 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
{% if sum_before_states > sum_after_states %}
<div class="row alert alert-danger">
{% trans 'Missing surfaces according to states before: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
{% if sum_before_states > sum_after_states %}
<div class="alert alert-danger mb-0">
{% trans 'Missing surfaces according to states before: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th class="w-25" scope="col">
<th class="w-50" scope="col">
{% trans 'Biotope type' %}
</th>
<th class="w-25" scope="col">
{% trans 'Biotope additional type' %}
</th>
<th scope="col">
{% trans 'Surface' %}
</th>
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
</tr>
</thead>
<tbody>
{% for state in after_states %}
<tr>
<td class="align-middle">
{{ state.biotope_type }}
</td>
<td class="align-middle">
{% for biotope_extra in state.biotope_type_details.all %}
<div class="mb-2" title="{{ biotope_extra }}">
{{ biotope_extra.long_name }}
</div>
<td>
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
{% endfor %}
</td>
<td class="align-middle">{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle">
<td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'ema:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'ema:state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %}
</button>

View File

@@ -20,45 +20,46 @@
</div>
</div>
</div>
<div class="card-body scroll-300">
{% if sum_before_states < sum_after_states %}
<div class="row alert alert-danger">
{% trans 'Missing surfaces according to states after: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
{% if sum_before_states < sum_after_states %}
<div class="alert alert-danger mb-0">
{% trans 'Missing surfaces according to states after: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>
<tr>
<th class="w-25" scope="col">
<th class="w-50" scope="col">
{% trans 'Biotope type' %}
</th>
<th class="w-25" scope="col">
{% trans 'Biotope additional type' %}
</th>
<th scope="col">
{% trans 'Surface' %}
</th>
<th scope="col">
{% trans 'Action' %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
</span>
</th>
</tr>
</thead>
<tbody>
{% for state in before_states %}
<tr>
<td class="align-middle">
{{ state.biotope_type }}
</td>
<td class="align-middle">
{% for biotope_extra in state.biotope_type_details.all %}
<div class="mb-2" title="{{ biotope_extra }}">
{{ biotope_extra.long_name }}
</div>
<td>
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
{% endfor %}
</td>
<td class="align-middle">{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle">
<td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
<button data-form-url="{% url 'ema:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>
<button data-form-url="{% url 'ema:state-remove' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %}
</button>

View File

@@ -14,16 +14,16 @@
{% block body %}
<div id="detail-header" class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<h3>{% trans 'Payment funded compensation' %} <br> {{obj.identifier}}</h3>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'ema/detail/includes/controls.html' %}
</div>
</div>
<hr>
<div id="data" class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="table-container">
<table class="table table-hover">
<tr>
@@ -56,25 +56,38 @@
<th scope="row">{% trans 'Action handler' %}</th>
<td class="align-middle">{{obj.responsible.handler|default_if_none:""}}</td>
</tr>
<tr>
<th scope="row">{% trans 'Is PIK' %}</th>
<td class="align-middle">
{% if obj.is_pik %}
{% trans 'Yes' %}
{% else %}
{% trans 'No' %}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans 'Last modified' %}</th>
<td class="align-middle">
{% if obj.modified %}
{{obj.modified.timestamp|default_if_none:""|naturalday}}
{{obj.modified.timestamp|default_if_none:""}}
<br>
{{obj.modified.user.username}}
{% else %}
{{obj.created.timestamp|default_if_none:""|naturalday}}
{{obj.created.timestamp|default_if_none:""}}
<br>
{{obj.created.user.username}}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans 'Shared with' %}</th>
<td class="align-middle">
{% for user in obj.users.all %}
{% for team in obj.shared_teams %}
{% include 'user/includes/team_data_modal_button.html' %}
{% endfor %}
<hr>
{% for user in obj.user.all %}
{% include 'user/includes/contact_modal_button.html' %}
{% endfor %}
</td>
@@ -82,12 +95,14 @@
</table>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="row">
{% include 'map/geom_form.html' %}
<div class="col-sm-12">
{% include 'map/geom_form.html' %}
</div>
</div>
<div class="row">
{% include 'konova/includes/parcels.html' %}
{% include 'konova/includes/parcels/parcels.html' %}
</div>
<div class="row">
{% include 'konova/includes/comment_card.html' %}
@@ -95,26 +110,27 @@
</div>
</div>
<hr>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'ema/detail/includes/states-before.html' %}
<div id="related_data">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'ema/detail/includes/states-before.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'ema/detail/includes/states-after.html' %}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'ema/detail/includes/states-after.html' %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'ema/detail/includes/actions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'ema/detail/includes/deadlines.html' %}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'ema/detail/includes/actions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'ema/detail/includes/deadlines.html' %}
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'ema/detail/includes/documents.html' %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
{% include 'ema/detail/includes/documents.html' %}
</div>
</div>
</div>

View File

@@ -3,7 +3,7 @@
{% block body %}
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<h3>{% trans 'Report' %}</h3>
<h4>{{obj.identifier}}</h4>
<div class="table-container">
@@ -21,8 +21,14 @@
<td class="align-middle">{{obj.responsible.conservation_file_number|default_if_none:""}}</td>
</tr>
<tr>
<th scope="row">{% trans 'Action handler' %}</th>
<td class="align-middle">{{obj.responsible.handler|default_if_none:""}}</td>
<th scope="row">{% trans 'Is PIK' %}</th>
<td class="align-middle">
{% if obj.is_pik %}
{% trans 'Yes' %}
{% else %}
{% trans 'No' %}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans 'Last modified' %}</th>
@@ -37,22 +43,17 @@
{% include 'compensation/detail/compensation/includes/states-after.html' %}
{% include 'compensation/detail/compensation/includes/actions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
<div class="row">
{% include 'map/geom_form.html' %}
<div class="col-sm-12">
{% include 'map/geom_form.html' %}
</div>
</div>
<div class="row">
{% include 'konova/includes/parcels.html' %}
{% include 'konova/includes/parcels/parcels.html' %}
</div>
<div class="row">
<div class="col-sm-6 col-md-6 col-lg-6">
<h4>{% trans 'Open in browser' %}</h4>
{{ qrcode|safe }}
</div>
<div class="col-sm-6 col-md-6 col-lg-6">
<h4>{% trans 'View in LANIS' %}</h4>
{{ qrcode_lanis|safe }}
</div>
{% include 'konova/includes/report/qrcodes.html' %}
</div>
</div>

View File

@@ -5,14 +5,15 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 26.10.21
"""
from django.db.models import Q
from django.urls import reverse
from django.test.client import Client
from compensation.tests.test_views import CompensationViewTestCase
from compensation.tests.compensation.test_views import CompensationViewTestCase
from ema.models import Ema
from intervention.models import Responsibility
from konova.models import Geometry
from konova.models import Geometry, Deadline, DeadlineType
from konova.settings import DEFAULT_GROUP, ETS_GROUP
from user.models import UserActionLogEntry
@@ -30,42 +31,59 @@ class EmaViewTestCase(CompensationViewTestCase):
def setUpTestData(cls) -> None:
super().setUpTestData()
def setUp(self) -> None:
super().setUp()
# Create dummy data and related objects, like states or actions
cls.create_dummy_data()
state = cls.create_dummy_states()
action = cls.create_dummy_action()
cls.ema.before_states.set([state])
cls.ema.after_states.set([state])
cls.ema.actions.set([action])
self.create_dummy_data()
state = self.create_dummy_states()
action = self.create_dummy_action()
self.ema.before_states.set([state])
self.ema.after_states.set([state])
self.ema.actions.set([action])
# Prepare urls
cls.index_url = reverse("ema:index", args=())
cls.new_url = reverse("ema:new", args=())
cls.new_id_url = reverse("ema:new-id", args=())
cls.detail_url = reverse("ema:detail", args=(cls.ema.id,))
cls.log_url = reverse("ema:log", args=(cls.ema.id,))
cls.edit_url = reverse("ema:edit", args=(cls.ema.id,))
cls.remove_url = reverse("ema:remove", args=(cls.ema.id,))
cls.share_url = reverse("ema:share", args=(cls.ema.id, cls.ema.access_token,))
cls.share_create_url = reverse("ema:share-create", args=(cls.ema.id,))
cls.record_url = reverse("ema:record", args=(cls.ema.id,))
cls.report_url = reverse("ema:report", args=(cls.ema.id,))
cls.new_doc_url = reverse("ema:new-doc", args=(cls.ema.id,))
cls.state_new_url = reverse("ema:new-state", args=(cls.ema.id,))
cls.action_new_url = reverse("ema:new-action", args=(cls.ema.id,))
cls.deadline_new_url = reverse("ema:new-deadline", args=(cls.ema.id,))
cls.state_remove_url = reverse("ema:state-remove", args=(cls.ema.id, state.id,))
cls.action_remove_url = reverse("ema:action-remove", args=(cls.ema.id, action.id,))
self.index_url = reverse("ema:index", args=())
self.new_url = reverse("ema:new", args=())
self.new_id_url = reverse("ema:new-id", args=())
self.detail_url = reverse("ema:detail", args=(self.ema.id,))
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.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,))
@classmethod
def create_dummy_data(cls):
self.state_new_url = reverse("ema:new-state", args=(self.ema.id,))
self.state_edit_url = reverse("ema:state-edit", args=(self.ema.id, state.id))
self.state_remove_url = reverse("ema:state-remove", args=(self.ema.id, state.id,))
self.action_new_url = reverse("ema:new-action", args=(self.ema.id,))
self.action_edit_url = reverse("ema:action-edit", args=(self.ema.id, action.id))
self.action_remove_url = reverse("ema:action-remove", args=(self.ema.id, action.id,))
self.deadline = Deadline.objects.get_or_create(
type=DeadlineType.FINISHED,
date="2020-01-01",
comment="TESTCOMMENT",
)[0]
self.ema.deadlines.add(self.deadline)
self.deadline_new_url = reverse("ema:new-deadline", args=(self.ema.id,))
self.deadline_edit_url = reverse("ema:deadline-edit", args=(self.ema.id, self.deadline.id))
self.deadline_remove_url = reverse("ema:deadline-remove", args=(self.ema.id, self.deadline.id))
def create_dummy_data(self):
# Create dummy data
# Create log entry
action = UserActionLogEntry.get_created_action(cls.superuser)
action = UserActionLogEntry.get_created_action(self.superuser)
# Create responsible data object
responsibility_data = Responsibility.objects.create()
responsibility_data = Responsibility.objects.create(
handler=self.handler
)
geometry = Geometry.objects.create()
cls.ema = Ema.objects.create(
self.ema = Ema.objects.create(
identifier="TEST",
title="Test_title",
created=action,
@@ -94,7 +112,7 @@ class EmaViewTestCase(CompensationViewTestCase):
# Sharing does not have any effect in here, since the default group will prohibit further functionality access
# to this user
self.ema.share_with_list([self.superuser])
self.ema.share_with_user_list([self.superuser])
success_urls = [
self.index_url,
@@ -106,10 +124,14 @@ class EmaViewTestCase(CompensationViewTestCase):
self.new_id_url,
self.edit_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_remove_url,
self.state_edit_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.action_edit_url,
self.action_remove_url,
self.action_new_url,
self.new_doc_url,
self.log_url,
self.remove_url,
@@ -140,7 +162,7 @@ class EmaViewTestCase(CompensationViewTestCase):
# Sharing does not have any effect in here, since the default group will prohibit further functionality access
# to this user
self.ema.share_with_list([])
self.ema.share_with_user_list([])
success_urls = [
self.index_url,
@@ -152,10 +174,14 @@ class EmaViewTestCase(CompensationViewTestCase):
self.new_id_url,
self.edit_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_edit_url,
self.state_remove_url,
self.action_new_url,
self.action_edit_url,
self.action_remove_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.new_doc_url,
self.log_url,
self.remove_url,
@@ -179,7 +205,7 @@ class EmaViewTestCase(CompensationViewTestCase):
groups = self.groups.filter(Q(name=ETS_GROUP)|Q(name=DEFAULT_GROUP))
self.superuser.groups.set(groups)
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.ema.share_with_list([self.superuser])
self.ema.share_with_user_list([self.superuser])
success_urls = [
self.index_url,
@@ -189,9 +215,13 @@ class EmaViewTestCase(CompensationViewTestCase):
self.new_id_url,
self.edit_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_edit_url,
self.state_remove_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.action_new_url,
self.action_edit_url,
self.action_remove_url,
self.new_doc_url,
self.log_url,
@@ -215,7 +245,7 @@ class EmaViewTestCase(CompensationViewTestCase):
groups = self.groups.filter(Q(name=ETS_GROUP)|Q(name=DEFAULT_GROUP))
self.superuser.groups.set(groups)
# Sharing is inherited by base intervention for compensation. Therefore configure the interventions share state
self.ema.share_with_list([])
self.ema.share_with_user_list([])
success_urls = [
self.index_url,
@@ -227,9 +257,13 @@ class EmaViewTestCase(CompensationViewTestCase):
fail_urls = [
self.edit_url,
self.state_new_url,
self.action_new_url,
self.deadline_new_url,
self.state_edit_url,
self.state_remove_url,
self.deadline_new_url,
self.deadline_edit_url,
self.deadline_remove_url,
self.action_new_url,
self.action_edit_url,
self.action_remove_url,
self.new_doc_url,
self.log_url,

192
ema/tests/test_workflow.py Normal file
View File

@@ -0,0 +1,192 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 08.02.22
"""
import datetime
from django.contrib.gis.geos import MultiPolygon
from django.core.exceptions import ObjectDoesNotExist
from django.urls import reverse
from ema.models import Ema
from konova.settings import ETS_GROUP
from konova.tests.test_views import BaseWorkflowTestCase
from user.models import UserAction
class EmaWorkflowTestCase(BaseWorkflowTestCase):
ema = None
@classmethod
def setUpTestData(cls) -> None:
super().setUpTestData()
def setUp(self) -> None:
super().setUp()
# Create a fresh dummy (non-valid) compensation before each test
self.ema = self.create_dummy_ema()
def test_new(self):
""" Test the creation of an Ema
Returns:
"""
self.superuser.groups.add(self.groups.get(name=ETS_GROUP))
# Prepare url and form data to be posted
new_url = reverse("ema:new")
test_id = self.create_dummy_string()
test_title = self.create_dummy_string()
test_geom = self.create_dummy_geometry()
geom_json = self.create_geojson(test_geom)
test_conservation_office = self.get_conservation_office_code()
post_data = {
"identifier": test_id,
"title": test_title,
"geom": geom_json,
"conservation_office": test_conservation_office.id
}
self.client_user.post(new_url, post_data)
try:
ema = Ema.objects.get(
identifier=test_id
)
except ObjectDoesNotExist:
self.fail(msg="Ema not created")
self.assertEqual(ema.identifier, test_id)
self.assertEqual(ema.title, test_title)
self.assert_equal_geometries(ema.geometry.geom, test_geom)
self.assertEqual(ema.log.count(), 1)
# Expect logs to be set
self.assertEqual(ema.log.count(), 1)
self.assertEqual(ema.log.first().action, UserAction.CREATED)
def test_edit(self):
""" Checks that the editing of an Ema works
Returns:
"""
self.superuser.groups.add(self.groups.get(name=ETS_GROUP))
self.ema.users.add(self.superuser)
url = reverse("ema:edit", args=(self.ema.id,))
self.ema = self.fill_out_ema(self.ema)
pre_edit_log_count = self.ema.log.count()
new_title = self.create_dummy_string()
new_identifier = self.create_dummy_string()
new_comment = self.create_dummy_string()
new_geometry = MultiPolygon(srid=4326) # Create an empty geometry
test_conservation_office = self.get_conservation_office_code()
check_on_elements = {
self.ema.title: new_title,
self.ema.identifier: new_identifier,
self.ema.comment: new_comment,
}
for k, v in check_on_elements.items():
self.assertNotEqual(k, v)
post_data = {
"identifier": new_identifier,
"title": new_title,
"comment": new_comment,
"geom": new_geometry.geojson,
"conservation_office": test_conservation_office.id
}
self.client_user.post(url, post_data)
self.ema.refresh_from_db()
check_on_elements = {
self.ema.title: new_title,
self.ema.identifier: new_identifier,
self.ema.comment: new_comment,
}
for k, v in check_on_elements.items():
self.assertEqual(k, v)
self.assert_equal_geometries(self.ema.geometry.geom, new_geometry)
# Expect logs to be set
self.assertEqual(pre_edit_log_count + 1, self.ema.log.count())
self.assertEqual(self.ema.log.first().action, UserAction.EDITED)
def test_non_editable_after_recording(self):
""" Tests that the EMA can not be edited after being recorded
User must be redirected to another page
Returns:
"""
self.superuser.groups.add(self.groups.get(name=ETS_GROUP))
self.assertIsNotNone(self.ema)
self.ema.share_with_user(self.superuser)
self.assertFalse(self.ema.is_recorded)
edit_url = reverse("ema:edit", args=(self.ema.id,))
response = self.client_user.get(edit_url)
has_redirect = response.status_code == 302
self.assertFalse(has_redirect)
self.ema.set_recorded(self.superuser)
self.assertTrue(self.ema.is_recorded)
edit_url = reverse("ema:edit", args=(self.ema.id,))
response = self.client_user.get(edit_url)
has_redirect = response.status_code == 302
self.assertTrue(has_redirect)
self.ema.set_unrecorded(self.superuser)
def test_recordability(self):
"""
This tests if the recordability of the Ema is triggered by the quality of it's data (e.g. not all fields filled)
Returns:
"""
# Add proper privilege for the user
self.superuser.groups.add(self.groups.get(name=ETS_GROUP))
self.ema.users.add(self.superuser)
pre_record_log_count = self.ema.log.count()
# Prepare url and form data
record_url = reverse("ema:record", args=(self.ema.id,))
post_data = {
"confirm": True,
}
# Make sure the ema is not recorded
self.assertIsNone(self.ema.recorded)
# Run the request --> expect fail, since the Ema is not valid, yet
self.client_user.post(record_url, post_data)
# Check that the Ema is still not recorded
self.assertIsNone(self.ema.recorded)
# Now fill out the data for a compensation
self.ema = self.fill_out_ema(self.ema)
# Rerun the request
self.client_user.post(record_url, post_data)
# Expect the Ema now to be recorded
# Attention: We can only test the date part of the timestamp,
# since the delay in microseconds would lead to fail
self.ema.refresh_from_db()
recorded = self.ema.recorded
self.assertIsNotNone(recorded)
self.assertEqual(self.superuser, recorded.user)
self.assertEqual(UserAction.RECORDED, recorded.action)
self.assertEqual(datetime.date.today(), recorded.timestamp.date())
# Expect the user action to be in the log
self.assertIn(recorded, self.ema.log.all())
self.assertEqual(pre_record_log_count + 1, self.ema.log.count())

View File

@@ -19,18 +19,26 @@ urlpatterns = [
path('<id>/remove', remove_view, name='remove'),
path('<id>/record', record_view, name='record'),
path('<id>/report', report_view, name='report'),
path('<id>/state/new', state_new_view, name='new-state'),
path('<id>/action/new', action_new_view, name='new-action'),
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>/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>/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>/share/<token>', share_view, name='share'),
path('<id>/share', create_share_view, name='share-create'),
# Documents
# Document remove route can be found in konova/urls.py
path('<id>/document/new/', document_new_view, name='new-doc'),
path('document/<doc_id>', get_document_view, name='get-doc'),
path('document/<doc_id>/remove/', remove_document_view, name='remove-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'),
]

View File

@@ -6,20 +6,27 @@ 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
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, NewEmaDocumentForm
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 import RemoveModalForm, SimpleGeomForm, RecordModalForm
from konova.forms import RemoveModalForm, SimpleGeomForm, RecordModalForm, RemoveDeadlineModalForm, \
EditDocumentModalForm
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
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
@@ -206,6 +213,13 @@ def edit_view(request: HttpRequest, id: str):
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)
@@ -290,7 +304,8 @@ def state_new_view(request: HttpRequest, id: str):
form = NewStateModalForm(request.POST or None, instance=ema, request=request)
return form.process_request(
request,
msg_success=_("State added")
msg_success=COMPENSATION_STATE_ADDED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
)
@@ -311,7 +326,32 @@ def action_new_view(request: HttpRequest, id: str):
form = NewActionModalForm(request.POST or None, instance=ema, request=request)
return form.process_request(
request,
msg_success=_("Action added")
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"
)
@@ -332,7 +372,8 @@ def deadline_new_view(request: HttpRequest, id: str):
form = NewDeadlineModalForm(request.POST or None, instance=ema, request=request)
return form.process_request(
request,
msg_success=_("Deadline added")
msg_success=DEADLINE_ADDED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
)
@@ -349,54 +390,78 @@ def document_new_view(request: HttpRequest, id: str):
"""
ema = get_object_or_404(Ema, id=id)
form = NewEmaDocumentForm(request.POST or None, request.FILES or None, instance=ema, request=request)
form = NewEmaDocumentModalForm(request.POST or None, request.FILES or None, instance=ema, request=request)
return form.process_request(
request,
msg_success=_("Document added")
msg_success=DOCUMENT_ADDED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
)
@login_required
@conservation_office_group_required
def get_document_view(request: HttpRequest, doc_id: str):
@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)
user = request.user
instance = doc.instance
# File download only possible if related instance is shared with user
if not instance.users.filter(id=user.id):
messages.info(
request,
DATA_UNSHARED
)
return redirect("ema:detail", id=instance.id)
return get_document(doc)
@login_required
@conservation_office_group_required
def remove_document_view(request: HttpRequest, doc_id: str):
@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,
@@ -418,11 +483,37 @@ def state_remove_view(request: HttpRequest, id: str, state_id: str):
Returns:
"""
ema = get_object_or_404(Ema, id=id)
state = get_object_or_404(CompensationState, id=state_id)
form = RemoveModalForm(request.POST or None, instance=state, request=request)
form = RemoveCompensationStateModalForm(request.POST or None, instance=ema, state=state, request=request)
return form.process_request(
request,
msg_success=_("State removed")
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"
)
@@ -440,11 +531,13 @@ def action_remove_view(request: HttpRequest, id: str, action_id: str):
Returns:
"""
ema = get_object_or_404(Ema, id=id)
action = get_object_or_404(CompensationAction, id=action_id)
form = RemoveModalForm(request.POST or None, instance=action, request=request)
form = RemoveCompensationActionModalForm(request.POST or None, instance=ema, action=action, request=request)
return form.process_request(
request,
msg_success=_("Action removed")
msg_success=COMPENSATION_ACTION_REMOVED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
)
@@ -477,14 +570,12 @@ def report_view(request:HttpRequest, id: str):
instance=ema,
)
parcels = ema.get_underlying_parcels()
qrcode_img = generate_qr_code(
request.build_absolute_uri(reverse("ema:report", args=(id,))),
10
)
qrcode_img_lanis = generate_qr_code(
ema.get_LANIS_link(),
7
)
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")
@@ -492,8 +583,14 @@ def report_view(request:HttpRequest, id: str):
context = {
"obj": ema,
"qrcode": qrcode_img,
"qrcode_lanis": qrcode_img_lanis,
"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,
@@ -535,7 +632,7 @@ def share_view(request: HttpRequest, id: str, token: str):
request,
_("{} has been shared with you").format(obj.identifier)
)
obj.share_with(user)
obj.share_with_user(user)
return redirect("ema:detail", id=id)
else:
messages.error(
@@ -564,4 +661,52 @@ def create_share_view(request: HttpRequest, id: str):
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"
)

View File

@@ -25,12 +25,14 @@ class InterventionAdmin(BaseObjectAdmin):
"checked",
"recorded",
"users",
"geometry",
]
def get_readonly_fields(self, request, obj=None):
return super().get_readonly_fields(request, obj) + [
"checked",
"recorded",
"geometry",
]

View File

@@ -7,6 +7,8 @@ Created on: 02.12.20
"""
from dal import autocomplete
from django import forms
from konova.utils.message_templates import EDITED_GENERAL_DATA
from user.models import User
from django.db import transaction
from django.urls import reverse, reverse_lazy
@@ -14,9 +16,9 @@ from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_PROCESS_TYPE_ID, CODELIST_LAW_ID, \
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID
from intervention.inputs import GenerateInput
from intervention.models import Intervention, Legal, Responsibility
from intervention.models import Intervention, Legal, Responsibility, Handler
from konova.forms import BaseForm, SimpleGeomForm
from user.models import UserActionLogEntry
@@ -136,12 +138,29 @@ class NewInterventionForm(BaseForm):
}
)
)
handler = forms.CharField(
label=_("Intervention handler"),
handler_type = forms.ModelChoiceField(
label=_("Intervention handler type"),
label_suffix="",
help_text=_("What type of handler is responsible for the intervention?"),
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=_("Intervention handler detail"),
label_suffix="",
max_length=255,
required=False,
help_text=_("Who performs the intervention"),
help_text=_("Detail input on the handler"),
widget=forms.TextInput(
attrs={
"placeholder": _("Company Mustermann"),
@@ -149,6 +168,7 @@ class NewInterventionForm(BaseForm):
}
)
)
registration_date = forms.DateField(
label=_("Registration date"),
label_suffix=_(""),
@@ -196,6 +216,10 @@ class NewInterventionForm(BaseForm):
identifier = tmp_intervention.generate_new_identifier()
self.initialize_form_field("identifier", identifier)
def is_valid(self):
super_valid_result = super().is_valid()
return super_valid_result
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
@@ -203,7 +227,8 @@ class NewInterventionForm(BaseForm):
title = self.cleaned_data.get("title", None)
_type = self.cleaned_data.get("type", None)
laws = self.cleaned_data.get("laws", None)
handler = self.cleaned_data.get("handler", None)
handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", None)
registration_office = self.cleaned_data.get("registration_office", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
@@ -224,6 +249,10 @@ class NewInterventionForm(BaseForm):
# Then add the M2M laws to the object
legal_data.laws.set(laws)
handler = Handler.objects.create(
type=handler_type,
detail=handler_detail
)
# Create responsible data object
responsibility_data = Responsibility.objects.create(
registration_office=registration_office,
@@ -251,7 +280,7 @@ class NewInterventionForm(BaseForm):
intervention.log.add(action)
# Add the performing user as the first user having access to the data
intervention.share_with(user)
intervention.share_with_user(user)
return intervention
@@ -282,7 +311,8 @@ class EditInterventionForm(NewInterventionForm):
"title": self.instance.title,
"type": self.instance.legal.process_type,
"laws": list(self.instance.legal.laws.values_list("id", flat=True)),
"handler": self.instance.responsible.handler,
"handler_type": self.instance.responsible.handler.type,
"handler_detail": self.instance.responsible.handler.detail,
"registration_office": self.instance.responsible.registration_office,
"registration_file_number": self.instance.responsible.registration_file_number,
"conservation_office": self.instance.responsible.conservation_office,
@@ -311,7 +341,8 @@ class EditInterventionForm(NewInterventionForm):
title = self.cleaned_data.get("title", None)
process_type = self.cleaned_data.get("type", None)
laws = self.cleaned_data.get("laws", None)
handler = self.cleaned_data.get("handler", None)
handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", None)
registration_office = self.cleaned_data.get("registration_office", None)
registration_file_number = self.cleaned_data.get("registration_file_number", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
@@ -326,14 +357,17 @@ class EditInterventionForm(NewInterventionForm):
self.instance.legal.laws.set(laws)
self.instance.legal.save()
self.instance.responsible.handler = handler
self.instance.responsible.handler.type = handler_type
self.instance.responsible.handler.detail = handler_detail
self.instance.responsible.handler.save()
self.instance.responsible.registration_office = registration_office
self.instance.responsible.registration_file_number = registration_file_number
self.instance.responsible.conservation_office = conservation_office
self.instance.responsible.conservation_file_number = conservation_file_number
self.instance.responsible.save()
user_action = UserActionLogEntry.get_edited_action(user)
user_action = self.instance.mark_as_edited(user, edit_comment=EDITED_GENERAL_DATA)
geometry = geom_form.save(user_action)
self.instance.geometry = geometry
@@ -347,8 +381,5 @@ class EditInterventionForm(NewInterventionForm):
self.instance.modified = user_action
self.instance.save()
# Uncheck and unrecord intervention due to changed data
self.instance.mark_as_edited(user)
return self.instance

View File

@@ -6,16 +6,20 @@ Created on: 27.09.21
"""
from dal import autocomplete
from user.models import User
from django.core.exceptions import ObjectDoesNotExist
from konova.utils.message_templates import DEDUCTION_ADDED, REVOCATION_ADDED, DEDUCTION_REMOVED, DEDUCTION_EDITED, \
REVOCATION_EDITED, ENTRY_REMOVE_MISSING_PERMISSION
from user.models import User, Team
from user.models import UserActionLogEntry
from django.db import transaction
from django import forms
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from compensation.models import EcoAccount
from compensation.models import EcoAccount, EcoAccountDeduction
from intervention.inputs import TextToClipboardInput
from intervention.models import Intervention, InterventionDocument
from konova.forms import BaseModalForm, NewDocumentForm
from intervention.models import Intervention, InterventionDocument, RevocationDocument
from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm
from konova.utils.general import format_german_float
from konova.utils.user_checks import is_default_group_only
@@ -33,7 +37,21 @@ class ShareModalForm(BaseModalForm):
}
)
)
user_select = forms.ModelMultipleChoiceField(
teams = forms.ModelMultipleChoiceField(
label=_("Add team to share with"),
label_suffix="",
help_text=_("Multiple selection possible - You can only select teams which do not already have access."),
required=False,
queryset=Team.objects.all(),
widget=autocomplete.ModelSelect2Multiple(
url="share-team-autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
"data-minimum-input-length": 3,
},
),
)
users = forms.ModelMultipleChoiceField(
label=_("Add user to share with"),
label_suffix="",
help_text=_("Multiple selection possible - You can only select users which do not already have access. Enter the full username."),
@@ -45,21 +63,8 @@ class ShareModalForm(BaseModalForm):
"data-placeholder": _("Click for selection"),
"data-minimum-input-length": 3,
},
forward=["users"]
),
)
users = forms.MultipleChoiceField(
label=_("Shared with"),
label_suffix="",
required=True,
help_text=_("Remove check to remove access for this user"),
widget=forms.CheckboxSelectMultiple(
attrs={
"class": "list-unstyled",
}
),
choices=[]
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -73,6 +78,48 @@ class ShareModalForm(BaseModalForm):
self._init_fields()
def _user_team_valid(self):
""" Checks whether users and teams have been removed by the user and if the user is allowed to do so or not
Returns:
"""
users = self.cleaned_data.get("users", User.objects.none())
teams = self.cleaned_data.get("teams", Team.objects.none())
_is_valid = True
if is_default_group_only(self.user):
shared_users = self.instance.shared_users
shared_teams = self.instance.shared_teams
shared_users_are_removed = not set(shared_users).issubset(users)
shared_teams_are_removed = not set(shared_teams).issubset(teams)
if shared_users_are_removed:
self.add_error(
"users",
ENTRY_REMOVE_MISSING_PERMISSION
)
_is_valid = False
if shared_teams_are_removed:
self.add_error(
"teams",
ENTRY_REMOVE_MISSING_PERMISSION
)
_is_valid = False
return _is_valid
def is_valid(self):
""" Extended validity check
Returns:
"""
super_valid = super().is_valid()
user_team_valid = self._user_team_valid()
_is_valid = super_valid and user_team_valid
return _is_valid
def _init_fields(self):
""" Wraps initializing of fields
@@ -80,43 +127,21 @@ class ShareModalForm(BaseModalForm):
"""
# Initialize share_link field
url_name = f"{self.instance._meta.app_label}:share"
self.share_link = self.request.build_absolute_uri(
reverse(url_name, args=(self.instance.id, self.instance.access_token,))
)
share_link = self.instance.get_share_link()
self.share_link = self.request.build_absolute_uri(share_link)
self.initialize_form_field(
"url",
self.share_link
)
# Initialize users field
# Disable field if user is not in registration or conservation group
if is_default_group_only(self.request.user):
self.disable_form_field("users")
self._add_user_choices_to_field()
def _add_user_choices_to_field(self):
""" Transforms the instance's sharing users into a list for the form field
Returns:
"""
users = self.instance.users.all()
choices = []
for n in users:
choices.append(
(n.id, n.username)
)
self.fields["users"].choices = choices
u_ids = list(users.values_list("id", flat=True))
self.initialize_form_field(
"users",
u_ids
)
form_data = {
"teams": self.instance.teams.all(),
"users": self.instance.users.all(),
}
self.load_initial_data(form_data)
def save(self):
self.instance.update_sharing_user(self)
self.instance.update_shared_access(self)
class NewRevocationModalForm(BaseModalForm):
@@ -158,6 +183,7 @@ class NewRevocationModalForm(BaseModalForm):
}
)
)
document_model = RevocationDocument
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -169,10 +195,51 @@ class NewRevocationModalForm(BaseModalForm):
def save(self):
revocation = self.instance.add_revocation(self)
self.instance.mark_as_edited(self.user, self.request)
self.instance.mark_as_edited(self.user, self.request, edit_comment=REVOCATION_ADDED)
return revocation
class EditRevocationModalForm(NewRevocationModalForm):
revocation = None
def __init__(self, *args, **kwargs):
self.revocation = kwargs.pop("revocation", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit revocation")
try:
doc = self.revocation.document.file
except ObjectDoesNotExist:
doc = None
form_data = {
"date": str(self.revocation.date),
"file": doc,
"comment": self.revocation.comment,
}
self.load_initial_data(form_data)
def save(self):
revocation = self.instance.edit_revocation(self)
self.instance.mark_as_edited(self.user, self.request, edit_comment=REVOCATION_EDITED)
return revocation
class RemoveRevocationModalForm(RemoveModalForm):
""" Removing modal form for Revocation
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
revocation = None
def __init__(self, *args, **kwargs):
revocation = kwargs.pop("revocation", None)
self.revocation = revocation
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_revocation(self)
class CheckModalForm(BaseModalForm):
""" The modal form for running a check on interventions and their compensations
@@ -224,7 +291,9 @@ class CheckModalForm(BaseModalForm):
Returns:
"""
comps = self.instance.compensations.all()
comps = self.instance.compensations.filter(
deleted=None,
)
comps_valid = True
for comp in comps:
checker = comp.quality_check()
@@ -333,6 +402,21 @@ class NewDeductionModalForm(BaseModalForm):
else:
raise NotImplementedError
def _get_available_surface(self, acc):
""" Calculates how much available surface is left on the account
Args:
acc (EcoAccount):
Returns:
"""
# Calculate valid surface
deductable_surface = acc.deductable_surface
sum_surface_deductions = acc.get_deductions_surface()
rest_surface = deductable_surface - sum_surface_deductions
return rest_surface
def is_valid(self):
""" Custom validity check
@@ -343,18 +427,24 @@ class NewDeductionModalForm(BaseModalForm):
"""
super_result = super().is_valid()
acc = self.cleaned_data["account"]
intervention = self.cleaned_data["intervention"]
objects_valid = True
if not acc.recorded:
self.add_error(
"account",
_("Eco-account {} is not recorded yet. You can only deduct from recorded accounts.").format(acc.identifier)
)
return False
objects_valid = False
# Calculate valid surface
deductable_surface = acc.deductable_surface
sum_surface_deductions = acc.get_deductions_surface()
rest_surface = deductable_surface - sum_surface_deductions
if intervention.is_recorded:
self.add_error(
"intervention",
_("Intervention {} is currently recorded. To change any data on it, the entry must be unrecorded.").format(intervention.identifier)
)
objects_valid = False
rest_surface = self._get_available_surface(acc)
form_surface = float(self.cleaned_data["surface"])
is_valid_surface = form_surface <= rest_surface
if not is_valid_surface:
@@ -366,13 +456,102 @@ class NewDeductionModalForm(BaseModalForm):
format_german_float(rest_surface),
),
)
return is_valid_surface and super_result
return is_valid_surface and objects_valid and super_result
def __create_deduction(self):
""" Creates the deduction
Returns:
"""
with transaction.atomic():
user_action_create = UserActionLogEntry.get_created_action(self.user)
deduction = EcoAccountDeduction.objects.create(
intervention=self.cleaned_data["intervention"],
account=self.cleaned_data["account"],
surface=self.cleaned_data["surface"],
created=user_action_create,
)
return deduction
def save(self):
deduction = self.instance.add_deduction(self)
self.instance.mark_as_edited(self.user, self.request, reset_recorded=False)
deduction = self.__create_deduction()
self.cleaned_data["intervention"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED)
self.cleaned_data["account"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED)
return deduction
class NewInterventionDocumentForm(NewDocumentForm):
class EditEcoAccountDeductionModalForm(NewDeductionModalForm):
deduction = None
def __init__(self, *args, **kwargs):
self.deduction = kwargs.pop("deduction", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit Deduction")
form_data = {
"account": self.deduction.account,
"intervention": self.deduction.intervention,
"surface": self.deduction.surface,
}
self.load_initial_data(form_data)
def _get_available_surface(self, acc):
rest_surface = super()._get_available_surface(acc)
# Increase available surface by the currently deducted surface, so we can 'deduct' the same amount again or
# increase the surface only a little, which will still be valid.
# Example: 200 m² left, 500 m² deducted. Entering 700 m² would fail if we would not add the 500 m² to the available
# surface again.
rest_surface += self.deduction.surface
return rest_surface
def save(self):
deduction = self.deduction
form_account = self.cleaned_data.get("account", None)
form_intervention = self.cleaned_data.get("intervention", None)
current_account = deduction.account
current_intervention = deduction.intervention
# If account or intervention has been changed, we put that change in the logs just as if the deduction has
# been removed for this entry. Act as if the deduction is newly created for the new entries
if current_account != form_account:
current_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED)
form_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_ADDED)
else:
current_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED)
if current_intervention != form_intervention:
current_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED)
form_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_ADDED)
else:
current_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED)
deduction.account = form_account
deduction.intervention = self.cleaned_data.get("intervention", None)
deduction.surface = self.cleaned_data.get("surface", None)
deduction.save()
return deduction
class RemoveEcoAccountDeductionModalForm(RemoveModalForm):
""" Removing modal form for EcoAccountDeduction
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
deduction = None
def __init__(self, *args, **kwargs):
deduction = kwargs.pop("deduction", None)
self.deduction = deduction
super().__init__(*args, **kwargs)
def save(self):
with transaction.atomic():
self.deduction.intervention.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED)
self.deduction.account.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED)
self.deduction.delete()
class NewInterventionDocumentModalForm(NewDocumentModalForm):
document_model = InterventionDocument

View File

@@ -1,4 +1,6 @@
from django import forms
from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID
class DummyFilterInput(forms.HiddenInput):
@@ -30,3 +32,112 @@ class GenerateInput(forms.TextInput):
"""
template_name = "konova/widgets/generate-content-input.html"
class TreeCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
""" Provides multiple selection of parent-child data
"""
template_name = "konova/widgets/tree/checkbox/checkbox-tree-select.html"
class meta:
abstract = True
class TreeRadioSelect(forms.RadioSelect):
""" Provides single selection of parent-child data
"""
template_name = "konova/widgets/tree/radio/radio-tree-select.html"
class meta:
abstract = True
class KonovaCodeTreeCheckboxSelectMultiple(TreeCheckboxSelectMultiple):
""" Provides multiple selection of KonovaCodes
"""
filter = None
def __init__(self, *args, **kwargs):
self.code_list = kwargs.pop("code_list", None)
self.filter = kwargs.pop("filter", {})
super().__init__(*args, **kwargs)
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
codes = KonovaCode.objects.filter(
**self.filter,
)
codes = [
parent_code.add_children()
for parent_code in codes
]
context["codes"] = codes
return context
class KonovaCodeTreeRadioSelect(TreeRadioSelect):
""" Provides single selection of KonovaCode
"""
filter = None
def __init__(self, *args, **kwargs):
self.code_list = kwargs.pop("code_list", None)
self.filter = kwargs.pop("filter", {})
super().__init__(*args, **kwargs)
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
codes = KonovaCode.objects.filter(
**self.filter,
)
codes = [
parent_code.add_children()
for parent_code in codes
]
context["codes"] = codes
return context
class CompensationActionTreeCheckboxSelectMultiple(KonovaCodeTreeCheckboxSelectMultiple):
""" Provides multiple selection of CompensationActions
"""
filter = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.filter = {
"code_lists__in": [CODELIST_COMPENSATION_ACTION_ID],
"parent": None,
}
class CompensationStateTreeRadioSelect(KonovaCodeTreeRadioSelect):
""" Provides single selection of CompensationState
"""
filter = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.filter = {
"code_lists__in": [CODELIST_BIOTOPES_ID],
"parent": None,
"is_archived": False,
}
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
codes = KonovaCode.objects.filter(
**self.filter,
)
codes = [
parent_code.add_children("short_name")
for parent_code in codes
]
context["codes"] = codes
return context

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.1.3 on 2022-02-18 09:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0003_team'),
('intervention', '0002_auto_20220114_0936'),
]
operations = [
migrations.AddField(
model_name='intervention',
name='teams',
field=models.ManyToManyField(help_text='Teams having access (data shared with)', to='user.Team'),
),
]

View File

@@ -0,0 +1,64 @@
# Generated by Django 3.1.3 on 2022-03-03 08:56
from django.db import migrations, models, transaction
import django.db.models.deletion
import uuid
def migrate_handler(apps, schema_editor):
KonovaCode = apps.get_model('codelist', 'KonovaCode')
Responsibility = apps.get_model('intervention', 'Responsibility')
Handler = apps.get_model('intervention', 'Handler')
all_responsibs = Responsibility.objects.all()
if all_responsibs.exists():
handler_tmp_code = KonovaCode.objects.get(
atom_id=710185,
)
with transaction.atomic():
for resp in all_responsibs:
handler_old = resp.handler_old
handler = Handler.objects.create(
type=handler_tmp_code,
detail=handler_old
)
resp.handler = handler
resp.save()
class Migration(migrations.Migration):
dependencies = [
('codelist', '0001_initial'),
('intervention', '0003_intervention_teams'),
]
operations = [
migrations.CreateModel(
name='Handler',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('detail', models.CharField(blank=True, max_length=500, null=True)),
('type', models.ForeignKey(blank=True, limit_choices_to={'code_lists__in': [1052], 'is_archived': False, 'is_selectable': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, to='codelist.konovacode')),
],
options={
'abstract': False,
},
),
migrations.RenameField(
model_name='responsibility',
old_name='handler',
new_name='handler_old',
),
migrations.AddField(
model_name='responsibility',
name='handler',
field=models.ForeignKey(blank=True, help_text="Refers to 'Eingriffsverursacher' or 'Maßnahmenträger'", null=True, on_delete=django.db.models.deletion.SET_NULL, to='intervention.handler'),
),
migrations.RunPython(migrate_handler),
migrations.RemoveField(
model_name='responsibility',
name='handler_old'
),
]

View File

@@ -8,14 +8,17 @@ Created on: 15.11.21
import shutil
from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist
from django.db.models.fields.files import FieldFile
from django.urls import reverse
from django.utils import timezone
from intervention.tasks import celery_export_to_egon
from user.models import User
from django.db import models, transaction
from django.db.models import QuerySet
from django.http import HttpRequest
from compensation.models import EcoAccountDeduction
from intervention.managers import InterventionManager
from intervention.models.legal import Legal
from intervention.models.responsibility import Responsibility
@@ -25,7 +28,8 @@ from konova.models import generate_document_file_upload_path, AbstractDocument,
ShareableObjectMixin, \
RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin
from konova.settings import LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT, DEFAULT_SRID_RLP
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DOCUMENT_REMOVED_TEMPLATE, \
PAYMENT_REMOVED, PAYMENT_ADDED, REVOCATION_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE
from user.models import UserActionLogEntry
@@ -99,34 +103,6 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
checker.run_check()
return checker
def get_LANIS_link(self) -> str:
""" Generates a link for LANIS depending on the geometry
Returns:
"""
try:
geom = self.geometry.geom.transform(DEFAULT_SRID_RLP, clone=True)
x = geom.centroid.x
y = geom.centroid.y
area = int(geom.envelope.area)
z_l = 16
for k_area, v_zoom in LANIS_ZOOM_LUT.items():
if k_area < area:
z_l = v_zoom
break
zoom_lvl = z_l
except (AttributeError, IndexError) as e:
# If no geometry has been added, yet.
x = 1
y = 1
zoom_lvl = 6
return LANIS_LINK_TEMPLATE.format(
zoom_lvl,
x,
y,
)
def get_documents(self) -> (QuerySet, QuerySet):
""" Getter for all documents of an intervention
@@ -156,6 +132,16 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
self.add_log_entry_to_compensations(log_entry)
return log_entry
def send_data_to_egon(self):
""" Performs the export to rabbitmq of this intervention's data
FOLLOWING BACKWARDS COMPATIBILITY LOGIC
Returns:
"""
celery_export_to_egon.delay(self.id)
def set_recorded(self, user: User) -> UserActionLogEntry:
log_entry = super().set_recorded(user)
self.add_log_entry_to_compensations(log_entry)
@@ -195,6 +181,9 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
comment=form_data.get("comment", None),
intervention=self,
)
self.mark_as_edited(user, form.request, edit_comment=PAYMENT_ADDED)
self.send_data_to_egon()
return pay
def add_revocation(self, form):
@@ -228,27 +217,55 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
)
return revocation
def add_deduction(self, form):
""" Adds a new deduction to the intervention
def edit_revocation(self, form):
""" Updates a revocation of the intervention
Args:
form (NewDeductionModalForm): The form holding the data
form (EditRevocationModalForm): The form holding the data
Returns:
"""
form_data = form.cleaned_data
user = form.user
file = form_data.get("file", None)
revocation = form.revocation
revocation.date = form_data.get("date", None)
revocation.comment = form_data.get("comment", None)
with transaction.atomic():
user_action_create = UserActionLogEntry.get_created_action(user)
deduction = EcoAccountDeduction.objects.create(
intervention=self,
account=form_data["account"],
surface=form_data["surface"],
created=user_action_create,
)
return deduction
try:
revocation.document.date_of_creation = revocation.date
revocation.document.comment = revocation.comment
if not isinstance(file, FieldFile):
revocation.document.replace_file(file)
revocation.document.save()
except ObjectDoesNotExist:
revocation.document = RevocationDocument.objects.create(
title="revocation_of_{}".format(self.identifier),
date_of_creation=revocation.date,
comment=revocation.comment,
file=file,
instance=revocation
)
revocation.save()
return revocation
def remove_revocation(self, form):
""" Removes a revocation from the intervention
Args:
form (RemoveRevocationModalForm): The form holding all relevant data
Returns:
"""
revocation = form.revocation
user = form.user
with transaction.atomic():
revocation.delete()
self.mark_as_edited(user, request=form.request, edit_comment=REVOCATION_REMOVED)
def mark_as_edited(self, performing_user: User, request: HttpRequest = None, edit_comment: str = None, reset_recorded: bool = True):
""" In case the object or a related object changed, internal processes need to be started, such as
@@ -263,9 +280,12 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
Returns:
"""
super().mark_as_edited(performing_user, request, edit_comment, reset_recorded)
action = super().mark_as_edited(performing_user, edit_comment=edit_comment)
if reset_recorded:
self.unrecord(performing_user, request)
if self.checked:
self.set_unchecked()
return action
def set_status_messages(self, request: HttpRequest):
""" Setter for different information that need to be rendered
@@ -278,6 +298,13 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
Returns:
request (HttpRequest): The modified request
"""
# Inform user about revocation
if self.legal.revocations.exists():
messages.error(
request,
INTERVENTION_HAS_REVOCATIONS_TEMPLATE.format(self.legal.revocations.count()),
extra_tags="danger",
)
if not self.is_shared_with(request.user):
messages.info(request, DATA_UNSHARED_EXPLANATION)
request = self.set_geometry_conflict_message(request)
@@ -299,6 +326,30 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
and is_free_of_revocations
return is_ready
def get_share_link(self):
""" Returns the share url for the object
Returns:
"""
return reverse("intervention:share", args=(self.id, self.access_token))
def remove_payment(self, form):
""" Removes a Payment from the intervention
Args:
form (RemovePaymentModalForm): The form holding all relevant data
Returns:
"""
payment = form.payment
user = form.user
with transaction.atomic():
payment.delete()
self.mark_as_edited(user, request=form.request, edit_comment=PAYMENT_REMOVED)
self.send_data_to_egon()
class InterventionDocument(AbstractDocument):
"""
@@ -314,7 +365,7 @@ class InterventionDocument(AbstractDocument):
max_length=1000,
)
def delete(self, *args, **kwargs):
def delete(self, user=None, *args, **kwargs):
"""
Custom delete functionality for InterventionDocuments.
Removes the folder from the file system if there are no further documents for this entry.
@@ -336,6 +387,9 @@ class InterventionDocument(AbstractDocument):
folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path)
if user:
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))
# Remove the file itself
super().delete(*args, **kwargs)

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