Compare commits

...

76 Commits
v0.2 ... v0.4

Author SHA1 Message Date
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
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
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
139 changed files with 4372 additions and 1185 deletions

View File

@@ -8,7 +8,10 @@
"responsible": {
"conservation_office": null,
"conservation_file_number": null,
"handler": null
"handler": {
"type": null,
"detail": "Someone"
}
},
"legal": {
"agreement_date": null

View File

@@ -7,7 +7,10 @@
"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()
@@ -114,7 +139,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 +168,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 +192,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

@@ -48,7 +48,10 @@
"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,7 +47,10 @@
"responsible": {
"conservation_office": null,
"conservation_file_number": "TEST_CHANGED",
"handler": "TEST_HANDLER_CHANGED"
"handler": {
"type": null,
"detail": "TEST_HANDLER_CHANGED"
}
},
"before_states": [],
"after_states": [],

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),))
@@ -108,7 +108,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 +126,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 +139,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 +156,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 +168,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 +184,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

@@ -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
@@ -44,7 +44,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 +64,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 +96,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
@@ -128,6 +134,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()
@@ -170,6 +177,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
@@ -31,7 +31,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 +51,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 +79,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
@@ -101,6 +107,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
obj = self._set_responsibility(obj, properties["responsible"])
obj.geometry.save()
obj.responsible.handler.save()
obj.responsible.save()
obj.save()
@@ -140,6 +147,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
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

@@ -11,7 +11,7 @@ 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 +36,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,26 @@ class KonovaCode(models.Model):
ret_val += ", " + self.parent.long_name
return ret_val
def add_children(self):
""" Adds all children (resurcively until leaf) as .children to the KonovaCode
Returns:
code (KonovaCode): The manipulated KonovaCode instance
"""
if self.is_leaf:
return None
children = KonovaCode.objects.filter(
code_lists__in=self.code_lists.all(),
parent=self
).order_by(
"long_name"
)
self.children = children
for child in children:
child.add_children()
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

@@ -21,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 = [

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__users__in=[self.user])
).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,10 +13,10 @@ 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, 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"),
@@ -345,7 +363,8 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
"registration_date",
"surface",
"conservation_file_number",
"handler",
"handler_type",
"handler_detail",
"comment",
]
@@ -368,7 +387,8 @@ 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)
@@ -379,6 +399,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,
@@ -400,7 +425,7 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
comment=comment,
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)
@@ -423,11 +448,13 @@ class EditEcoAccountForm(NewEcoAccountForm):
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,
@@ -445,7 +472,8 @@ 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)
@@ -458,7 +486,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()

View File

@@ -17,10 +17,11 @@ 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
from konova.contexts import BaseContext
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, \
from konova.utils.message_templates import FORM_INVALID, ADDED_COMPENSATION_STATE, \
ADDED_COMPENSATION_ACTION, PAYMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED
@@ -112,6 +113,7 @@ class EditPaymentModalForm(NewPaymentForm):
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),
@@ -126,6 +128,7 @@ class EditPaymentModalForm(NewPaymentForm):
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
@@ -267,6 +270,7 @@ class EditCompensationStateModalForm(NewStateModalForm):
def __init__(self, *args, **kwargs):
self.state = kwargs.pop("state", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit state")
form_data = {
"biotope_type": self.state.biotope_type,
"biotope_extra": self.state.biotope_type_details.all(),
@@ -379,6 +383,7 @@ class EditDeadlineModalForm(NewDeadlineModalForm):
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),
@@ -405,22 +410,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"),
@@ -482,6 +478,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)
@@ -495,8 +501,9 @@ class EditCompensationActionModalForm(NewActionModalForm):
def __init__(self, *args, **kwargs):
self.action = kwargs.pop("action", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit action")
form_data = {
"action_type": self.action.action_type,
"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,
@@ -506,7 +513,7 @@ class EditCompensationActionModalForm(NewActionModalForm):
def save(self):
action = self.action
action.action_type = self.cleaned_data.get("action_type", None)
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)

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

@@ -10,9 +10,7 @@ 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
from konova.utils.message_templates import COMPENSATION_ACTION_REMOVED
class UnitChoices(models.TextChoices):
@@ -31,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],
@@ -57,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,7 @@ Created on: 16.11.21
import shutil
from django.contrib import messages
from user.models import User
from user.models import User, Team
from django.db import models, transaction
from django.db.models import QuerySet, Sum
from django.http import HttpRequest
@@ -104,12 +104,12 @@ 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)
@@ -299,7 +299,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:
@@ -308,10 +308,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:
@@ -322,6 +321,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
@@ -331,6 +352,15 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
"""
return self.intervention.users.all()
@property
def shared_teams(self) -> QuerySet:
""" Shortcut for fetching the teams which have shared access on this object
Returns:
users (QuerySet)
"""
return self.intervention.teams.all()
def get_documents(self) -> QuerySet:
""" Getter for all documents of a compensation
@@ -388,6 +418,18 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
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):
"""

View File

@@ -134,7 +134,7 @@ class CompensationTable(BaseTable, TableRenderMixin):
"""
parcels = value.get_underlying_parcels().values_list(
"gmrkng",
"parcel_group__name",
flat=True
).distinct()
html = render_to_string(
@@ -181,9 +181,7 @@ class CompensationTable(BaseTable, TableRenderMixin):
"""
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"),
@@ -295,7 +293,7 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
"""
parcels = value.get_underlying_parcels().values_list(
"gmrkng",
"parcel_group__name",
flat=True
).distinct()
html = render_to_string(
@@ -343,7 +341,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

@@ -47,13 +47,15 @@
{% for action in actions %}
<tr>
<td class="">
<span>{{ action.action_type }}</span>
{% if action.action_type_details.count > 0 %}
<br>
{% for detail in action.action_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% endfor %}
{% endif %}
{% 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 %}
<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="">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
<td class="">

View File

@@ -48,13 +48,13 @@
{% for state in after_states %}
<tr>
<td>
<span>{{ state.biotope_type }}</span>
{% if state.biotope_type_details.count > 0 %}
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% endfor %}
{% endif %}
<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>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">

View File

@@ -48,13 +48,13 @@
{% for state in before_states %}
<tr>
<td>
<span>{{ state.biotope_type }}</span>
{% if state.biotope_type_details.count > 0 %}
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% endfor %}
{% endif %}
<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>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">

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.
@@ -89,14 +90,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.teams.all %}
{% 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 %}
@@ -111,7 +122,7 @@
{% include 'map/geom_form.html' %}
</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' %}

View File

@@ -46,13 +46,15 @@
{% for action in actions %}
<tr>
<td class="">
<span>{{ action.action_type }}</span>
{% if action.action_type_details.count > 0 %}
<br>
{% for detail in action.action_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% endfor %}
{% endif %}
{% 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 %}
<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="">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
<td class="">

View File

@@ -10,7 +10,7 @@
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and has_access %}
{% 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' %}
@@ -61,7 +61,7 @@
<td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td>
<td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td>
<td class="align-middle float-right">
{% if is_default_member and has_access %}
{% 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>

View File

@@ -48,13 +48,13 @@
{% for state in after_states %}
<tr>
<td>
<span>{{ state.biotope_type }}</span>
{% if state.biotope_type_details.count > 0 %}
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% endfor %}
{% endif %}
<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>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">

View File

@@ -48,13 +48,13 @@
{% for state in before_states %}
<tr>
<td>
<span>{{ state.biotope_type }}</span>
{% if state.biotope_type_details.count > 0 %}
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% endfor %}
{% endif %}
<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>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">

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.
@@ -72,14 +73,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.teams.all %}
{% include 'user/includes/team_data_modal_button.html' %}
{% endfor %}
<hr>
{% for user in obj.users.all %}
{% include 'user/includes/contact_modal_button.html' %}
{% endfor %}
@@ -93,7 +104,7 @@
{% include 'map/geom_form.html' %}
</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' %}

View File

@@ -38,17 +38,10 @@
{% include 'map/geom_form.html' %}
</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

@@ -20,10 +20,6 @@
<th scope="row">{% trans 'Conservation office file number' %}</th>
<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>
</tr>
<tr>
<th scope="row">{% trans 'Deductions for' %}</th>
<td class="align-middle">
@@ -55,17 +51,10 @@
{% include 'map/geom_form.html' %}
</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

@@ -103,7 +103,7 @@ class CompensationViewTestCase(BaseViewTestCase):
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
self.intervention.share_with_list([self.superuser])
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
@@ -143,7 +143,7 @@ class CompensationViewTestCase(BaseViewTestCase):
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([])
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
@@ -185,7 +185,7 @@ class CompensationViewTestCase(BaseViewTestCase):
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])
self.intervention.share_with_user_list([self.superuser])
success_urls = [
self.index_url,
@@ -221,7 +221,7 @@ class CompensationViewTestCase(BaseViewTestCase):
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.intervention.share_with_user_list([])
success_urls = [
self.index_url,

View File

@@ -25,7 +25,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
super().setUp()
# Give the user shared access to the dummy intervention -> inherits the access to the compensation
self.intervention.share_with(self.superuser)
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)
@@ -60,8 +60,9 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
# 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)
@@ -261,3 +262,26 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
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
User must be redirected to another page
Returns:
"""
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

@@ -78,7 +78,7 @@ class EcoAccountViewTestCase(CompensationViewTestCase):
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
self.eco_account.share_with_list([self.superuser])
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
@@ -119,7 +119,7 @@ class EcoAccountViewTestCase(CompensationViewTestCase):
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
self.eco_account.share_with_list([])
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
@@ -163,7 +163,7 @@ class EcoAccountViewTestCase(CompensationViewTestCase):
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])
self.eco_account.share_with_user_list([self.superuser])
success_urls = [
self.index_url,
@@ -200,7 +200,7 @@ class EcoAccountViewTestCase(CompensationViewTestCase):
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([])
self.eco_account.share_with_user_list([])
success_urls = [
self.index_url,

View File

@@ -27,7 +27,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
# 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_list([self.superuser])
self.eco_account.share_with_user_list([self.superuser])
def test_new(self):
""" Test the creation of an EcoAccount
@@ -73,7 +73,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
Returns:
"""
self.eco_account.share_with(self.superuser)
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()
@@ -129,7 +129,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
"""
# Add proper privilege for the user
self.eco_account.share_with(self.superuser)
self.eco_account.share_with_user(self.superuser)
pre_record_log_count = self.eco_account.log.count()
# Prepare url and form data
@@ -178,7 +178,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
"""
# Give user shared access to the dummy intervention, which will be needed here
self.intervention.share_with(self.superuser)
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()
@@ -231,7 +231,9 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
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,
@@ -279,8 +281,8 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
"confirm": True,
}
intervention.share_with(self.superuser)
account.share_with(self.superuser)
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()
@@ -300,3 +302,27 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
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

@@ -64,7 +64,7 @@ class PaymentViewTestCase(BaseViewTestCase):
client = Client()
client.login(username=self.superuser.username, password=self.superuser_pw)
self.superuser.groups.set([])
self.intervention.share_with_list([self.superuser])
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
@@ -91,7 +91,7 @@ class PaymentViewTestCase(BaseViewTestCase):
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([])
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
@@ -120,7 +120,7 @@ class PaymentViewTestCase(BaseViewTestCase):
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])
self.intervention.share_with_user_list([self.superuser])
success_urls = [
self.new_url,
@@ -143,7 +143,7 @@ class PaymentViewTestCase(BaseViewTestCase):
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.intervention.share_with_user_list([])
success_urls = [
]

View File

@@ -21,7 +21,7 @@ class PaymentWorkflowTestCase(BaseWorkflowTestCase):
def setUp(self) -> None:
super().setUp()
# Give the user shared access to the dummy intervention
self.intervention.share_with(self.superuser)
self.intervention.share_with_user(self.superuser)
self.payment = Payment.objects.get_or_create(
intervention=self.intervention,

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
@@ -22,7 +23,7 @@ from konova.utils.message_templates import FORM_INVALID, IDENTIFIER_REPLACED, DA
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
DEADLINE_EDITED, RECORDED_BLOCKS_EDIT, PARAMS_INVALID
from konova.utils.user_checks import in_group
@@ -69,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":
@@ -134,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)
@@ -596,14 +617,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")
@@ -611,8 +630,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

@@ -35,7 +35,8 @@ 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, 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
DEDUCTION_EDITED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED, \
RECORDED_BLOCKS_EDIT
from konova.utils.user_checks import in_group
@@ -145,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)
@@ -272,7 +280,6 @@ def remove_view(request: HttpRequest, id: str):
@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
@@ -287,6 +294,8 @@ 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")
@@ -300,7 +309,6 @@ def deduction_remove_view(request: HttpRequest, id: str, deduction_id: str):
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def deduction_edit_view(request: HttpRequest, id: str, deduction_id: str):
""" Renders a modal view for editing deductions
@@ -315,6 +323,8 @@ def deduction_edit_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")
@@ -679,7 +689,6 @@ def remove_document_view(request: HttpRequest, id: str, 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
@@ -691,6 +700,8 @@ 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,
@@ -728,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()\
@@ -749,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,
@@ -793,7 +808,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("compensation:acc:detail", id=id)
else:
messages.error(

View File

@@ -14,7 +14,7 @@ from django.utils.translation import gettext_lazy as _
from compensation.forms.forms import AbstractCompensationForm, CompensationResponsibleFormMixin
from ema.models import Ema, EmaDocument
from intervention.models import Responsibility
from intervention.models import Responsibility, Handler
from konova.forms import SimpleGeomForm, NewDocumentModalForm
from user.models import UserActionLogEntry
@@ -31,7 +31,8 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
"title",
"conservation_office",
"conservation_file_number",
"handler",
"handler_type",
"handler_detail",
"comment",
]
@@ -53,7 +54,8 @@ 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)
comment = self.cleaned_data.get("comment", None)
@@ -63,6 +65,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,
@@ -80,7 +86,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
)
# 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,7 +111,8 @@ 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,
@@ -121,7 +128,8 @@ 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)
@@ -132,7 +140,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()

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

@@ -104,7 +104,7 @@ class EmaTable(BaseTable, TableRenderMixin):
"""
parcels = value.get_underlying_parcels().values_list(
"gmrkng",
"parcel_group__name",
flat=True
).distinct()
html = render_to_string(
@@ -151,9 +151,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

@@ -44,13 +44,15 @@
{% for action in obj.actions.all %}
<tr>
<td class="">
<span>{{ action.action_type }}</span>
{% if action.action_type_details.count > 0 %}
<br>
{% for detail in action.action_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% endfor %}
{% endif %}
{% 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 %}
<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="">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
<td class="">

View File

@@ -46,13 +46,13 @@
{% for state in after_states %}
<tr>
<td>
<span>{{ state.biotope_type }}</span>
{% if state.biotope_type_details.count > 0 %}
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% endfor %}
{% endif %}
<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>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">

View File

@@ -46,13 +46,13 @@
{% for state in before_states %}
<tr>
<td>
<span>{{ state.biotope_type }}</span>
{% if state.biotope_type_details.count > 0 %}
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% endfor %}
{% endif %}
<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>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">

View File

@@ -60,20 +60,23 @@
<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 team in obj.teams.all %}
{% include 'user/includes/team_data_modal_button.html' %}
{% endfor %}
<hr>
{% for user in obj.users.all %}
{% include 'user/includes/contact_modal_button.html' %}
{% endfor %}
@@ -87,7 +90,7 @@
{% include 'map/geom_form.html' %}
</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' %}

View File

@@ -20,10 +20,6 @@
<th scope="row">{% trans 'Conservation office file number' %}</th>
<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>
</tr>
<tr>
<th scope="row">{% trans 'Last modified' %}</th>
<td class="align-middle">
@@ -42,17 +38,10 @@
{% include 'map/geom_form.html' %}
</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

@@ -79,7 +79,9 @@ class EmaViewTestCase(CompensationViewTestCase):
# Create log entry
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()
self.ema = Ema.objects.create(
identifier="TEST",
@@ -110,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,
@@ -160,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,
@@ -203,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,
@@ -243,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,

View File

@@ -117,6 +117,32 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
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)

View File

@@ -26,7 +26,7 @@ from konova.utils.generators import generate_qr_code
from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \
DOCUMENT_ADDED, COMPENSATION_STATE_REMOVED, COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, \
COMPENSATION_ACTION_ADDED, DEADLINE_ADDED, DEADLINE_REMOVED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, \
COMPENSATION_ACTION_EDITED, DEADLINE_EDITED
COMPENSATION_ACTION_EDITED, DEADLINE_EDITED, RECORDED_BLOCKS_EDIT
from konova.utils.user_checks import in_group
@@ -213,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)
@@ -563,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")
@@ -578,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,
@@ -621,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(

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

@@ -16,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
@@ -138,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"),
@@ -151,6 +168,7 @@ class NewInterventionForm(BaseForm):
}
)
)
registration_date = forms.DateField(
label=_("Registration date"),
label_suffix=_(""),
@@ -205,7 +223,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)
@@ -226,6 +245,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,
@@ -253,7 +276,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
@@ -284,7 +307,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,
@@ -313,7 +337,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)
@@ -328,7 +353,10 @@ 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

View File

@@ -7,11 +7,11 @@ Created on: 27.09.21
"""
from dal import autocomplete
from django.core.exceptions import ObjectDoesNotExist
from django.db.models.fields.files import FieldFile
from konova.utils.message_templates import DEDUCTION_ADDED, REVOCATION_ADDED, DEDUCTION_REMOVED, DEDUCTION_EDITED, \
REVOCATION_EDITED, FILE_TYPE_UNSUPPORTED, FILE_SIZE_TOO_LARGE
from user.models import User, UserActionLogEntry
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.utils.translation import gettext_lazy as _
@@ -37,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."),
@@ -49,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)
@@ -77,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
@@ -91,34 +134,14 @@ class ShareModalForm(BaseModalForm):
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):
@@ -182,6 +205,7 @@ class EditRevocationModalForm(NewRevocationModalForm):
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:
@@ -267,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()
@@ -401,13 +427,22 @@ 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
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"])
@@ -421,7 +456,7 @@ 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
@@ -452,6 +487,7 @@ class EditEcoAccountDeductionModalForm(NewDeductionModalForm):
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,

View File

@@ -1,4 +1,6 @@
from django import forms
from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID
class DummyFilterInput(forms.HiddenInput):
@@ -30,3 +32,51 @@ 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/checkbox-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 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,
}

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

@@ -13,6 +13,7 @@ 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
@@ -131,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)
@@ -171,6 +182,8 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
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):
@@ -335,6 +348,7 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
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):

View File

@@ -6,10 +6,42 @@ Created on: 15.11.21
"""
from django.db import models
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_REGISTRATION_OFFICE_ID
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_REGISTRATION_OFFICE_ID, \
CODELIST_HANDLER_ID
from konova.models import UuidModel
from konova.utils.message_templates import UNKNOWN, NO_DETAILS
class Handler(UuidModel):
""" The handler of an entry
Refers to 'Eingriffsverursacher' or 'Maßnahmenträger'
"""
type = models.ForeignKey(
KonovaCode,
on_delete=models.SET_NULL,
null=True,
blank=True,
limit_choices_to={
"code_lists__in": [CODELIST_HANDLER_ID],
"is_selectable": True,
"is_archived": False,
}
)
detail = models.CharField(
max_length=500,
null=True,
blank=True,
)
def __str__(self):
detail = self.detail or NO_DETAILS
_type = self.type.long_name if self.type is not None else UNKNOWN
return f'{_type}, {detail}'
class Responsibility(UuidModel):
@@ -43,11 +75,17 @@ class Responsibility(UuidModel):
}
)
conservation_file_number = models.CharField(max_length=1000, blank=True, null=True)
handler = models.CharField(max_length=500, null=True, blank=True, help_text="Refers to 'Eingriffsverursacher' or 'Maßnahmenträger'")
handler = models.ForeignKey(
Handler,
null=True,
blank=True,
help_text="Refers to 'Eingriffsverursacher' or 'Maßnahmenträger'",
on_delete=models.SET_NULL,
)
def __str__(self):
return "ZB: {} | ETS: {} | Handler: {}".format(
self.registration_office,
self.conservation_office,
self.handler
str(self.handler)
)

View File

@@ -6,4 +6,11 @@ Created on: 30.11.20
"""
INTERVENTION_IDENTIFIER_LENGTH = 6
INTERVENTION_IDENTIFIER_TEMPLATE = "EIV-{}"
INTERVENTION_IDENTIFIER_TEMPLATE = "EIV-{}"
# EGON connection settings via rabbitmq
# NEEDED FOR BACKWARDS COMPATIBILITY
EGON_RABBITMQ_HOST = "CHANGE_ME"
EGON_RABBITMQ_PORT = "CHANGE_ME"
EGON_RABBITMQ_USER = "CHANGE_ME"
EGON_RABBITMQ_PW = "CHANGE_ME"

View File

@@ -131,7 +131,7 @@ class InterventionTable(BaseTable, TableRenderMixin):
"""
parcels = value.get_underlying_parcels().values_list(
"gmrkng",
"parcel_group__name",
flat=True
).distinct()
html = render_to_string(
@@ -177,9 +177,7 @@ class InterventionTable(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"),

18
intervention/tasks.py Normal file
View File

@@ -0,0 +1,18 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 21.03.22
"""
from celery import shared_task
from intervention.utils.egon_export import EgonExporter
@shared_task
def celery_export_to_egon(intervention_id: str):
from intervention.models import Intervention
intervention = Intervention.objects.get(id=intervention_id)
egon_exporter = EgonExporter(intervention)
egon_exporter.export_to_rabbitmq()

View File

@@ -1,5 +1,5 @@
{% extends 'base.html' %}
{% load i18n l10n static fontawesome_5 humanize %}
{% load i18n l10n static fontawesome_5 %}
{% block head %}
{% comment %}
@@ -106,14 +106,24 @@
<tr>
<th scope="row">{% trans 'Last modified' %}</th>
<td class="align-middle">
{{obj.created.timestamp|default_if_none:""|naturalday}}
<br>
{{obj.created.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.teams.all %}
{% include 'user/includes/team_data_modal_button.html' %}
{% endfor %}
<hr>
{% for user in obj.users.all %}
{% include 'user/includes/contact_modal_button.html' %}
{% endfor %}
@@ -127,7 +137,7 @@
{% include 'map/geom_form.html' %}
</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' %}

View File

@@ -41,10 +41,6 @@
<th scope="row">{% trans 'Conservation office file number' %}</th>
<td class="align-middle">{{obj.responsible.conservation_file_number|default_if_none:""}}</td>
</tr>
<tr>
<th scope="row">{% trans 'Intervention handler' %}</th>
<td class="align-middle">{{obj.responsible.handler|default_if_none:""}}</td>
</tr>
<tr>
<th scope="row">{% trans 'Compensations' %}</th>
<td class="align-middle">
@@ -101,17 +97,10 @@
{% include 'map/geom_form.html' %}
</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

@@ -144,7 +144,7 @@ class InterventionViewTestCase(BaseViewTestCase):
# Add user to default group
default_group = Group.objects.get(name=DEFAULT_GROUP)
self.superuser.groups.set([default_group])
self.intervention.share_with_list([self.superuser])
self.intervention.share_with_user_list([self.superuser])
success_urls = [
self.index_url,
@@ -190,7 +190,7 @@ class InterventionViewTestCase(BaseViewTestCase):
# Add user to default group
default_group = Group.objects.get(name=DEFAULT_GROUP)
self.superuser.groups.set([default_group])
self.intervention.share_with_list([])
self.intervention.share_with_user_list([])
success_urls = [
self.index_url,
@@ -236,7 +236,7 @@ class InterventionViewTestCase(BaseViewTestCase):
# Add user to zb group
zb_group = self.groups.get(name=ZB_GROUP)
self.superuser.groups.set([zb_group])
self.intervention.share_with_list([self.superuser])
self.intervention.share_with_user_list([self.superuser])
success_urls = [
self.index_url,
@@ -282,7 +282,7 @@ class InterventionViewTestCase(BaseViewTestCase):
# Add user to zb group
zb_group = self.groups.get(name=ZB_GROUP)
self.superuser.groups.set([zb_group])
self.intervention.share_with_list([])
self.intervention.share_with_user_list([])
success_urls = [
self.index_url,
@@ -328,7 +328,7 @@ class InterventionViewTestCase(BaseViewTestCase):
# Add user to ets group
ets_group = Group.objects.get(name=ETS_GROUP)
self.superuser.groups.set([ets_group])
self.intervention.share_with_list([self.superuser])
self.intervention.share_with_user_list([self.superuser])
success_urls = [
self.index_url,
@@ -374,7 +374,7 @@ class InterventionViewTestCase(BaseViewTestCase):
# Add user to default group
ets_group = Group.objects.get(name=ETS_GROUP)
self.superuser.groups.set([ets_group])
self.intervention.share_with_list([])
self.intervention.share_with_user_list([])
success_urls = [
self.index_url,

View File

@@ -30,7 +30,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
super().setUp()
# Recreate a new (bare minimum) intervention before each test
self.intervention = self.create_dummy_intervention()
self.intervention.share_with(self.superuser)
self.intervention.share_with_user(self.superuser)
def test_new(self):
"""
@@ -89,6 +89,30 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
self.assertIn(self.superuser, obj.users.all())
self.assertEqual(1, obj.users.count())
def test_non_editable_after_recording(self):
""" Tests that the intervention can not be edited after being recorded
User must be redirected to another page
Returns:
"""
self.assertIsNotNone(self.intervention)
self.assertFalse(self.intervention.is_recorded)
edit_url = reverse("intervention:edit", args=(self.intervention.id,))
response = self.client_user.get(edit_url)
has_redirect = response.status_code == 302
self.assertFalse(has_redirect)
self.intervention.set_recorded(self.user)
self.assertTrue(self.intervention.is_recorded)
edit_url = reverse("intervention:edit", args=(self.intervention.id,))
response = self.client_user.get(edit_url)
has_redirect = response.status_code == 302
self.assertTrue(has_redirect)
self.intervention.set_unrecorded(self.user)
def test_checkability(self):
""" Tests that the intervention can only be checked if all required data has been added
@@ -303,7 +327,6 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
Reasons for failing are:
* EcoAccount does not provide enough 'deductable_surface'
* EcoAccount is not recorded (not "approved"), yet
* EcoAccount is not shared with performing user
Args:
new_url (str): The url to send the post data to
@@ -315,7 +338,6 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
# Before running fail positive tests, we need to have an account in a (normally) fine working state
self.assertIsNotNone(self.eco_account.recorded) # -> is recorded
self.assertGreater(self.eco_account.deductable_surface, test_surface) # -> has more deductable surface than we need
self.assertIn(self.superuser, self.eco_account.users.all()) # -> is shared with the performing user
# Count the number of already existing deductions in total and for the account for later comparison
num_deductions = self.eco_account.deductions.count()
@@ -333,20 +355,11 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
self.assertEqual(num_deductions, self.eco_account.deductions.count())
self.assertEqual(num_deductions_total, EcoAccountDeduction.objects.count())
# Now restore the deductable surface to a valid size back again but remove the user from the shared list
# Now restore the deductable surface to a valid size back again
self.eco_account.deductable_surface = test_surface + 100.00
self.eco_account.share_with_list([])
self.eco_account.save()
# Now perform the (expected) failing request (again)
self.client_user.post(new_url, post_data)
# Expect no changes at all, since the account is not shared
self.assertEqual(num_deductions, self.eco_account.deductions.count())
self.assertEqual(num_deductions_total, EcoAccountDeduction.objects.count())
# Restore the sharing but remove the recording state
self.eco_account.share_with_list([self.superuser])
# Remove the recording state
self.eco_account.recorded.delete()
self.eco_account.refresh_from_db()
self.eco_account.save()
@@ -376,7 +389,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
if self.eco_account.recorded is None:
rec_action = UserActionLogEntry.get_recorded_action(self.superuser)
self.eco_account.recorded = rec_action
self.eco_account.share_with_list([self.superuser])
self.eco_account.share_with_user_list([self.superuser])
self.eco_account.save()
num_all_deducs = EcoAccountDeduction.objects.count()

View File

@@ -0,0 +1,250 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 07.03.22
"""
import base64
import json
import pika
import xmltodict
from django.db.models import Sum
from intervention.settings import EGON_RABBITMQ_HOST, EGON_RABBITMQ_USER, EGON_RABBITMQ_PW, EGON_RABBITMQ_PORT
from konova.sub_settings.django_settings import DEFAULT_DATE_FORMAT
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
class EgonExporter:
"""
EGON is the payment management system of SNU RLP. Due to compatibility reasons we need to provide the old style
of data transmission between KSP and EGON:
1. Create GML from intervention object
2. Send created GML to the appropriate RabbitMQ channel
"""
intervention = None
gml_builder = None
def __init__(self, intervention):
self.intervention = intervention
self.gml_builder = EgonGmlBuilder(intervention)
def export_to_rabbitmq(self):
""" Sends the exporter gml to message broker rabbitmq to be fetched by EGON application from there
Returns:
"""
msg = {
"nachricht": self.gml_builder.gml,
}
msg = json.dumps(msg)
print(msg)
credentials = pika.PlainCredentials(EGON_RABBITMQ_USER, EGON_RABBITMQ_PW)
params = pika.ConnectionParameters(
EGON_RABBITMQ_HOST,
EGON_RABBITMQ_PORT,
"/",
credentials
)
conn = pika.BlockingConnection(params)
channel = conn.channel()
channel.basic_publish(
exchange="",
routing_key="KSP_EGON",
body=msg.encode("utf-8"),
)
conn.close()
class EgonGmlBuilder:
"""
Creates the GML for EGON export
"""
intervention = None
gml = None
def __init__(self, intervention):
self.intervention = intervention
self.gml = self.build_gml()
def _gen_flurstuecksKennzeichen(self, parcel):
""" Generates oneo:flurstuecksKennzeichen to provide backwards compatibility
Args:
parcel (Parcel): The requested parcel
Returns:
str
"""
gmrkng_code = "{0:06d}".format(int(parcel.parcel_group.key) or 0)
flr_code = "{0:03d}".format(int(parcel.flr or 0))
flrstckzhlr_code = "{0:05d}".format(int(parcel.flrstck_zhlr or 0))
flrstcknnr_code = "{0:06d}".format(int(parcel.flrstck_nnr or 0))
return gmrkng_code + flr_code + flrstckzhlr_code + flrstcknnr_code
def _sum_all_payments(self):
all_payments = self.intervention.payments.aggregate(
summed=Sum("amount")
)["summed"]
return all_payments
def _gen_kompensationsArt(self) -> (str, int):
comp_type = "Ersatzzahlung"
comp_type_code = 774898901
if self.intervention.compensations.exists():
comp_type += " und Kompensation"
comp_type_code = 771655351
return comp_type, comp_type_code
def _gen_geometry_list(self):
geom = self.intervention.geometry.geom
geom.transform(DEFAULT_SRID_RLP)
geoms_list = [
{
"gml:Polygon": {
"gml:exterior": {
"gml:LinearRing": {
"gml:posList": " ".join([f"{str(coord[0])} {str(coord[1])}" for coord in coords[0]])
}
}
}
} for coords in geom.coords
]
return geoms_list
def _gen_raumreferenz(self):
parcels = self.intervention.get_underlying_parcels()
spatial_reference_list = [
{
"oneo:datumAbgleich": None,
"oneo:ortsangabe": {
"oneo:Ortsangaben": {
"oneo:kreisSchluessel": {
"xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/588/{parcel.district.key}",
},
"oneo:gemeindeSchluessel": {
"xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/910/{parcel.municipal.key}",
},
"oneo:verbandsgemeindeSchluessel": {
"xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/589/{None}",
},
"oneo:flurstuecksKennzeichen": self._gen_flurstuecksKennzeichen(parcel),
}
},
} for parcel in parcels
]
return spatial_reference_list
def _gen_foto(self):
revoc_docs, regular_docs = self.intervention.get_documents()
docs_list = [
{
"oneo:Foto": {
"oneo:aufnahmezeitpunkt": doc.date_of_creation.strftime(DEFAULT_DATE_FORMAT),
"oneo:bemerkung": doc.comment,
"oneo:fotoverweis": base64.b64encode(doc.file.read()).decode("utf-8"),
"oneo:dateiname": doc.title,
"oneo:hauptfoto": False,
}
} for doc in regular_docs
]
return docs_list
def build_gml(self):
comp_type, comp_type_code = self._gen_kompensationsArt()
payment = self.intervention.payments.first()
payment_date = None
if payment is not None:
payment_date = payment.due_on
payment_date = payment_date.strftime(DEFAULT_DATE_FORMAT)
cons_office = self.intervention.responsible.conservation_office
reg_office = self.intervention.responsible.registration_office
law = self.intervention.legal.laws.first()
process_type = self.intervention.legal.process_type
handler = self.intervention.responsible.handler
reg_date = self.intervention.legal.registration_date
bind_date = self.intervention.legal.binding_date
xml_dict = {
"wfs:FeatureCollection": {
"@xmlns:wfs": "http://www.opengis.net/wfs",
"@xmlns:xlink": "http://www.w3.org/1999/xlink",
"@xmlns:oneo": "http://www.osiris-projekt.rlp.de/oneo",
"@xmlns:gmlexr": "http://www.opengis.net/gml/3.3/exr",
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
"@xmlns:gml": "http://www.opengis.net/gml/3.2",
"oneo:Eingriffsverfahren": {
"@gml:id": self.intervention.identifier,
"oneo:azEintragungsstelle": self.intervention.responsible.conservation_file_number,
"oneo:azZulassungsstelle": self.intervention.responsible.registration_file_number,
"oneo:bemerkungZulassungsstelle": None,
"oneo:eintragungsstelle": {
"@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/907/{cons_office.atom_id if cons_office else None}",
"#text": cons_office.long_name if cons_office else None
},
"oneo:zulassungsstelle": {
"@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/1053/{reg_office.atom_id if reg_office else None}",
"#text": reg_office.long_name if reg_office else None
},
"oneo:ersatzzahlung": self._sum_all_payments(),
"oneo:kompensationsart": {
"@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/88140/{comp_type_code}",
"#text": comp_type
},
"oneo:verfahrensrecht": {
"@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/1048/{law.atom_id if law else None}",
"#text": law.short_name if law else None
},
"oneo:verfahrenstyp": {
"@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/44382/{process_type.atom_id if process_type else None}",
"#text": process_type.long_name if process_type else None,
},
"oneo:eingreifer": {
"oneo:Eingreifer": {
"oneo:art": {
"@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/1053/{handler.type.atom_id if handler.type else None}",
"#text": handler.type.long_name if handler.type else None,
},
"oneo:bemerkung": handler.detail if handler else None,
}
},
"oneo:erfasser": {
"oneo:Erfasser": {
"oneo:name": None,
"oneo:bemerkung": None,
}
},
"oneo:zulassung": {
"oneo:Zulassungstermin": {
"oneo:bauBeginn": payment_date,
"oneo:erlass": reg_date.strftime(DEFAULT_DATE_FORMAT) if reg_date else None,
"oneo:rechtsKraft": bind_date.strftime(DEFAULT_DATE_FORMAT) if bind_date else None,
}
},
"oneo:geometrie": {
"gml:multiSurfaceProperty": {
"gml:MultiPolygon": {
"@srsName": f"http://www.opengis.net/gml/srs/epsg.xml#{DEFAULT_SRID_RLP}",
"gml:polygonMember": self._gen_geometry_list(),
}
},
},
"oneo:kennung": self.intervention.identifier,
"oneo:bezeichnung": self.intervention.title,
"oneo:bemerkung": self.intervention.comment,
"oneo:verantwortlicheStelle": None,
"oneo:veroffentlichtAm": None,
"oneo:raumreferenz": {
"oneo:Raumreferenz": self._gen_raumreferenz(),
},
"oneo:foto": self._gen_foto(),
}
},
}
gml = xmltodict.unparse(xml_dict)
return gml

View File

@@ -18,7 +18,8 @@ from konova.utils.documents import remove_document, get_document
from konova.utils.generators import generate_qr_code
from konova.utils.message_templates import INTERVENTION_INVALID, FORM_INVALID, IDENTIFIER_REPLACED, \
CHECKED_RECORDED_RESET, DEDUCTION_REMOVED, DEDUCTION_ADDED, REVOCATION_ADDED, REVOCATION_REMOVED, \
COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, DEDUCTION_EDITED, REVOCATION_EDITED, DOCUMENT_EDITED
COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, DEDUCTION_EDITED, REVOCATION_EDITED, DOCUMENT_EDITED, \
RECORDED_BLOCKS_EDIT
from konova.utils.user_checks import in_group
@@ -302,6 +303,13 @@ def edit_view(request: HttpRequest, id: str):
template = "intervention/form/view.html"
# Get object from db
intervention = get_object_or_404(Intervention, id=id)
if intervention.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("intervention:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditInterventionForm(request.POST or None, instance=intervention)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=intervention)
@@ -432,7 +440,7 @@ def share_view(request: HttpRequest, id: str, token: str):
request,
_("{} has been shared with you").format(intervention.identifier)
)
intervention.share_with(user)
intervention.share_with_user(user)
return redirect("intervention:detail", id=id)
else:
messages.error(
@@ -693,19 +701,22 @@ def report_view(request:HttpRequest, id: str):
distinct_deductions = intervention.deductions.all().distinct(
"account"
)
qrcode_img = generate_qr_code(
request.build_absolute_uri(reverse("intervention:report", args=(id,))),
10
)
qrcode_img_lanis = generate_qr_code(
intervention.get_LANIS_link(),
7
)
qrcode_url = request.build_absolute_uri(reverse("intervention:report", args=(id,)))
qrcode_img = generate_qr_code(qrcode_url, 10)
qrcode_lanis_url = intervention.get_LANIS_link()
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
context = {
"obj": intervention,
"deductions": distinct_deductions,
"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,
},
"geom_form": geom_form,
"parcels": parcels,
TAB_TITLE_IDENTIFIER: tab_title,

View File

@@ -7,7 +7,8 @@ Created on: 22.07.21
"""
from django.contrib import admin
from konova.models import Geometry, Deadline, GeometryConflict, Parcel, District
from konova.models import Geometry, Deadline, GeometryConflict, Parcel, District, Municipal, ParcelGroup
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
from user.models import UserAction
@@ -16,13 +17,28 @@ class GeometryAdmin(admin.ModelAdmin):
list_display = [
"id",
"created",
"st_area",
]
readonly_fields = [
"st_area",
"created",
"modified",
]
def st_area(self, obj):
val = None
geom = obj.geom
if geom is not None:
geom.transform(ct=DEFAULT_SRID_RLP)
val = geom.area
return val
st_area.short_description = f"Area (srid={DEFAULT_SRID_RLP})"
class ParcelAdmin(admin.ModelAdmin):
list_display = [
"id",
"gmrkng",
"parcel_group",
"flr",
"flrstck_nnr",
"flrstck_zhlr",
@@ -32,9 +48,27 @@ class ParcelAdmin(admin.ModelAdmin):
class DistrictAdmin(admin.ModelAdmin):
list_display = [
"name",
"key",
"id",
]
class MunicipalAdmin(admin.ModelAdmin):
list_display = [
"name",
"key",
"district",
"id",
]
class ParcelGroupAdmin(admin.ModelAdmin):
list_display = [
"name",
"key",
"municipal",
"id",
"gmnd",
"krs",
]
@@ -105,5 +139,7 @@ class BaseObjectAdmin(BaseResourceAdmin):
#admin.site.register(Geometry, GeometryAdmin)
#admin.site.register(Parcel, ParcelAdmin)
#admin.site.register(District, DistrictAdmin)
#admin.site.register(Municipal, MunicipalAdmin)
#admin.site.register(ParcelGroup, ParcelGroupAdmin)
#admin.site.register(GeometryConflict, GeometryConflictAdmin)
#admin.site.register(Deadline, DeadlineAdmin)

View File

@@ -5,14 +5,19 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 07.12.20
"""
import collections
from dal_select2.views import Select2QuerySetView, Select2GroupQuerySetView
from user.models import User
from django.core.exceptions import ImproperlyConfigured
from konova.utils.message_templates import UNGROUPED
from user.models import User, Team
from django.db.models import Q
from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_LAW_ID, \
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_PROCESS_TYPE_ID, \
CODELIST_BIOTOPES_EXTRA_CODES_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID
CODELIST_BIOTOPES_EXTRA_CODES_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_HANDLER_ID
from compensation.models import EcoAccount
from intervention.models import Intervention
@@ -20,7 +25,7 @@ from intervention.models import Intervention
class EcoAccountAutocomplete(Select2QuerySetView):
""" Autocomplete for ecoAccount entries
Only returns entries that are accessible for the requesting user and already are recorded
Only returns entries that are already recorded and not deleted
"""
def get_queryset(self):
@@ -29,7 +34,6 @@ class EcoAccountAutocomplete(Select2QuerySetView):
qs = EcoAccount.objects.filter(
deleted=None,
recorded__isnull=False,
users__in=[self.request.user],
).order_by(
"identifier"
)
@@ -48,14 +52,16 @@ class InterventionAutocomplete(Select2QuerySetView):
"""
def get_queryset(self):
if self.request.user.is_anonymous:
user = self.request.user
if user.is_anonymous:
return Intervention.objects.none()
qs = Intervention.objects.filter(
deleted=None,
users__in=[self.request.user],
Q(deleted=None) &
Q(users__in=[user]) |
Q(teams__in=user.teams.all())
).order_by(
"identifier"
)
).distinct()
if self.q:
qs = qs.filter(
Q(identifier__icontains=self.q) |
@@ -65,27 +71,40 @@ class InterventionAutocomplete(Select2QuerySetView):
class ShareUserAutocomplete(Select2QuerySetView):
""" Autocomplete for intervention entries
""" Autocomplete for share with single users
Only returns entries that are accessible for the requesting user
"""
def get_queryset(self):
if self.request.user.is_anonymous:
return User.objects.none()
exclude_user_ids = self.forwarded.get("users", [])
_exclude = {"id__in": exclude_user_ids}
qs = User.objects.all().exclude(
**_exclude
).order_by(
"username"
)
qs = User.objects.all()
if self.q:
# Due to privacy concerns only a full username match will return the proper user entry
qs = qs.filter(
Q(username=self.q) |
Q(email=self.q)
).distinct()
qs = qs.order_by("username")
return qs
class ShareTeamAutocomplete(Select2QuerySetView):
""" Autocomplete for share with teams
"""
def get_queryset(self):
if self.request.user.is_anonymous:
return Team.objects.none()
qs = Team.objects.all()
if self.q:
# Due to privacy concerns only a full username match will return the proper user entry
qs = qs.filter(
name__icontains=self.q
)
qs = qs.order_by(
"name"
)
return qs
@@ -139,6 +158,8 @@ class KonovaCodeAutocomplete(Select2GroupQuerySetView):
q_or |= Q(short_name__icontains=keyword)
q_or |= Q(parent__long_name__icontains=keyword)
q_or |= Q(parent__short_name__icontains=keyword)
q_or |= Q(parent__parent__long_name__icontains=keyword)
q_or |= Q(parent__parent__short_name__icontains=keyword)
_filter.add(q_or, Q.AND)
qs = qs.filter(_filter).distinct()
return qs
@@ -181,7 +202,7 @@ class CompensationActionDetailCodeAutocomplete(KonovaCodeAutocomplete):
def order_by(self, qs):
return qs.order_by(
"parent__long_name"
"long_name"
)
@@ -214,6 +235,41 @@ class BiotopeCodeAutocomplete(KonovaCodeAutocomplete):
def get_result_label(self, result):
return f"{result.long_name} ({result.short_name})"
def get_results(self, context):
"""Return the options grouped by a common related model.
Raises ImproperlyConfigured if self.group_by_name is not configured
"""
if not self.group_by_related:
raise ImproperlyConfigured("Missing group_by_related.")
super_groups = collections.OrderedDict()
object_list = context['object_list']
for result in object_list:
group = result.parent if result.parent else None
group_name = f"{group.long_name} ({group.short_name})" if group else UNGROUPED
super_group = result.parent.parent if result.parent else None
super_group_name = f"{super_group.long_name} ({super_group.short_name})" if super_group else UNGROUPED
super_groups.setdefault(super_group_name, {})
super_groups[super_group_name].setdefault(group_name, [])
super_groups[super_group_name][group_name].append(result)
return [{
'id': None,
'text': super_group,
'children': [{
"id": None,
"text": group,
"children": [{
'id': self.get_result_value(result),
'text': self.get_result_label(result),
'selected_text': self.get_selected_result_label(result),
} for result in results]
} for group, results in groups.items()]
} for super_group, groups in super_groups.items()]
class BiotopeExtraCodeAutocomplete(KonovaCodeAutocomplete):
"""
@@ -239,7 +295,7 @@ class BiotopeExtraCodeAutocomplete(KonovaCodeAutocomplete):
qs (QuerySet): The ordered queryset
"""
return qs.order_by(
"parent__long_name",
"long_name",
)
def get_result_label(self, result):
@@ -284,6 +340,11 @@ class RegistrationOfficeCodeAutocomplete(KonovaCodeAutocomplete):
self.c = CODELIST_REGISTRATION_OFFICE_ID
super().__init__(*args, **kwargs)
def order_by(self, qs):
return qs.order_by(
"parent__long_name"
)
class ConservationOfficeCodeAutocomplete(KonovaCodeAutocomplete):
"""
@@ -297,4 +358,19 @@ class ConservationOfficeCodeAutocomplete(KonovaCodeAutocomplete):
super().__init__(*args, **kwargs)
def get_result_label(self, result):
return f"{result.long_name} ({result.short_name})"
return f"{result.long_name} ({result.short_name})"
class HandlerCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_HANDLER_ID
super().__init__(*args, **kwargs)
def get_result_label(self, result):
return result.long_name

View File

@@ -145,26 +145,20 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
class Meta:
abstract = True
def _filter_parcel_reference(self, queryset, name, value, filter_value) -> QuerySet:
""" Filters the parcel entries by a given filter_value.
filter_value may already include further filter annotations like 'xy__icontains'
def _filter_parcel_reference(self, queryset, filter_q) -> QuerySet:
""" Filters the parcel entries by a given filter_q
Args:
queryset ():
name ():
value ():
filter_value ():
queryset (QuerySet): The queryset
filter_q (Q): The Q-style filter expression
Returns:
"""
_filter = {
filter_value: value
}
matching_parcels = Parcel.objects.filter(
**_filter
filter_q
)
related_geoms = matching_parcels.values(
"geometries"
).distinct()
@@ -185,8 +179,9 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
"""
matching_districts = District.objects.filter(
krs=value
)
Q(name__icontains=value) |
Q(key__icontains=value)
).distinct()
matching_parcels = Parcel.objects.filter(
district__in=matching_districts
)
@@ -209,7 +204,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
Returns:
"""
queryset = self._filter_parcel_reference(queryset, name, value, "gmrkng__istartswith")
queryset = self._filter_parcel_reference(
queryset,
Q(parcel_group__name__icontains=value) | Q(parcel_group__key__icontains=value),
)
return queryset
def filter_parcel(self, queryset, name, value) -> QuerySet:
@@ -224,7 +222,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
"""
value = value.replace("-", "")
queryset = self._filter_parcel_reference(queryset, name, value, "flr")
queryset = self._filter_parcel_reference(
queryset,
Q(flr=value),
)
return queryset
def filter_parcel_counter(self, queryset, name, value) -> QuerySet:
@@ -239,7 +240,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
"""
value = value.replace("-", "")
queryset = self._filter_parcel_reference(queryset, name, value, "flrstck_zhlr")
queryset = self._filter_parcel_reference(
queryset,
Q(flrstck_zhlr=value)
)
return queryset
def filter_parcel_number(self, queryset, name, value) -> QuerySet:
@@ -254,7 +258,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
"""
value = value.replace("-", "")
queryset = self._filter_parcel_reference(queryset, name, value, "flrstck_nnr")
queryset = self._filter_parcel_reference(
queryset,
Q(flrstck_nnr=value),
)
return queryset
@@ -297,8 +304,9 @@ class ShareableTableFilterMixin(django_filters.FilterSet):
"""
if not value:
return queryset.filter(
users__in=[self.user], # requesting user has access
)
Q(users__in=[self.user]) | # requesting user has access
Q(teams__users__in=[self.user])
).distinct()
else:
return queryset

View File

@@ -57,6 +57,8 @@ class BaseForm(forms.Form):
self.has_required_fields = True
break
self.check_for_recorded_instance()
@abstractmethod
def save(self):
# To be implemented in subclasses!
@@ -136,6 +138,38 @@ class BaseForm(forms.Form):
set_class = set_class.replace(cls, "")
self.fields[field].widget.attrs["class"] = set_class
def check_for_recorded_instance(self):
""" Checks if the instance is recorded and runs some special logic if yes
If the instance is recorded, the form shall not display any possibility to
edit any data. Instead, the users should get some information about why they can not edit anything.
There are situations where the form should be rendered regularly,
e.g deduction forms for (recorded) eco accounts.
Returns:
"""
from intervention.forms.modalForms import NewDeductionModalForm, EditEcoAccountDeductionModalForm, \
RemoveEcoAccountDeductionModalForm
is_none = self.instance is None
is_other_data_type = not isinstance(self.instance, BaseObject)
is_deduction_form = isinstance(
self,
(
NewDeductionModalForm,
EditEcoAccountDeductionModalForm,
RemoveEcoAccountDeductionModalForm,
)
)
if is_none or is_other_data_type or is_deduction_form:
# Do nothing
return
if self.instance.is_recorded:
self.template = "form/recorded_no_edit.html"
class RemoveForm(BaseForm):
check = forms.BooleanField(
@@ -410,7 +444,6 @@ class NewDocumentModalForm(BaseModalForm):
super().__init__(*args, **kwargs)
self.form_title = _("Add new document")
self.form_caption = _("")
self.template = "modal/modal_form.html"
self.form_attrs = {
"enctype": "multipart/form-data", # important for file upload
}
@@ -471,6 +504,7 @@ class EditDocumentModalForm(NewDocumentModalForm):
def __init__(self, *args, **kwargs):
self.document = kwargs.pop("document", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit document")
form_data = {
"title": self.document.title,
"comment": self.document.comment,
@@ -596,4 +630,12 @@ class RecordModalForm(BaseModalForm):
self.instance.set_unrecorded(self.user)
else:
self.instance.set_recorded(self.user)
return self.instance
return self.instance
def check_for_recorded_instance(self):
""" Overwrite the check method for doing nothing on the RecordModalForm
Returns:
"""
pass

View File

@@ -9,7 +9,7 @@ from compensation.models import CompensationState, Compensation, EcoAccount, Com
from ema.models import Ema
from intervention.models import Intervention
from konova.management.commands.setup import BaseKonovaCommand
from konova.models import Deadline, Geometry, Parcel, District
from konova.models import Deadline, Geometry, Parcel, District, Municipal, ParcelGroup
from user.models import UserActionLogEntry, UserAction
@@ -271,13 +271,26 @@ class Command(BaseKonovaCommand):
self._write_success("No unused states found.")
self._break_line()
def __sanitize_parcel_sub_type(self, cls):
unrelated_entries = cls.objects.filter(
parcels=None,
)
num_unrelated_entries = unrelated_entries.count()
cls_name = cls.__name__
if num_unrelated_entries > 0:
self._write_error(f"Found {num_unrelated_entries} unrelated {cls_name} entries. Delete now...")
unrelated_entries.delete()
self._write_success(f"Unrelated {cls_name} deleted.")
else:
self._write_success(f"No unrelated {cls_name} found.")
def sanitize_parcels_and_districts(self):
""" Removes unattached parcels and districts
Returns:
"""
self._write_warning("=== Sanitize parcels and districts ===")
self._write_warning("=== Sanitize administrative spatial references ===")
unrelated_parcels = Parcel.objects.filter(
geometries=None,
)
@@ -289,16 +302,12 @@ class Command(BaseKonovaCommand):
else:
self._write_success("No unrelated parcels found.")
unrelated_districts = District.objects.filter(
parcels=None,
)
num_unrelated_districts = unrelated_districts.count()
if num_unrelated_districts > 0:
self._write_error(f"Found {num_unrelated_districts} unrelated district entries. Delete now...")
unrelated_districts.delete()
self._write_success("Unrelated districts deleted.")
else:
self._write_success("No unrelated districts found.")
self._break_line()
sub_types = [
District,
Municipal,
ParcelGroup
]
for sub_type in sub_types:
self.__sanitize_parcel_sub_type(sub_type)
self._break_line()

View File

@@ -5,6 +5,10 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.01.22
"""
import datetime
from django.contrib.gis.db.models.functions import Area
from konova.management.commands.setup import BaseKonovaCommand
from konova.models import Geometry, Parcel, District
@@ -23,12 +27,21 @@ class Command(BaseKonovaCommand):
num_parcels_before = Parcel.objects.count()
num_districts_before = District.objects.count()
self._write_warning("=== Update parcels and districts ===")
# Order geometries by size to process smaller once at first
geometries = Geometry.objects.all().exclude(
geom=None
).annotate(area=Area("geom")).order_by(
'area'
)
self._write_warning(f"Process parcels for {geometries.count()} geometry entries now ...")
i = 0
num_geoms = geometries.count()
for geometry in geometries:
self._write_warning(f"--- {datetime.datetime.now()} Process {geometry.id} now ...")
geometry.update_parcels()
self._write_warning(f"--- Processed {geometry.get_underlying_parcels().count()} underlying parcels")
i += 1
self._write_warning(f"--- {i}/{num_geoms} processed")
num_parcels_after = Parcel.objects.count()
num_districts_after = District.objects.count()

View File

@@ -0,0 +1,40 @@
# Generated by Django 3.1.3 on 2022-02-16 07:56
from django.db import migrations, transaction
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_AFTER_STATE_BIOTOPES__ID
def migrate_biotopes_from_974_to_654(apps, schema_editor):
KonovaCode = apps.get_model("codelist", "KonovaCode")
CompensationState = apps.get_model("compensation", "CompensationState")
all_states = CompensationState.objects.all()
with transaction.atomic():
for state in all_states:
new_biotope_code = KonovaCode.objects.get(
code_lists__in=[CODELIST_BIOTOPES_ID],
is_selectable=True,
is_archived=False,
short_name=state.biotope_type.short_name,
)
state.biotope_type = new_biotope_code
state.save()
all_states = CompensationState.objects.all()
after_state_list_elements = all_states.filter(
biotope_type__code_lists__in=[CODELIST_AFTER_STATE_BIOTOPES__ID]
)
if after_state_list_elements.count() > 0:
raise Exception("Still states with wrong codelist entries!")
class Migration(migrations.Migration):
dependencies = [
('konova', '0004_auto_20220209_0839'),
]
operations = [
migrations.RunPython(migrate_biotopes_from_974_to_654),
]

View File

@@ -0,0 +1,71 @@
# Generated by Django 3.1.3 on 2022-04-11 06:35
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('konova', '0005_auto_20220216_0856'),
]
operations = [
migrations.CreateModel(
name='Municipal',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('key', models.IntegerField(blank=True, help_text='Represents Gemeindeschlüssel', null=True)),
('name', models.CharField(blank=True, help_text='Gemeinde', max_length=1000, null=True)),
],
options={
'abstract': False,
},
),
migrations.RenameField(
model_name='district',
old_name='krs',
new_name='name',
),
migrations.RemoveField(
model_name='district',
name='gmnd',
),
migrations.RemoveField(
model_name='parcel',
name='gmrkng',
),
migrations.AddField(
model_name='district',
name='key',
field=models.IntegerField(blank=True, help_text='Represents Kreisschlüssel', null=True),
),
migrations.CreateModel(
name='ParcelGroup',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('key', models.IntegerField(blank=True, help_text='Represents Gemarkungsschlüssel', null=True)),
('name', models.CharField(blank=True, help_text='Gemarkung', max_length=1000, null=True)),
('municipal', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='konova.municipal')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='municipal',
name='district',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='konova.district'),
),
migrations.AddField(
model_name='parcel',
name='municipal',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parcels', to='konova.municipal'),
),
migrations.AddField(
model_name='parcel',
name='parcel_group',
field=models.ForeignKey(blank=True, help_text='Gemarkung', null=True, on_delete=django.db.models.deletion.SET_NULL, to='konova.parcelgroup'),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 3.1.3 on 2022-04-11 06:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('konova', '0006_auto_20220411_0835'),
]
operations = [
migrations.AlterField(
model_name='district',
name='key',
field=models.CharField(blank=True, help_text='Represents Kreisschlüssel', max_length=255, null=True),
),
migrations.AlterField(
model_name='municipal',
name='key',
field=models.CharField(blank=True, help_text='Represents Gemeindeschlüssel', max_length=255, null=True),
),
migrations.AlterField(
model_name='parcelgroup',
name='key',
field=models.CharField(blank=True, help_text='Represents Gemarkungsschlüssel', max_length=255, null=True),
),
]

View File

@@ -0,0 +1,48 @@
# Generated by Django 3.1.3 on 2022-04-11 07:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('konova', '0007_auto_20220411_0848'),
]
operations = [
migrations.AlterField(
model_name='municipal',
name='key',
field=models.CharField(blank=True, help_text='Represents Kreisschlüssel', max_length=255, null=True),
),
migrations.AlterField(
model_name='municipal',
name='name',
field=models.CharField(blank=True, help_text='Kreis', max_length=1000, null=True),
),
migrations.AlterField(
model_name='parcel',
name='flr',
field=models.IntegerField(blank=True, help_text='Flur', null=True),
),
migrations.AlterField(
model_name='parcel',
name='flrstck_nnr',
field=models.IntegerField(blank=True, help_text='Flurstücksnenner', null=True),
),
migrations.AlterField(
model_name='parcel',
name='flrstck_zhlr',
field=models.IntegerField(blank=True, help_text='Flurstückszähler', null=True),
),
migrations.AlterField(
model_name='parcelgroup',
name='key',
field=models.CharField(blank=True, help_text='Represents Kreisschlüssel', max_length=255, null=True),
),
migrations.AlterField(
model_name='parcelgroup',
name='name',
field=models.CharField(blank=True, help_text='Kreis', max_length=1000, null=True),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.1.3 on 2022-04-11 08:04
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('konova', '0008_auto_20220411_0914'),
]
operations = [
migrations.AlterField(
model_name='parcel',
name='parcel_group',
field=models.ForeignKey(blank=True, help_text='Gemarkung', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parcels', to='konova.parcelgroup'),
),
]

View File

@@ -20,6 +20,9 @@ class Geometry(BaseResource):
from konova.settings import DEFAULT_SRID
geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID)
def __str__(self):
return str(self.id)
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self.check_for_conflicts()
@@ -99,7 +102,7 @@ class Geometry(BaseResource):
Returns:
"""
from konova.models import Parcel, District, ParcelIntersection
from konova.models import Parcel, District, ParcelIntersection, Municipal, ParcelGroup
parcel_fetcher = ParcelWFSFetcher(
geometry_id=self.id,
)
@@ -110,20 +113,38 @@ class Geometry(BaseResource):
_now = timezone.now()
underlying_parcels = []
for result in fetched_parcels:
fetched_parcel = result[typename]
parcel_properties = result["properties"]
# There could be parcels which include the word 'Flur',
# which needs to be deleted and just keep the numerical values
## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE!
flr_val = fetched_parcel["ave:flur"].replace("Flur ", "")
parcel_obj = Parcel.objects.get_or_create(
gmrkng=fetched_parcel["ave:gemarkung"],
flr=flr_val,
flrstck_nnr=fetched_parcel['ave:flstnrnen'],
flrstck_zhlr=fetched_parcel['ave:flstnrzae'],
)[0]
flr_val = parcel_properties["flur"].replace("Flur ", "")
district = District.objects.get_or_create(
gmnd=fetched_parcel["ave:gemeinde"],
krs=fetched_parcel["ave:kreis"],
key=parcel_properties["kreisschl"],
name=parcel_properties["kreis"],
)[0]
municipal = Municipal.objects.get_or_create(
key=parcel_properties["gmdschl"],
name=parcel_properties["gemeinde"],
district=district,
)[0]
parcel_group = ParcelGroup.objects.get_or_create(
key=parcel_properties["gemaschl"],
name=parcel_properties["gemarkung"],
municipal=municipal,
)[0]
flrstck_nnr = parcel_properties['flstnrnen']
if not flrstck_nnr:
flrstck_nnr = None
flrstck_zhlr = parcel_properties['flstnrzae']
if not flrstck_zhlr:
flrstck_zhlr = None
parcel_obj = Parcel.objects.get_or_create(
district=district,
municipal=municipal,
parcel_group=parcel_group,
flr=flr_val,
flrstck_nnr=flrstck_nnr,
flrstck_zhlr=flrstck_zhlr,
)[0]
parcel_obj.district = district
parcel_obj.updated_on = _now
@@ -155,9 +176,10 @@ class Geometry(BaseResource):
parcels = self.parcels.filter(
parcelintersection__calculated_on__isnull=False,
).prefetch_related(
"district"
"district",
"municipal",
).order_by(
"gmrkng",
"municipal__name",
)
return parcels

View File

@@ -15,8 +15,10 @@ from django.db.models import QuerySet
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP, LANIS_ZOOM_LUT, LANIS_LINK_TEMPLATE
from konova.tasks import celery_send_mail_shared_access_removed, celery_send_mail_shared_access_given, \
celery_send_mail_shared_data_recorded, celery_send_mail_shared_data_unrecorded, \
celery_send_mail_shared_data_deleted, celery_send_mail_shared_data_checked
from user.models import User
celery_send_mail_shared_data_deleted, celery_send_mail_shared_data_checked, \
celery_send_mail_shared_access_given_team, celery_send_mail_shared_access_removed_team, \
celery_send_mail_shared_data_checked_team, celery_send_mail_shared_data_deleted_team, \
celery_send_mail_shared_data_unrecorded_team, celery_send_mail_shared_data_recorded_team
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpRequest
from django.utils.timezone import now
@@ -28,7 +30,6 @@ from intervention.settings import INTERVENTION_IDENTIFIER_LENGTH, INTERVENTION_I
from konova.utils import generators
from konova.utils.generators import generate_random_string
from konova.utils.message_templates import CHECKED_RECORDED_RESET, GEOMETRY_CONFLICT_WITH_TEMPLATE
from user.models import UserActionLogEntry, UserAction
class UuidModel(models.Model):
@@ -50,14 +51,14 @@ class BaseResource(UuidModel):
A basic resource model, which defines attributes for every derived model
"""
created = models.ForeignKey(
UserActionLogEntry,
"user.UserActionLogEntry",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='+'
)
modified = models.ForeignKey(
UserActionLogEntry,
"user.UserActionLogEntry",
on_delete=models.SET_NULL,
null=True,
blank=True,
@@ -94,9 +95,9 @@ class BaseObject(BaseResource):
"""
identifier = models.CharField(max_length=1000, null=True, blank=True)
title = models.CharField(max_length=1000, null=True, blank=True)
deleted = models.ForeignKey(UserActionLogEntry, on_delete=models.SET_NULL, null=True, blank=True, related_name='+')
deleted = models.ForeignKey("user.UserActionLogEntry", on_delete=models.SET_NULL, null=True, blank=True, related_name='+')
comment = models.TextField(null=True, blank=True)
log = models.ManyToManyField(UserActionLogEntry, blank=True, help_text="Keeps all user actions of an object", editable=False)
log = models.ManyToManyField("user.UserActionLogEntry", blank=True, help_text="Keeps all user actions of an object", editable=False)
class Meta:
abstract = True
@@ -105,7 +106,7 @@ class BaseObject(BaseResource):
def set_status_messages(self, request: HttpRequest):
raise NotImplementedError
def mark_as_deleted(self, user: User, send_mail: bool = True):
def mark_as_deleted(self, user, send_mail: bool = True):
""" Mark an entry as deleted
Does not delete from database but sets a timestamp for being deleted on and which user deleted the object
@@ -116,6 +117,7 @@ class BaseObject(BaseResource):
Returns:
"""
from user.models import UserActionLogEntry
if self.deleted:
# Nothing to do here
return
@@ -131,9 +133,14 @@ class BaseObject(BaseResource):
for user_id in shared_users:
celery_send_mail_shared_data_deleted.delay(self.identifier, self.title, user_id)
# Send mail
shared_teams = self.shared_teams.values_list("id", flat=True)
for team_id in shared_teams:
celery_send_mail_shared_data_deleted_team.delay(self.identifier, self.title, team_id)
self.save()
def mark_as_edited(self, performing_user: User, request: HttpRequest = None, edit_comment: str = None):
def mark_as_edited(self, performing_user, request: HttpRequest = None, edit_comment: str = None):
""" In case the object or a related object changed the log history needs to be updated
Args:
@@ -144,13 +151,14 @@ class BaseObject(BaseResource):
Returns:
"""
from user.models import UserActionLogEntry
edit_action = UserActionLogEntry.get_edited_action(performing_user, edit_comment)
self.modified = edit_action
self.log.add(edit_action)
self.save()
return edit_action
def add_log_entry(self, action: UserAction, user: User, comment: str):
def add_log_entry(self, action, user, comment: str):
""" Wraps adding of UserActionLogEntry to log
Args:
@@ -161,6 +169,7 @@ class BaseObject(BaseResource):
Returns:
"""
from user.models import UserActionLogEntry
user_action = UserActionLogEntry.objects.create(
user=user,
action=action,
@@ -229,7 +238,7 @@ class RecordableObjectMixin(models.Model):
"""
# Refers to "verzeichnen"
recorded = models.OneToOneField(
UserActionLogEntry,
"user.UserActionLogEntry",
on_delete=models.SET_NULL,
null=True,
blank=True,
@@ -240,7 +249,7 @@ class RecordableObjectMixin(models.Model):
class Meta:
abstract = True
def set_unrecorded(self, user: User):
def set_unrecorded(self, user):
""" Perform unrecording
Args:
@@ -249,6 +258,7 @@ class RecordableObjectMixin(models.Model):
Returns:
"""
from user.models import UserActionLogEntry
if not self.recorded:
return None
action = UserActionLogEntry.get_unrecorded_action(user)
@@ -256,13 +266,18 @@ class RecordableObjectMixin(models.Model):
self.save()
self.log.add(action)
shared_users = self.users.all().values_list("id", flat=True)
shared_users = self.shared_users.values_list("id", flat=True)
shared_teams = self.shared_teams.values_list("id", flat=True)
for user_id in shared_users:
celery_send_mail_shared_data_unrecorded.delay(self.identifier, self.title, user_id)
for team_id in shared_teams:
celery_send_mail_shared_data_unrecorded_team.delay(self.identifier, self.title, team_id)
return action
def set_recorded(self, user: User):
def set_recorded(self, user):
""" Perform recording
Args:
@@ -271,20 +286,28 @@ class RecordableObjectMixin(models.Model):
Returns:
"""
from user.models import UserActionLogEntry
if self.recorded:
return None
self.unshare_with_default_users()
action = UserActionLogEntry.get_recorded_action(user)
self.recorded = action
self.save()
self.log.add(action)
shared_users = self.users.all().values_list("id", flat=True)
shared_users = self.shared_users.values_list("id", flat=True)
shared_teams = self.shared_teams.values_list("id", flat=True)
for user_id in shared_users:
celery_send_mail_shared_data_recorded.delay(self.identifier, self.title, user_id)
for team_id in shared_teams:
celery_send_mail_shared_data_recorded_team.delay(self.identifier, self.title, team_id)
return action
def unrecord(self, performing_user: User, request: HttpRequest = None):
def unrecord(self, performing_user, request: HttpRequest = None):
""" Unrecords a dataset
Args:
@@ -314,11 +337,20 @@ class RecordableObjectMixin(models.Model):
"""
raise NotImplementedError("Implement this in the subclass!")
@property
def is_recorded(self):
""" Getter for record status as property
Returns:
"""
return self.recorded is not None
class CheckableObjectMixin(models.Model):
# Checks - Refers to "Genehmigen" but optional
checked = models.OneToOneField(
UserActionLogEntry,
"user.UserActionLogEntry",
on_delete=models.SET_NULL,
null=True,
blank=True,
@@ -346,7 +378,7 @@ class CheckableObjectMixin(models.Model):
self.save()
return None
def set_checked(self, user: User) -> UserActionLogEntry:
def set_checked(self, user):
""" Perform checking
Args:
@@ -355,6 +387,7 @@ class CheckableObjectMixin(models.Model):
Returns:
"""
from user.models import UserActionLogEntry
if self.checked:
# Nothing to do
return
@@ -363,17 +396,23 @@ class CheckableObjectMixin(models.Model):
self.save()
# Send mail
shared_users = self.users.all().values_list("id", flat=True)
shared_users = self.shared_users.values_list("id", flat=True)
for user_id in shared_users:
celery_send_mail_shared_data_checked.delay(self.identifier, self.title, user_id)
# Send mail
shared_teams = self.shared_teams.values_list("id", flat=True)
for team_id in shared_teams:
celery_send_mail_shared_data_checked_team.delay(self.identifier, self.title, team_id)
self.log.add(action)
return action
class ShareableObjectMixin(models.Model):
# Users having access on this object
users = models.ManyToManyField(User, help_text="Users having access (data shared with)")
users = models.ManyToManyField("user.User", help_text="Users having access (data shared with)")
teams = models.ManyToManyField("user.Team", help_text="Teams having access (data shared with)")
access_token = models.CharField(
max_length=255,
null=True,
@@ -420,7 +459,7 @@ class ShareableObjectMixin(models.Model):
self.access_token = token
self.save()
def is_shared_with(self, user: User):
def is_shared_with(self, user):
""" Access check
Checks whether a given user has access to this object
@@ -431,9 +470,36 @@ class ShareableObjectMixin(models.Model):
Returns:
"""
return self.users.filter(id=user.id)
directly_shared = self.users.filter(id=user.id).exists()
team_shared = self.teams.filter(
users__in=[user]
).exists()
is_shared = directly_shared or team_shared
return is_shared
def share_with(self, user: User):
def share_with_team(self, team):
""" Adds team to list of shared access teans
Args:
team (Team): The team to be added to the object
Returns:
"""
self.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.teams.set(team_list)
def share_with_user(self, user):
""" Adds user to list of shared access users
Args:
@@ -445,7 +511,7 @@ class ShareableObjectMixin(models.Model):
if not self.is_shared_with(user):
self.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:
@@ -456,8 +522,8 @@ class ShareableObjectMixin(models.Model):
"""
self.users.set(user_list)
def update_sharing_user(self, form):
""" Adds a new user with shared access to the object
def _update_shared_teams(self, form):
""" Updates shared access on the object for teams
Args:
form (ShareModalForm): The form holding the data
@@ -466,25 +532,65 @@ class ShareableObjectMixin(models.Model):
"""
form_data = form.cleaned_data
shared_teams = self.shared_teams
keep_accessing_users = form_data["users"]
new_accessing_users = list(form_data["user_select"].values_list("id", flat=True))
accessing_users = keep_accessing_users + new_accessing_users
users = User.objects.filter(
# Fetch selected teams and find out which user IDs are in removed teams -> mails need to be sent
accessing_teams = form_data["teams"]
removed_teams = shared_teams.exclude(
id__in=accessing_teams
).values_list("id", flat=True)
new_teams = accessing_teams.exclude(
id__in=shared_teams
).values_list("id", flat=True)
for team_id in new_teams:
celery_send_mail_shared_access_given_team.delay(self.identifier, self.title, team_id)
for team_id in removed_teams:
celery_send_mail_shared_access_removed_team.delay(self.identifier, self.title, team_id)
self.share_with_team_list(accessing_teams)
def _update_shared_users(self, form):
""" Updates shared access on the object for single users
Args:
form (ShareModalForm): The form holding the data
Returns:
"""
form_data = form.cleaned_data
shared_users = self.shared_users
# Fetch selected users
accessing_users = form_data["users"]
removed_users = shared_users.exclude(
id__in=accessing_users
)
removed_users = self.users.all().exclude(
id__in=accessing_users
).values("id")
).values_list("id", flat=True)
new_users = accessing_users.exclude(
id__in=shared_users
).values_list("id", flat=True)
# Send mails
for user in removed_users:
celery_send_mail_shared_access_removed.delay(self.identifier, self.title, user["id"])
for user in new_accessing_users:
celery_send_mail_shared_access_given.delay(self.identifier, self.title, user)
for user_id in removed_users:
celery_send_mail_shared_access_removed.delay(self.identifier, self.title, user_id)
for user_id in new_users:
celery_send_mail_shared_access_given.delay(self.identifier, self.title, user_id)
# Set new shared users
self.share_with_list(users)
self.share_with_user_list(accessing_users)
def update_shared_access(self, form):
""" Updates shared access on the object
Args:
form (ShareModalForm): The form holding the data
Returns:
"""
self._update_shared_teams(form)
self._update_shared_users(form)
@property
def shared_users(self) -> QuerySet:
@@ -495,6 +601,15 @@ class ShareableObjectMixin(models.Model):
"""
return self.users.all()
@property
def shared_teams(self) -> QuerySet:
""" Shortcut for fetching the teams which have shared access on this object
Returns:
teams (QuerySet)
"""
return self.teams.all()
@abstractmethod
def get_share_url(self):
""" Returns the share url for the object
@@ -504,6 +619,26 @@ class ShareableObjectMixin(models.Model):
"""
raise NotImplementedError("Must be implemented in subclasses!")
def unshare_with_default_users(self):
""" Removes all shared users from direct shared access which are only default group users
Returns:
"""
from konova.utils.user_checks import is_default_group_only
users = self.shared_users
cleaned_users = []
default_users = []
for user in users:
if not is_default_group_only(user):
cleaned_users.append(user)
else:
default_users.append(user)
self.share_with_user_list(cleaned_users)
for user in default_users:
celery_send_mail_shared_access_removed.delay(self.identifier, self.title, user.id)
class GeoReferencedMixin(models.Model):
geometry = models.ForeignKey("konova.Geometry", null=True, blank=True, on_delete=models.SET_NULL)

View File

@@ -10,8 +10,64 @@ from django.db import models
from konova.models import UuidModel
class AdministrativeSpatialReference(models.Model):
key = models.CharField(
max_length=255,
help_text="Represents Kreisschlüssel",
null=True,
blank=True
)
name = models.CharField(
max_length=1000,
help_text="Kreis",
null=True,
blank=True,
)
class Meta:
abstract = True
def __str__(self):
return f"{self.name} ({self.key})"
@property
def table_str(self):
return f"{self.name} ({self.key})"
class District(UuidModel, AdministrativeSpatialReference):
""" The model District refers to "Kreis"
"""
pass
class Municipal(UuidModel, AdministrativeSpatialReference):
""" The model Municipal refers to "Gemeinde"
"""
district = models.ForeignKey(
District,
on_delete=models.SET_NULL,
null=True,
blank=True,
)
class ParcelGroup(UuidModel, AdministrativeSpatialReference):
""" The model ParcelGroup refers to "Gemarkung", which is defined as a loose group of parcels
"""
municipal = models.ForeignKey(
Municipal,
on_delete=models.SET_NULL,
null=True,
blank=True,
)
class Parcel(UuidModel):
""" The Parcel model holds administrative data on the covered properties.
""" The Parcel model holds administrative data on covered properties.
Due to the unique but relevant naming of the administrative data, we have to use these namings as field
names in german. Any try to translate them to English result in strange or insufficient translations.
@@ -24,59 +80,34 @@ class Parcel(UuidModel):
"""
geometries = models.ManyToManyField("konova.Geometry", blank=True, related_name="parcels", through='ParcelIntersection')
district = models.ForeignKey("konova.District", on_delete=models.SET_NULL, null=True, blank=True, related_name="parcels")
gmrkng = models.CharField(
max_length=1000,
municipal = models.ForeignKey("konova.Municipal", on_delete=models.SET_NULL, null=True, blank=True, related_name="parcels")
parcel_group = models.ForeignKey(
"konova.ParcelGroup",
on_delete=models.SET_NULL,
help_text="Gemarkung",
null=True,
blank=True,
related_name="parcels"
)
flrstck_nnr = models.CharField(
max_length=1000,
flr = models.IntegerField(
help_text="Flur",
null=True,
blank=True,
)
flrstck_nnr = models.IntegerField(
help_text="Flurstücksnenner",
null=True,
blank=True,
)
flrstck_zhlr = models.CharField(
max_length=1000,
flrstck_zhlr = models.IntegerField(
help_text="Flurstückszähler",
null=True,
blank=True,
)
flr = models.CharField(
max_length=1000,
help_text="Flur",
null=True,
blank=True,
)
updated_on = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.gmrkng} | {self.flr} | {self.flrstck_zhlr} | {self.flrstck_nnr}"
class District(UuidModel):
""" The model District holds more coarse information, such as Kreis, Verbandsgemeinde and Gemeinde.
There might be the case that a geometry lies on a hundred Parcel entries but only on one District entry.
Therefore a geometry can have a lot of relations to Parcel entries but only a few or only a single one to one
District.
"""
gmnd = models.CharField(
max_length=1000,
help_text="Gemeinde",
null=True,
blank=True,
)
krs = models.CharField(
max_length=1000,
help_text="Kreis",
null=True,
blank=True,
)
def __str__(self):
return f"{self.gmnd} | {self.krs}"
return f"{self.parcel_group} | {self.flr} | {self.flrstck_zhlr} | {self.flrstck_nnr}"
class ParcelIntersection(UuidModel):

View File

@@ -242,15 +242,24 @@ Similar to bootstraps 'shadow-lg'
.select2-results__option--highlighted{
background-color: var(--rlp-red) !important;
}
/*
.select2-container--default .select2-results__group{
background-color: var(--rlp-gray-light);
}
.select2-container--default .select2-results__option .select2-results__option{
padding-left: 2em !important;
padding-left: 1em !important;
}
*/
.select2-results__options--nested{
padding-left: 1em !important;
}
.select2-container--default .select2-results > .select2-results__options{
max-height: 500px !important;
}
/*
.select2-container--default .select2-results__option .select2-results__option{
padding-left: 2em;
}
}
*/

View File

@@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/3.1/ref/settings/
"""
import os
from django.utils.translation import gettext_lazy as _
from django.conf.locale.de import formats as de_formats
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = os.path.dirname(
@@ -162,9 +163,15 @@ LANGUAGES = [
USE_THOUSAND_SEPARATOR = True
# Regular python relevant date/datetime formatting
DEFAULT_DATE_TIME_FORMAT = '%d.%m.%Y %H:%M:%S'
DEFAULT_DATE_FORMAT = '%d.%m.%Y'
# Template relevant date/datetime formatting
# See the Note on here: https://docs.djangoproject.com/en/3.2/ref/templates/builtins/#date
de_formats.DATETIME_FORMAT = "d.m.Y, H:i"
de_formats.DATE_FORMAT = "d.m.Y"
TIME_ZONE = 'Europe/Berlin'
USE_I18N = True

View File

@@ -19,6 +19,6 @@ PAGE_SIZE_OPTIONS_TUPLES = [
(50, 50),
(100, 100),
]
PAGE_SIZE_DEFAULT = 5
PAGE_SIZE_DEFAULT = 10
PAGE_SIZE_MAX = 100
PAGE_DEFAULT = 1

View File

@@ -38,6 +38,20 @@ def celery_send_mail_shared_access_given(obj_identifier, obj_title=None, user_id
user.send_mail_shared_access_given(obj_identifier, obj_title)
@shared_task
def celery_send_mail_shared_access_removed_team(obj_identifier, obj_title=None, team_id=None):
from user.models import Team
team = Team.objects.get(id=team_id)
team.send_mail_shared_access_removed(obj_identifier, obj_title)
@shared_task
def celery_send_mail_shared_access_given_team(obj_identifier, obj_title=None, team_id=None):
from user.models import Team
team = Team.objects.get(id=team_id)
team.send_mail_shared_access_given_team(obj_identifier, obj_title)
@shared_task
def celery_send_mail_shared_data_recorded(obj_identifier, obj_title=None, user_id=None):
from user.models import User
@@ -52,6 +66,20 @@ def celery_send_mail_shared_data_unrecorded(obj_identifier, obj_title=None, user
user.send_mail_shared_data_unrecorded(obj_identifier, obj_title)
@shared_task
def celery_send_mail_shared_data_recorded_team(obj_identifier, obj_title=None, team_id=None):
from user.models import Team
team = Team.objects.get(id=team_id)
team.send_mail_shared_data_recorded(obj_identifier, obj_title)
@shared_task
def celery_send_mail_shared_data_unrecorded_team(obj_identifier, obj_title=None, team_id=None):
from user.models import Team
team = Team.objects.get(id=team_id)
team.send_mail_shared_data_unrecorded(obj_identifier, obj_title)
@shared_task
def celery_send_mail_shared_data_deleted(obj_identifier, obj_title=None, user_id=None):
from user.models import User
@@ -64,3 +92,17 @@ def celery_send_mail_shared_data_checked(obj_identifier, obj_title=None, user_id
from user.models import User
user = User.objects.get(id=user_id)
user.send_mail_shared_data_checked(obj_identifier, obj_title)
@shared_task
def celery_send_mail_shared_data_deleted_team(obj_identifier, obj_title=None, team_id=None):
from user.models import Team
team = Team.objects.get(id=team_id)
team.send_mail_shared_data_deleted(obj_identifier, obj_title)
@shared_task
def celery_send_mail_shared_data_checked_team(obj_identifier, obj_title=None, team_id=None):
from user.models import Team
team = Team.objects.get(id=team_id)
team.send_mail_shared_data_checked(obj_identifier, obj_title)

View File

@@ -1,37 +0,0 @@
{% load i18n %}
<div>
<h3>{% trans 'Spatial reference' %}</h3>
</div>
<div class="table-container w-100 scroll-300">
{% if parcels|length == 0 %}
<article class="alert alert-info">
{% blocktrans %}
If the geometry is not empty, the parcels are currently recalculated. Please refresh this page in a few moments.
{% endblocktrans %}
</article>
{% else %}
<table class="table table-hover">
<thead>
<tr>
<th scope="col">{% trans 'Kreis' %}</th>
<th scope="col">{% trans 'Gemarkung' %}</th>
<th scope="col">{% trans 'Parcel' %}</th>
<th scope="col">{% trans 'Parcel counter' %}</th>
<th scope="col">{% trans 'Parcel number' %}</th>
</tr>
</thead>
<tbody>
{% for parcel in parcels %}
<tr>
<td>{{parcel.district.krs|default_if_none:"-"}}</td>
<td>{{parcel.gmrkng|default_if_none:"-"}}</td>
<td>{{parcel.flr|default_if_none:"-"}}</td>
<td>{{parcel.flrstck_zhlr|default_if_none:"-"}}</td>
<td>{{parcel.flrstck_nnr|default_if_none:"-"}}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>

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