diff --git a/api/tests/v1/create/test_api_create.py b/api/tests/v1/create/test_api_create.py
index 72ece97..51a82e7 100644
--- a/api/tests/v1/create/test_api_create.py
+++ b/api/tests/v1/create/test_api_create.py
@@ -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"
diff --git a/api/tests/v1/delete/test_api_delete.py b/api/tests/v1/delete/test_api_delete.py
index cd016cf..350fdf2 100644
--- a/api/tests/v1/delete/test_api_delete.py
+++ b/api/tests/v1/delete/test_api_delete.py
@@ -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)
diff --git a/api/tests/v1/get/test_api_get.py b/api/tests/v1/get/test_api_get.py
index 38af864..953b0f6 100644
--- a/api/tests/v1/get/test_api_get.py
+++ b/api/tests/v1/get/test_api_get.py
@@ -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)
diff --git a/api/tests/v1/update/intervention_share_update_put_body.json b/api/tests/v1/update/intervention_share_update_put_body.json
new file mode 100644
index 0000000..c160aee
--- /dev/null
+++ b/api/tests/v1/update/intervention_share_update_put_body.json
@@ -0,0 +1,8 @@
+{
+ "users": [
+ "CHANGE_ME"
+ ],
+ "teams": [
+ "CHANGE_ME"
+ ]
+}
\ No newline at end of file
diff --git a/api/tests/v1/update/test_api_update.py b/api/tests/v1/update/test_api_update.py
index e371188..500aec2 100644
--- a/api/tests/v1/update/test_api_update.py
+++ b/api/tests/v1/update/test_api_update.py
@@ -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),))
@@ -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),))
@@ -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))
diff --git a/api/utils/serializer/serializer.py b/api/utils/serializer/serializer.py
index 443e10c..8d618e4 100644
--- a/api/utils/serializer/serializer.py
+++ b/api/utils/serializer/serializer.py
@@ -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
diff --git a/api/utils/serializer/v1/serializer.py b/api/utils/serializer/v1/serializer.py
index f56db81..caae2de 100644
--- a/api/utils/serializer/v1/serializer.py
+++ b/api/utils/serializer/v1/serializer.py
@@ -367,7 +367,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 +386,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 +398,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 +440,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()
],
diff --git a/api/views/v1/views.py b/api/views/v1/views.py
index 7789680..8da5d49 100644
--- a/api/views/v1/views.py
+++ b/api/views/v1/views.py
@@ -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,
diff --git a/api/views/views.py b/api/views/views.py
index fb4f6df..75d764e 100644
--- a/api/views/views.py
+++ b/api/views/views.py
@@ -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
diff --git a/codelist/models.py b/codelist/models.py
index d5be5b1..de7a210 100644
--- a/codelist/models.py
+++ b/codelist/models.py
@@ -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):
"""
diff --git a/codelist/settings.py b/codelist/settings.py
index 598a3a6..b4226b5 100644
--- a/codelist/settings.py
+++ b/codelist/settings.py
@@ -13,7 +13,8 @@ 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
diff --git a/compensation/filters.py b/compensation/filters.py
index d028e3c..b637709 100644
--- a/compensation/filters.py
+++ b/compensation/filters.py
@@ -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
diff --git a/compensation/forms/forms.py b/compensation/forms/forms.py
index 72b1a71..46b235f 100644
--- a/compensation/forms/forms.py
+++ b/compensation/forms/forms.py
@@ -400,7 +400,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)
diff --git a/compensation/forms/modalForms.py b/compensation/forms/modalForms.py
index 804ebad..cdcc26e 100644
--- a/compensation/forms/modalForms.py
+++ b/compensation/forms/modalForms.py
@@ -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
@@ -405,22 +406,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 +474,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)
@@ -496,7 +498,7 @@ class EditCompensationActionModalForm(NewActionModalForm):
self.action = kwargs.pop("action", None)
super().__init__(*args, **kwargs)
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 +508,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)
diff --git a/compensation/managers.py b/compensation/managers.py
index c97cd51..b496eb1 100644
--- a/compensation/managers.py
+++ b/compensation/managers.py
@@ -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
diff --git a/compensation/migrations/0004_auto_20220210_1402.py b/compensation/migrations/0004_auto_20220210_1402.py
new file mode 100644
index 0000000..7cc8126
--- /dev/null
+++ b/compensation/migrations/0004_auto_20220210_1402.py
@@ -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',
+ )
+ ]
diff --git a/compensation/migrations/0005_auto_20220218_0917.py b/compensation/migrations/0005_auto_20220218_0917.py
new file mode 100644
index 0000000..43e7db9
--- /dev/null
+++ b/compensation/migrations/0005_auto_20220218_0917.py
@@ -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'),
+ ),
+ ]
diff --git a/compensation/migrations/0006_ecoaccount_teams.py b/compensation/migrations/0006_ecoaccount_teams.py
new file mode 100644
index 0000000..14cf509
--- /dev/null
+++ b/compensation/migrations/0006_ecoaccount_teams.py
@@ -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'),
+ ),
+ ]
diff --git a/compensation/models/action.py b/compensation/models/action.py
index a557915..e54f53d 100644
--- a/compensation/models/action.py
+++ b/compensation/models/action.py
@@ -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):
diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py
index 4dd2b4c..ea69cef 100644
--- a/compensation/models/compensation.py
+++ b/compensation/models/compensation.py
@@ -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
diff --git a/compensation/templates/compensation/detail/compensation/includes/actions.html b/compensation/templates/compensation/detail/compensation/includes/actions.html
index 33037ec..87f4914 100644
--- a/compensation/templates/compensation/detail/compensation/includes/actions.html
+++ b/compensation/templates/compensation/detail/compensation/includes/actions.html
@@ -47,13 +47,15 @@
{% for action in actions %}
- {{ action.action_type }}
- {% if action.action_type_details.count > 0 %}
-
- {% for detail in action.action_type_details.all %}
- {{detail.long_name}}
- {% endfor %}
- {% endif %}
+ {% for type in action.action_type.all %}
+ {{type.parent.parent.long_name}} {% fa5_icon 'angle-right' %} {{type.parent.long_name}} {% fa5_icon 'angle-right' %} {{type.long_name}}
+
+ {% endfor %}
+ {% for detail in action.action_type_details.all %}
+ {{detail.long_name}}
+ {% empty %}
+ {% trans 'No action type details' %}
+ {% endfor %}
{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}
diff --git a/compensation/templates/compensation/detail/eco_account/includes/deductions.html b/compensation/templates/compensation/detail/eco_account/includes/deductions.html
index 10f177e..b91fb21 100644
--- a/compensation/templates/compensation/detail/eco_account/includes/deductions.html
+++ b/compensation/templates/compensation/detail/eco_account/includes/deductions.html
@@ -10,7 +10,7 @@
- {% if is_default_member and has_access %}
+ {% if is_default_member and obj.recorded %}
{% fa5_icon 'plus' %}
{% fa5_icon 'tree' %}
@@ -61,7 +61,7 @@
{{ deduction.surface|floatformat:2|intcomma }} m²
{{ deduction.created.timestamp|default_if_none:""|naturalday}}
- {% 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 %}
{% fa5_icon 'edit' %}
diff --git a/compensation/templates/compensation/detail/eco_account/includes/states-after.html b/compensation/templates/compensation/detail/eco_account/includes/states-after.html
index bead6f2..4fce2f0 100644
--- a/compensation/templates/compensation/detail/eco_account/includes/states-after.html
+++ b/compensation/templates/compensation/detail/eco_account/includes/states-after.html
@@ -48,13 +48,13 @@
{% for state in after_states %}
- {{ state.biotope_type }}
- {% if state.biotope_type_details.count > 0 %}
-
- {% for detail in state.biotope_type_details.all %}
- {{detail.long_name}}
- {% endfor %}
- {% endif %}
+ {{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})
+
+ {% for detail in state.biotope_type_details.all %}
+ {{detail.long_name}}
+ {% empty %}
+ {% trans 'No biotope type details' %}
+ {% endfor %}
{{ state.surface|floatformat:2 }} m²
diff --git a/compensation/templates/compensation/detail/eco_account/includes/states-before.html b/compensation/templates/compensation/detail/eco_account/includes/states-before.html
index c19b404..1c6311c 100644
--- a/compensation/templates/compensation/detail/eco_account/includes/states-before.html
+++ b/compensation/templates/compensation/detail/eco_account/includes/states-before.html
@@ -48,13 +48,13 @@
{% for state in before_states %}
- {{ state.biotope_type }}
- {% if state.biotope_type_details.count > 0 %}
-
- {% for detail in state.biotope_type_details.all %}
- {{detail.long_name}}
- {% endfor %}
- {% endif %}
+ {{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})
+
+ {% for detail in state.biotope_type_details.all %}
+ {{detail.long_name}}
+ {% empty %}
+ {% trans 'No biotope type details' %}
+ {% endfor %}
{{ state.surface|floatformat:2 }} m²
diff --git a/compensation/templates/compensation/detail/eco_account/view.html b/compensation/templates/compensation/detail/eco_account/view.html
index f276f93..ee4a0f7 100644
--- a/compensation/templates/compensation/detail/eco_account/view.html
+++ b/compensation/templates/compensation/detail/eco_account/view.html
@@ -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.
@@ -80,6 +81,10 @@
{% trans 'Shared with' %}
+ {% for team in obj.teams.all %}
+ {% include 'user/includes/team_data_modal_button.html' %}
+ {% endfor %}
+
{% for user in obj.users.all %}
{% include 'user/includes/contact_modal_button.html' %}
{% endfor %}
diff --git a/compensation/tests/compensation/test_views.py b/compensation/tests/compensation/test_views.py
index 27218f2..1174895 100644
--- a/compensation/tests/compensation/test_views.py
+++ b/compensation/tests/compensation/test_views.py
@@ -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,
diff --git a/compensation/tests/compensation/test_workflow.py b/compensation/tests/compensation/test_workflow.py
index 7b73be8..5b7decf 100644
--- a/compensation/tests/compensation/test_workflow.py
+++ b/compensation/tests/compensation/test_workflow.py
@@ -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)
diff --git a/compensation/tests/ecoaccount/test_views.py b/compensation/tests/ecoaccount/test_views.py
index 617f743..aaa7a4c 100644
--- a/compensation/tests/ecoaccount/test_views.py
+++ b/compensation/tests/ecoaccount/test_views.py
@@ -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,
diff --git a/compensation/tests/ecoaccount/test_workflow.py b/compensation/tests/ecoaccount/test_workflow.py
index 1cdb030..03b4996 100644
--- a/compensation/tests/ecoaccount/test_workflow.py
+++ b/compensation/tests/ecoaccount/test_workflow.py
@@ -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()
diff --git a/compensation/tests/payment/test_views.py b/compensation/tests/payment/test_views.py
index b1eca5a..30ffa00 100644
--- a/compensation/tests/payment/test_views.py
+++ b/compensation/tests/payment/test_views.py
@@ -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 = [
]
diff --git a/compensation/tests/payment/test_workflow.py b/compensation/tests/payment/test_workflow.py
index 790fb61..81259b8 100644
--- a/compensation/tests/payment/test_workflow.py
+++ b/compensation/tests/payment/test_workflow.py
@@ -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,
diff --git a/compensation/views/eco_account.py b/compensation/views/eco_account.py
index cf5f328..85b1371 100644
--- a/compensation/views/eco_account.py
+++ b/compensation/views/eco_account.py
@@ -272,7 +272,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 +286,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 +301,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 +315,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 +681,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 +692,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,
@@ -793,7 +796,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(
diff --git a/ema/forms.py b/ema/forms.py
index 2f19360..8e6faab 100644
--- a/ema/forms.py
+++ b/ema/forms.py
@@ -80,7 +80,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)
diff --git a/ema/migrations/0003_ema_teams.py b/ema/migrations/0003_ema_teams.py
new file mode 100644
index 0000000..606781d
--- /dev/null
+++ b/ema/migrations/0003_ema_teams.py
@@ -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'),
+ ),
+ ]
diff --git a/ema/templates/ema/detail/includes/actions.html b/ema/templates/ema/detail/includes/actions.html
index 02772b3..0c352c1 100644
--- a/ema/templates/ema/detail/includes/actions.html
+++ b/ema/templates/ema/detail/includes/actions.html
@@ -44,13 +44,15 @@
{% for action in obj.actions.all %}
- {{ action.action_type }}
- {% if action.action_type_details.count > 0 %}
-
- {% for detail in action.action_type_details.all %}
- {{detail.long_name}}
- {% endfor %}
- {% endif %}
+ {% for type in action.action_type.all %}
+ {{type.parent.parent.long_name}} {% fa5_icon 'angle-right' %} {{type.parent.long_name}} {% fa5_icon 'angle-right' %} {{type.long_name}}
+
+ {% endfor %}
+ {% for detail in action.action_type_details.all %}
+ {{detail.long_name}}
+ {% empty %}
+ {% trans 'No action type details' %}
+ {% endfor %}
{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}
diff --git a/ema/templates/ema/detail/includes/states-after.html b/ema/templates/ema/detail/includes/states-after.html
index e09f4ba..56e87be 100644
--- a/ema/templates/ema/detail/includes/states-after.html
+++ b/ema/templates/ema/detail/includes/states-after.html
@@ -46,13 +46,13 @@
{% for state in after_states %}
- {{ state.biotope_type }}
- {% if state.biotope_type_details.count > 0 %}
-
- {% for detail in state.biotope_type_details.all %}
- {{detail.long_name}}
- {% endfor %}
- {% endif %}
+ {{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})
+
+ {% for detail in state.biotope_type_details.all %}
+ {{detail.long_name}}
+ {% empty %}
+ {% trans 'No biotope type details' %}
+ {% endfor %}
{{ state.surface|floatformat:2 }} m²
diff --git a/ema/templates/ema/detail/includes/states-before.html b/ema/templates/ema/detail/includes/states-before.html
index 1369829..2fd7c35 100644
--- a/ema/templates/ema/detail/includes/states-before.html
+++ b/ema/templates/ema/detail/includes/states-before.html
@@ -46,13 +46,13 @@
{% for state in before_states %}
- {{ state.biotope_type }}
- {% if state.biotope_type_details.count > 0 %}
-
- {% for detail in state.biotope_type_details.all %}
- {{detail.long_name}}
- {% endfor %}
- {% endif %}
+ {{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})
+
+ {% for detail in state.biotope_type_details.all %}
+ {{detail.long_name}}
+ {% empty %}
+ {% trans 'No biotope type details' %}
+ {% endfor %}
{{ state.surface|floatformat:2 }} m²
diff --git a/ema/templates/ema/detail/view.html b/ema/templates/ema/detail/view.html
index 32ddd66..7b56703 100644
--- a/ema/templates/ema/detail/view.html
+++ b/ema/templates/ema/detail/view.html
@@ -74,6 +74,10 @@
{% trans 'Shared with' %}
+ {% for team in obj.teams.all %}
+ {% include 'user/includes/team_data_modal_button.html' %}
+ {% endfor %}
+
{% for user in obj.users.all %}
{% include 'user/includes/contact_modal_button.html' %}
{% endfor %}
diff --git a/ema/tests/test_views.py b/ema/tests/test_views.py
index 9654e8f..b2c23e1 100644
--- a/ema/tests/test_views.py
+++ b/ema/tests/test_views.py
@@ -110,7 +110,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 +160,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 +203,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 +243,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,
diff --git a/ema/views.py b/ema/views.py
index e9d0acb..c145511 100644
--- a/ema/views.py
+++ b/ema/views.py
@@ -621,7 +621,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(
diff --git a/intervention/forms/forms.py b/intervention/forms/forms.py
index 1c5cf1c..94ff970 100644
--- a/intervention/forms/forms.py
+++ b/intervention/forms/forms.py
@@ -253,7 +253,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
diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py
index a8fa98d..1e5febf 100644
--- a/intervention/forms/modalForms.py
+++ b/intervention/forms/modalForms.py
@@ -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,32 +37,33 @@ class ShareModalForm(BaseModalForm):
}
)
)
- user_select = forms.ModelMultipleChoiceField(
- label=_("Add user to share with"),
+ teams = forms.ModelMultipleChoiceField(
+ label=_("Add team 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."),
+ help_text=_("Multiple selection possible - You can only select teams which do not already have access."),
required=False,
- queryset=User.objects.all(),
+ queryset=Team.objects.all(),
widget=autocomplete.ModelSelect2Multiple(
- url="share-user-autocomplete",
+ url="share-team-autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
"data-minimum-input-length": 3,
},
- forward=["users"]
),
)
- users = forms.MultipleChoiceField(
- label=_("Shared with"),
+ users = forms.ModelMultipleChoiceField(
+ label=_("Add user to share with"),
label_suffix="",
- required=True,
- help_text=_("Remove check to remove access for this user"),
- widget=forms.CheckboxSelectMultiple(
+ help_text=_("Multiple selection possible - You can only select users which do not already have access. Enter the full username."),
+ required=False,
+ queryset=User.objects.all(),
+ widget=autocomplete.ModelSelect2Multiple(
+ url="share-user-autocomplete",
attrs={
- "class": "list-unstyled",
- }
+ "data-placeholder": _("Click for selection"),
+ "data-minimum-input-length": 3,
+ },
),
- choices=[]
)
def __init__(self, *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):
@@ -267,7 +290,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()
diff --git a/intervention/inputs.py b/intervention/inputs.py
index 2e84edc..34fe043 100644
--- a/intervention/inputs.py
+++ b/intervention/inputs.py
@@ -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,
+ }
\ No newline at end of file
diff --git a/intervention/migrations/0003_intervention_teams.py b/intervention/migrations/0003_intervention_teams.py
new file mode 100644
index 0000000..8dd7624
--- /dev/null
+++ b/intervention/migrations/0003_intervention_teams.py
@@ -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'),
+ ),
+ ]
diff --git a/intervention/templates/intervention/detail/view.html b/intervention/templates/intervention/detail/view.html
index 8e7fc6c..f5680cc 100644
--- a/intervention/templates/intervention/detail/view.html
+++ b/intervention/templates/intervention/detail/view.html
@@ -114,6 +114,10 @@
{% trans 'Shared with' %}
+ {% for team in obj.teams.all %}
+ {% include 'user/includes/team_data_modal_button.html' %}
+ {% endfor %}
+
{% for user in obj.users.all %}
{% include 'user/includes/contact_modal_button.html' %}
{% endfor %}
diff --git a/intervention/tests/test_views.py b/intervention/tests/test_views.py
index f12ee7a..a049f3e 100644
--- a/intervention/tests/test_views.py
+++ b/intervention/tests/test_views.py
@@ -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,
diff --git a/intervention/tests/test_workflow.py b/intervention/tests/test_workflow.py
index 69f606f..c529050 100644
--- a/intervention/tests/test_workflow.py
+++ b/intervention/tests/test_workflow.py
@@ -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):
"""
@@ -303,7 +303,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 +314,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 +331,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 +365,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()
diff --git a/intervention/views.py b/intervention/views.py
index 00440bb..3004a79 100644
--- a/intervention/views.py
+++ b/intervention/views.py
@@ -432,7 +432,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(
diff --git a/konova/autocompletes.py b/konova/autocompletes.py
index 3a79ab6..5ecc50d 100644
--- a/konova/autocompletes.py
+++ b/konova/autocompletes.py
@@ -5,8 +5,13 @@ 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
@@ -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"
)
@@ -65,27 +69,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 +156,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 +200,7 @@ class CompensationActionDetailCodeAutocomplete(KonovaCodeAutocomplete):
def order_by(self, qs):
return qs.order_by(
- "parent__long_name"
+ "long_name"
)
@@ -214,6 +233,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 +293,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 +338,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 +356,4 @@ class ConservationOfficeCodeAutocomplete(KonovaCodeAutocomplete):
super().__init__(*args, **kwargs)
def get_result_label(self, result):
- return f"{result.long_name} ({result.short_name})"
\ No newline at end of file
+ return f"{result.long_name} ({result.short_name})"
diff --git a/konova/filters/mixins.py b/konova/filters/mixins.py
index 5625ff3..5a07e3c 100644
--- a/konova/filters/mixins.py
+++ b/konova/filters/mixins.py
@@ -297,8 +297,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
diff --git a/konova/migrations/0005_auto_20220216_0856.py b/konova/migrations/0005_auto_20220216_0856.py
new file mode 100644
index 0000000..43c518a
--- /dev/null
+++ b/konova/migrations/0005_auto_20220216_0856.py
@@ -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),
+ ]
diff --git a/konova/models/object.py b/konova/models/object.py
index a6164f5..f224b68 100644
--- a/konova/models/object.py
+++ b/konova/models/object.py
@@ -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,6 +286,7 @@ class RecordableObjectMixin(models.Model):
Returns:
"""
+ from user.models import UserActionLogEntry
if self.recorded:
return None
action = UserActionLogEntry.get_recorded_action(user)
@@ -278,13 +294,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_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:
@@ -318,7 +339,7 @@ class RecordableObjectMixin(models.Model):
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 +367,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 +376,7 @@ class CheckableObjectMixin(models.Model):
Returns:
"""
+ from user.models import UserActionLogEntry
if self.checked:
# Nothing to do
return
@@ -363,17 +385,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 +448,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 +459,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_team(self, team):
+ """ Adds team to list of shared access teans
+
+ Args:
+ team (Team): The team to be added to the object
+
+ Returns:
- def share_with(self, user: User):
+ """
+ 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 +500,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 +511,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 +521,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(
- id__in=accessing_users
- )
- removed_users = self.users.all().exclude(
+ # 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
- ).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 +590,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
diff --git a/konova/static/css/konova.css b/konova/static/css/konova.css
index 709d3eb..7e7f3fd 100644
--- a/konova/static/css/konova.css
+++ b/konova/static/css/konova.css
@@ -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;
-}
\ No newline at end of file
+}
+
+ */
\ No newline at end of file
diff --git a/konova/tasks.py b/konova/tasks.py
index c74a2bd..798effb 100644
--- a/konova/tasks.py
+++ b/konova/tasks.py
@@ -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)
diff --git a/konova/templates/konova/widgets/checkbox-tree-select-content.html b/konova/templates/konova/widgets/checkbox-tree-select-content.html
new file mode 100644
index 0000000..9bf5725
--- /dev/null
+++ b/konova/templates/konova/widgets/checkbox-tree-select-content.html
@@ -0,0 +1,21 @@
+{% load l10n fontawesome_5 %}
+
+{% for code in codes %}
+
+
+ {% if code.is_leaf%}
+
+ {% else %}
+ {% fa5_icon 'angle-right' %}
+ {% endif %}
+ {{code.long_name}}
+
+ {% if not code.is_leaf %}
+
+ {% with code.children as codes %}
+ {% include 'konova/widgets/checkbox-tree-select-content.html' %}
+ {% endwith %}
+
+ {% endif %}
+
+{% endfor %}
\ No newline at end of file
diff --git a/konova/templates/konova/widgets/checkbox-tree-select.html b/konova/templates/konova/widgets/checkbox-tree-select.html
new file mode 100644
index 0000000..c2b107c
--- /dev/null
+++ b/konova/templates/konova/widgets/checkbox-tree-select.html
@@ -0,0 +1,68 @@
+{% load i18n %}
+
+
+
+
+
+
+ {% include 'konova/widgets/checkbox-tree-select-content.html' %}
+
+
+
\ No newline at end of file
diff --git a/konova/tests/test_autocompletes.py b/konova/tests/test_autocompletes.py
index 7d9e508..95a3508 100644
--- a/konova/tests/test_autocompletes.py
+++ b/konova/tests/test_autocompletes.py
@@ -71,6 +71,7 @@ class AutocompleteTestCase(BaseTestCase):
"codes-registration-office-autocomplete",
"codes-conservation-office-autocomplete",
"share-user-autocomplete",
+ "share-team-autocomplete",
]
for test in tests:
self.client.login(username=self.superuser.username, password=self.superuser_pw)
diff --git a/konova/tests/test_views.py b/konova/tests/test_views.py
index 6218a6c..ff99ea3 100644
--- a/konova/tests/test_views.py
+++ b/konova/tests/test_views.py
@@ -9,7 +9,7 @@ import datetime
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID
from ema.models import Ema
-from user.models import User
+from user.models import User, Team
from django.contrib.auth.models import Group
from django.contrib.gis.geos import MultiPolygon, Polygon
from django.core.exceptions import ObjectDoesNotExist
@@ -65,6 +65,7 @@ class BaseTestCase(TestCase):
self.create_dummy_states()
self.create_dummy_action()
self.codes = self.create_dummy_codes()
+ self.team = self.create_dummy_team()
# Set the default group as only group for the user
default_group = self.groups.get(name=DEFAULT_GROUP)
@@ -251,6 +252,24 @@ class BaseTestCase(TestCase):
])
return codes
+ def create_dummy_team(self):
+ """ Creates a dummy team
+
+ Returns:
+
+ """
+ if self.superuser is None:
+ self.create_users()
+
+ team = Team.objects.get_or_create(
+ name="Testteam",
+ description="Testdescription",
+ admin=self.superuser,
+ )[0]
+ team.users.add(self.superuser)
+
+ return team
+
@staticmethod
def create_dummy_geometry() -> MultiPolygon:
""" Creates some geometry
diff --git a/konova/urls.py b/konova/urls.py
index 68256e7..1cd8851 100644
--- a/konova/urls.py
+++ b/konova/urls.py
@@ -20,7 +20,7 @@ from django.urls import path, include
from konova.autocompletes import EcoAccountAutocomplete, \
InterventionAutocomplete, CompensationActionCodeAutocomplete, BiotopeCodeAutocomplete, LawCodeAutocomplete, \
RegistrationOfficeCodeAutocomplete, ConservationOfficeCodeAutocomplete, ProcessTypeCodeAutocomplete, \
- ShareUserAutocomplete, BiotopeExtraCodeAutocomplete, CompensationActionDetailCodeAutocomplete
+ ShareUserAutocomplete, BiotopeExtraCodeAutocomplete, CompensationActionDetailCodeAutocomplete, ShareTeamAutocomplete
from konova.settings import SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY, DEBUG
from konova.sso.sso import KonovaSSOClient
from konova.views import logout_view, home_view
@@ -52,6 +52,7 @@ urlpatterns = [
path("atcmplt/codes/reg-off", RegistrationOfficeCodeAutocomplete.as_view(), name="codes-registration-office-autocomplete"),
path("atcmplt/codes/cons-off", ConservationOfficeCodeAutocomplete.as_view(), name="codes-conservation-office-autocomplete"),
path("atcmplt/share/u", ShareUserAutocomplete.as_view(), name="share-user-autocomplete"),
+ path("atcmplt/share/t", ShareTeamAutocomplete.as_view(), name="share-team-autocomplete"),
]
if DEBUG:
diff --git a/konova/utils/mailer.py b/konova/utils/mailer.py
index 33907f3..dd8eef1 100644
--- a/konova/utils/mailer.py
+++ b/konova/utils/mailer.py
@@ -92,6 +92,144 @@ class Mailer:
msg
)
+ def send_mail_shared_access_given_team(self, obj_identifier, obj_title, team):
+ """ Send a mail if a team just got access to the object
+
+ Args:
+ obj_identifier (str): The object identifier
+
+ Returns:
+
+ """
+ context = {
+ "team": team,
+ "obj_identifier": obj_identifier,
+ "obj_title": obj_title,
+ "EMAIL_REPLY_TO": EMAIL_REPLY_TO,
+ }
+ msg = render_to_string("email/sharing/shared_access_given_team.html", context)
+ user_mail_address = team.users.values_list("email", flat=True)
+ self.send(
+ user_mail_address,
+ _("{} - Shared access given").format(obj_identifier),
+ msg
+ )
+
+ def send_mail_shared_access_removed_team(self, obj_identifier, obj_title, team):
+ """ Send a mail if a team just lost access to the object
+
+ Args:
+ obj_identifier (str): The object identifier
+
+ Returns:
+
+ """
+ context = {
+ "team": team,
+ "obj_identifier": obj_identifier,
+ "obj_title": obj_title,
+ "EMAIL_REPLY_TO": EMAIL_REPLY_TO,
+ }
+ msg = render_to_string("email/sharing/shared_access_removed_team.html", context)
+ user_mail_address = team.users.values_list("email", flat=True)
+ self.send(
+ user_mail_address,
+ _("{} - Shared access removed").format(obj_identifier),
+ msg
+ )
+
+ def send_mail_shared_data_unrecorded_team(self, obj_identifier, obj_title, team):
+ """ Send a mail if data has just been unrecorded
+
+ Args:
+ obj_identifier (str): The object identifier
+
+ Returns:
+
+ """
+ context = {
+ "team": team,
+ "obj_identifier": obj_identifier,
+ "obj_title": obj_title,
+ "EMAIL_REPLY_TO": EMAIL_REPLY_TO,
+ }
+ msg = render_to_string("email/recording/shared_data_unrecorded_team.html", context)
+ user_mail_address = team.users.values_list("email", flat=True)
+ self.send(
+ user_mail_address,
+ _("{} - Shared data unrecorded").format(obj_identifier),
+ msg
+ )
+
+ def send_mail_shared_data_recorded_team(self, obj_identifier, obj_title, team):
+ """ Send a mail if data has just been recorded
+
+ Args:
+ obj_identifier (str): The object identifier
+
+ Returns:
+
+ """
+ context = {
+ "team": team,
+ "obj_identifier": obj_identifier,
+ "obj_title": obj_title,
+ "EMAIL_REPLY_TO": EMAIL_REPLY_TO,
+ }
+ msg = render_to_string("email/recording/shared_data_recorded_team.html", context)
+ user_mail_address = team.users.values_list("email", flat=True)
+ self.send(
+ user_mail_address,
+ _("{} - Shared data recorded").format(obj_identifier),
+ msg
+ )
+
+ def send_mail_shared_data_checked_team(self, obj_identifier, obj_title, team):
+ """ Send a mail if data has just been checked
+
+ Args:
+ obj_identifier (str): The object identifier
+
+ Returns:
+
+ """
+ context = {
+ "team": team,
+ "obj_identifier": obj_identifier,
+ "obj_title": obj_title,
+ "EMAIL_REPLY_TO": EMAIL_REPLY_TO,
+ }
+ msg = render_to_string("email/checking/shared_data_checked_team.html", context)
+ user_mail_address = team.users.values_list("email", flat=True)
+ self.send(
+ user_mail_address,
+ _("{} - Shared data checked").format(obj_identifier),
+ msg
+ )
+
+ def send_mail_shared_data_deleted_team(self, obj_identifier, obj_title, team):
+ """ Send a mail if data has just been deleted
+
+ Args:
+ obj_identifier (str): The object identifier
+
+ Returns:
+
+ """
+ context = {
+ "team": team,
+ "obj_identifier": obj_identifier,
+ "obj_title": obj_title,
+ "EMAIL_REPLY_TO": EMAIL_REPLY_TO,
+ }
+ msg = render_to_string("email/deleting/shared_data_deleted_team.html", context)
+ user_mail_address = team.users.values_list("email", flat=True)
+ self.send(
+ user_mail_address,
+ _("{} - Shared data deleted").format(obj_identifier),
+ msg
+ )
+
def send_mail_shared_data_recorded(self, obj_identifier, obj_title, user):
""" Send a mail if the user's shared data has just been unrecorded
diff --git a/konova/utils/message_templates.py b/konova/utils/message_templates.py
index c7a2be0..6c0c614 100644
--- a/konova/utils/message_templates.py
+++ b/konova/utils/message_templates.py
@@ -7,17 +7,19 @@ Created on: 02.08.21
"""
from django.utils.translation import gettext_lazy as _
-
+UNGROUPED = _("Ungrouped")
FORM_INVALID = _("There was an error on this form.")
PARAMS_INVALID = _("Invalid parameters")
INTERVENTION_INVALID = _("There are errors in this intervention.")
IDENTIFIER_REPLACED = _("The identifier '{}' had to be changed to '{}' since another entry has been added in the meanwhile, which uses this identifier")
-DATA_UNSHARED = _("This data is not shared with you")
-DATA_UNSHARED_EXPLANATION = _("Remember: This data has not been shared with you, yet. This means you can only read but can not edit or perform any actions like running a check or recording.")
+ENTRY_REMOVE_MISSING_PERMISSION = _("Only conservation or registration office users are allowed to remove entries.")
MISSING_GROUP_PERMISSION = _("You need to be part of another user group.")
-
CHECKED_RECORDED_RESET = _("Status of Checked and Recorded reseted")
+# SHARE
+DATA_UNSHARED = _("This data is not shared with you")
+DATA_UNSHARED_EXPLANATION = _("Remember: This data has not been shared with you, yet. This means you can only read but can not edit or perform any actions like running a check or recording.")
+
# FILES
FILE_TYPE_UNSUPPORTED = _("Unsupported file type")
FILE_SIZE_TOO_LARGE = _("File too large")
diff --git a/konova/views.py b/konova/views.py
index 2a4c8ad..f3c53f0 100644
--- a/konova/views.py
+++ b/konova/views.py
@@ -9,9 +9,12 @@ from django.contrib.auth import logout
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, FileResponse
from django.shortcuts import redirect, render, get_object_or_404
+from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
+from codelist.models import KonovaCode
+from codelist.settings import CODELIST_COMPENSATION_ACTION_ID
from compensation.models import Compensation, EcoAccount
from intervention.models import Intervention
from konova.contexts import BaseContext
diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo
index 9905835..9437cb9 100644
Binary files a/locale/de/LC_MESSAGES/django.mo and b/locale/de/LC_MESSAGES/django.mo differ
diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po
index cd7cd88..051894c 100644
--- a/locale/de/LC_MESSAGES/django.po
+++ b/locale/de/LC_MESSAGES/django.po
@@ -3,21 +3,21 @@
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR , YEAR.
#
-#: compensation/filters.py:122 compensation/forms/modalForms.py:35
-#: compensation/forms/modalForms.py:46 compensation/forms/modalForms.py:62
-#: compensation/forms/modalForms.py:355 compensation/forms/modalForms.py:471
+#: compensation/filters.py:123 compensation/forms/modalForms.py:36
+#: compensation/forms/modalForms.py:47 compensation/forms/modalForms.py:63
+#: compensation/forms/modalForms.py:356 compensation/forms/modalForms.py:463
#: intervention/forms/forms.py:54 intervention/forms/forms.py:156
-#: intervention/forms/forms.py:168 intervention/forms/modalForms.py:127
-#: intervention/forms/modalForms.py:140 intervention/forms/modalForms.py:153
+#: intervention/forms/forms.py:168 intervention/forms/modalForms.py:150
+#: intervention/forms/modalForms.py:163 intervention/forms/modalForms.py:176
#: konova/filters/mixins.py:53 konova/filters/mixins.py:54
#: konova/filters/mixins.py:81 konova/filters/mixins.py:82
#: konova/filters/mixins.py:94 konova/filters/mixins.py:95
#: konova/filters/mixins.py:107 konova/filters/mixins.py:108
#: konova/filters/mixins.py:120 konova/filters/mixins.py:121
#: konova/filters/mixins.py:134 konova/filters/mixins.py:135
-#: konova/filters/mixins.py:270 konova/filters/mixins.py:315
-#: konova/filters/mixins.py:353 konova/filters/mixins.py:354
-#: konova/filters/mixins.py:385 konova/filters/mixins.py:386
+#: konova/filters/mixins.py:270 konova/filters/mixins.py:316
+#: konova/filters/mixins.py:354 konova/filters/mixins.py:355
+#: konova/filters/mixins.py:386 konova/filters/mixins.py:387
#: konova/forms.py:143 konova/forms.py:244 konova/forms.py:315
#: konova/forms.py:359 konova/forms.py:369 konova/forms.py:382
#: konova/forms.py:394 konova/forms.py:412 user/forms.py:42
@@ -26,7 +26,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-02-10 13:31+0100\n"
+"POT-Creation-Date: 2022-02-18 14:50+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -45,14 +45,14 @@ msgid "To"
msgstr "Bis"
#: analysis/forms.py:47 compensation/forms/forms.py:77
-#: compensation/templates/compensation/detail/eco_account/view.html:58
+#: compensation/templates/compensation/detail/eco_account/view.html:59
#: compensation/templates/compensation/report/eco_account/report.html:16
#: compensation/utils/quality.py:100 ema/templates/ema/detail/view.html:49
#: ema/templates/ema/report/report.html:16 ema/utils/quality.py:26
#: intervention/forms/forms.py:102
#: intervention/templates/intervention/detail/view.html:56
#: intervention/templates/intervention/report/report.html:37
-#: intervention/utils/quality.py:49 konova/filters/mixins.py:395
+#: intervention/utils/quality.py:49 konova/filters/mixins.py:396
msgid "Conservation office"
msgstr "Eintragungsstelle"
@@ -64,6 +64,7 @@ msgstr "Verantwortliche Stelle"
#: compensation/forms/forms.py:165 intervention/forms/forms.py:64
#: intervention/forms/forms.py:81 intervention/forms/forms.py:97
#: intervention/forms/forms.py:113 intervention/forms/modalForms.py:49
+#: intervention/forms/modalForms.py:63 user/forms.py:196
msgid "Click for selection"
msgstr "Auswählen..."
@@ -95,7 +96,7 @@ msgstr ""
#: analysis/templates/analysis/reports/includes/eco_account/amount.html:3
#: analysis/templates/analysis/reports/includes/intervention/amount.html:3
#: analysis/templates/analysis/reports/includes/old_data/amount.html:3
-#: compensation/forms/modalForms.py:455
+#: compensation/forms/modalForms.py:447
#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:34
#: intervention/templates/intervention/detail/includes/deductions.html:31
msgid "Amount"
@@ -137,7 +138,7 @@ msgstr "Zuständigkeitsbereich"
#: analysis/templates/analysis/reports/includes/intervention/compensated_by.html:8
#: analysis/templates/analysis/reports/includes/intervention/laws.html:17
#: compensation/tables.py:40
-#: compensation/templates/compensation/detail/compensation/view.html:63
+#: compensation/templates/compensation/detail/compensation/view.html:64
#: intervention/tables.py:39
#: intervention/templates/intervention/detail/view.html:68
#: user/models/user_action.py:20
@@ -153,9 +154,9 @@ msgstr "Geprüft"
#: analysis/templates/analysis/reports/includes/intervention/laws.html:20
#: analysis/templates/analysis/reports/includes/old_data/amount.html:18
#: compensation/tables.py:46 compensation/tables.py:222
-#: compensation/templates/compensation/detail/compensation/view.html:77
+#: compensation/templates/compensation/detail/compensation/view.html:78
#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:31
-#: compensation/templates/compensation/detail/eco_account/view.html:44
+#: compensation/templates/compensation/detail/eco_account/view.html:45
#: ema/tables.py:44 ema/templates/ema/detail/view.html:35
#: intervention/tables.py:45
#: intervention/templates/intervention/detail/view.html:82
@@ -213,14 +214,14 @@ msgstr "Abbuchungen"
#: analysis/templates/analysis/reports/includes/eco_account/deductions.html:9
#: analysis/templates/analysis/reports/includes/eco_account/deductions.html:11
-#: compensation/forms/modalForms.py:193
+#: compensation/forms/modalForms.py:194
#: compensation/templates/compensation/detail/compensation/includes/states-after.html:36
#: compensation/templates/compensation/detail/compensation/includes/states-before.html:36
#: compensation/templates/compensation/detail/eco_account/includes/states-after.html:36
#: compensation/templates/compensation/detail/eco_account/includes/states-before.html:36
#: ema/templates/ema/detail/includes/states-after.html:36
#: ema/templates/ema/detail/includes/states-before.html:36
-#: intervention/forms/modalForms.py:338
+#: intervention/forms/modalForms.py:361
msgid "Surface"
msgstr "Fläche"
@@ -239,14 +240,14 @@ msgstr "Kompensationsart"
#: analysis/templates/analysis/reports/includes/intervention/compensated_by.html:15
#: analysis/templates/analysis/reports/includes/old_data/amount.html:29
-#: compensation/templates/compensation/detail/compensation/view.html:19
+#: compensation/templates/compensation/detail/compensation/view.html:20
#: konova/templates/konova/includes/quickstart/compensations.html:4
#: templates/navbars/navbar.html:28
msgid "Compensation"
msgstr "Kompensation"
#: analysis/templates/analysis/reports/includes/intervention/compensated_by.html:21
-#: compensation/forms/modalForms.py:75
+#: compensation/forms/modalForms.py:76
msgid "Payment"
msgstr "Zahlung"
@@ -283,8 +284,8 @@ msgid "Type"
msgstr "Typ"
#: analysis/templates/analysis/reports/includes/old_data/amount.html:24
-#: compensation/tables.py:89 intervention/forms/modalForms.py:349
-#: intervention/forms/modalForms.py:356 intervention/tables.py:88
+#: compensation/tables.py:89 intervention/forms/modalForms.py:372
+#: intervention/forms/modalForms.py:379 intervention/tables.py:88
#: intervention/templates/intervention/detail/view.html:19
#: konova/templates/konova/includes/quickstart/interventions.html:4
#: templates/navbars/navbar.html:22
@@ -293,8 +294,8 @@ msgstr "Eingriff"
#: analysis/templates/analysis/reports/includes/old_data/amount.html:34
#: compensation/tables.py:266
-#: compensation/templates/compensation/detail/eco_account/view.html:19
-#: intervention/forms/modalForms.py:322 intervention/forms/modalForms.py:329
+#: compensation/templates/compensation/detail/eco_account/view.html:20
+#: intervention/forms/modalForms.py:345 intervention/forms/modalForms.py:352
#: konova/templates/konova/includes/quickstart/ecoaccounts.html:4
#: templates/navbars/navbar.html:34
msgid "Eco-account"
@@ -308,7 +309,7 @@ msgstr "Altfälle"
msgid "Before"
msgstr "Vor"
-#: compensation/filters.py:121
+#: compensation/filters.py:122
msgid "Show only unrecorded"
msgstr "Nur unverzeichnete anzeigen"
@@ -327,9 +328,9 @@ msgstr "Automatisch generiert"
#: compensation/forms/forms.py:44 compensation/tables.py:30
#: compensation/tables.py:202
#: compensation/templates/compensation/detail/compensation/includes/documents.html:28
-#: compensation/templates/compensation/detail/compensation/view.html:31
+#: compensation/templates/compensation/detail/compensation/view.html:32
#: compensation/templates/compensation/detail/eco_account/includes/documents.html:28
-#: compensation/templates/compensation/detail/eco_account/view.html:31
+#: compensation/templates/compensation/detail/eco_account/view.html:32
#: compensation/templates/compensation/report/compensation/report.html:12
#: compensation/templates/compensation/report/eco_account/report.html:12
#: ema/tables.py:34 ema/templates/ema/detail/includes/documents.html:28
@@ -352,8 +353,8 @@ msgstr "Aussagekräftiger Titel"
msgid "Compensation XY; Location ABC"
msgstr "Kompensation XY; Flur ABC"
-#: compensation/forms/forms.py:57 compensation/forms/modalForms.py:61
-#: compensation/forms/modalForms.py:354 compensation/forms/modalForms.py:470
+#: compensation/forms/forms.py:57 compensation/forms/modalForms.py:62
+#: compensation/forms/modalForms.py:355 compensation/forms/modalForms.py:462
#: compensation/templates/compensation/detail/compensation/includes/actions.html:35
#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:34
#: compensation/templates/compensation/detail/compensation/includes/documents.html:34
@@ -363,7 +364,7 @@ msgstr "Kompensation XY; Flur ABC"
#: ema/templates/ema/detail/includes/actions.html:34
#: ema/templates/ema/detail/includes/deadlines.html:34
#: ema/templates/ema/detail/includes/documents.html:34
-#: intervention/forms/forms.py:180 intervention/forms/modalForms.py:152
+#: intervention/forms/forms.py:180 intervention/forms/modalForms.py:175
#: intervention/templates/intervention/detail/includes/documents.html:34
#: intervention/templates/intervention/detail/includes/payments.html:34
#: intervention/templates/intervention/detail/includes/revocation.html:38
@@ -371,13 +372,13 @@ msgstr "Kompensation XY; Flur ABC"
msgid "Comment"
msgstr "Kommentar"
-#: compensation/forms/forms.py:59 compensation/forms/modalForms.py:472
+#: compensation/forms/forms.py:59 compensation/forms/modalForms.py:464
#: intervention/forms/forms.py:182
msgid "Additional comment"
msgstr "Zusätzlicher Kommentar"
#: compensation/forms/forms.py:93
-#: compensation/templates/compensation/detail/eco_account/view.html:62
+#: compensation/templates/compensation/detail/eco_account/view.html:63
#: compensation/templates/compensation/report/eco_account/report.html:20
#: compensation/utils/quality.py:102 ema/templates/ema/detail/view.html:53
#: ema/templates/ema/report/report.html:20 ema/utils/quality.py:28
@@ -422,7 +423,7 @@ msgid ""
msgstr "Optional: Handelt es sich um eine Kohärenzsicherungsmaßnahme?"
#: compensation/forms/forms.py:156
-#: compensation/templates/compensation/detail/compensation/view.html:35
+#: compensation/templates/compensation/detail/compensation/view.html:36
#: compensation/templates/compensation/report/compensation/report.html:16
msgid "compensates intervention"
msgstr "kompensiert Eingriff"
@@ -448,7 +449,7 @@ msgid "The amount that can be used for deductions"
msgstr "Die für Abbuchungen zur Verfügung stehende Menge"
#: compensation/forms/forms.py:328
-#: compensation/templates/compensation/detail/eco_account/view.html:66
+#: compensation/templates/compensation/detail/eco_account/view.html:67
#: compensation/utils/quality.py:72
msgid "Agreement date"
msgstr "Vereinbarungsdatum"
@@ -469,175 +470,155 @@ msgstr "Ökokonto XY; Flur ABC"
msgid "Edit Eco-Account"
msgstr "Ökokonto bearbeiten"
-#: compensation/forms/modalForms.py:36
+#: compensation/forms/modalForms.py:37
msgid "in Euro"
msgstr "in Euro"
-#: compensation/forms/modalForms.py:45
+#: compensation/forms/modalForms.py:46
#: intervention/templates/intervention/detail/includes/payments.html:31
msgid "Due on"
msgstr "Fällig am"
-#: compensation/forms/modalForms.py:48
+#: compensation/forms/modalForms.py:49
msgid "Due on which date"
msgstr "Zahlung wird an diesem Datum erwartet"
-#: compensation/forms/modalForms.py:63 compensation/forms/modalForms.py:356
-#: intervention/forms/modalForms.py:154 konova/forms.py:395
+#: compensation/forms/modalForms.py:64 compensation/forms/modalForms.py:357
+#: intervention/forms/modalForms.py:177 konova/forms.py:395
msgid "Additional comment, maximum {} letters"
msgstr "Zusätzlicher Kommentar, maximal {} Zeichen"
-#: compensation/forms/modalForms.py:76
+#: compensation/forms/modalForms.py:77
msgid "Add a payment for intervention '{}'"
msgstr "Neue Ersatzzahlung zu Eingriff '{}' hinzufügen"
-#: compensation/forms/modalForms.py:96
+#: compensation/forms/modalForms.py:97
msgid "If there is no date you can enter, please explain why."
msgstr "Falls Sie kein Datum angeben können, erklären Sie bitte weshalb."
-#: compensation/forms/modalForms.py:157 compensation/forms/modalForms.py:169
+#: compensation/forms/modalForms.py:158 compensation/forms/modalForms.py:170
msgid "Biotope Type"
msgstr "Biotoptyp"
-#: compensation/forms/modalForms.py:160
+#: compensation/forms/modalForms.py:161
msgid "Select the biotope type"
msgstr "Biotoptyp wählen"
-#: compensation/forms/modalForms.py:174 compensation/forms/modalForms.py:186
+#: compensation/forms/modalForms.py:175 compensation/forms/modalForms.py:187
msgid "Biotope additional type"
msgstr "Zusatzbezeichnung"
-#: compensation/forms/modalForms.py:177
+#: compensation/forms/modalForms.py:178
msgid "Select an additional biotope type"
msgstr "Zusatzbezeichnung wählen"
-#: compensation/forms/modalForms.py:196 intervention/forms/modalForms.py:340
+#: compensation/forms/modalForms.py:197 intervention/forms/modalForms.py:363
msgid "in m²"
msgstr ""
-#: compensation/forms/modalForms.py:207
+#: compensation/forms/modalForms.py:208
msgid "New state"
msgstr "Neuer Zustand"
-#: compensation/forms/modalForms.py:208
+#: compensation/forms/modalForms.py:209
msgid "Insert data for the new state"
msgstr "Geben Sie die Daten des neuen Zustandes ein"
-#: compensation/forms/modalForms.py:215 konova/forms.py:193
+#: compensation/forms/modalForms.py:216 konova/forms.py:193
msgid "Object removed"
msgstr "Objekt entfernt"
-#: compensation/forms/modalForms.py:326
+#: compensation/forms/modalForms.py:327
msgid "Deadline Type"
msgstr "Fristart"
-#: compensation/forms/modalForms.py:329
+#: compensation/forms/modalForms.py:330
msgid "Select the deadline type"
msgstr "Fristart wählen"
-#: compensation/forms/modalForms.py:338
+#: compensation/forms/modalForms.py:339
#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:31
#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:31
#: ema/templates/ema/detail/includes/deadlines.html:31
-#: intervention/forms/modalForms.py:126
+#: intervention/forms/modalForms.py:149
msgid "Date"
msgstr "Datum"
-#: compensation/forms/modalForms.py:341
+#: compensation/forms/modalForms.py:342
msgid "Select date"
msgstr "Datum wählen"
-#: compensation/forms/modalForms.py:368
+#: compensation/forms/modalForms.py:369
msgid "New deadline"
msgstr "Neue Frist"
-#: compensation/forms/modalForms.py:369
+#: compensation/forms/modalForms.py:370
msgid "Insert data for the new deadline"
msgstr "Geben Sie die Daten der neuen Frist ein"
-#: compensation/forms/modalForms.py:409
+#: compensation/forms/modalForms.py:410
msgid "Action Type"
msgstr "Maßnahmentyp"
-#: compensation/forms/modalForms.py:412
-msgid "Select the action type"
-msgstr "Maßnahmentyp wählen"
-
-#: compensation/forms/modalForms.py:421
-#: compensation/templates/compensation/detail/compensation/includes/actions.html:40
-#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:39
-#: compensation/templates/compensation/detail/compensation/includes/documents.html:39
-#: compensation/templates/compensation/detail/compensation/includes/states-after.html:41
-#: compensation/templates/compensation/detail/compensation/includes/states-before.html:41
-#: compensation/templates/compensation/detail/eco_account/includes/actions.html:39
-#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:38
-#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:41
-#: compensation/templates/compensation/detail/eco_account/includes/documents.html:38
-#: compensation/templates/compensation/detail/eco_account/includes/states-after.html:41
-#: compensation/templates/compensation/detail/eco_account/includes/states-before.html:41
-#: ema/templates/ema/detail/includes/actions.html:38
-#: ema/templates/ema/detail/includes/deadlines.html:38
-#: ema/templates/ema/detail/includes/documents.html:38
-#: ema/templates/ema/detail/includes/states-after.html:40
-#: ema/templates/ema/detail/includes/states-before.html:40
-#: intervention/templates/intervention/detail/includes/compensations.html:38
-#: intervention/templates/intervention/detail/includes/deductions.html:39
-#: intervention/templates/intervention/detail/includes/documents.html:39
-#: intervention/templates/intervention/detail/includes/payments.html:39
-#: intervention/templates/intervention/detail/includes/revocation.html:43
-#: templates/log.html:10
-msgid "Action"
-msgstr "Aktionen"
+#: compensation/forms/modalForms.py:413
+msgid ""
+"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."
+msgstr ""
+"Eine Maßnahme kann aus mehreren verschiedenen Maßnahmentypen bestehen. Alle "
+"hier gewählten Einträge sollen mit der weiter unten angegebenen Einheit und "
+"Menge umgesetzt werden. "
-#: compensation/forms/modalForms.py:426 compensation/forms/modalForms.py:438
+#: compensation/forms/modalForms.py:418 compensation/forms/modalForms.py:430
msgid "Action Type detail"
msgstr "Zusatzmerkmal"
-#: compensation/forms/modalForms.py:429
+#: compensation/forms/modalForms.py:421
msgid "Select the action type detail"
msgstr "Zusatzmerkmal wählen"
-#: compensation/forms/modalForms.py:443
+#: compensation/forms/modalForms.py:435
msgid "Unit"
msgstr "Einheit"
-#: compensation/forms/modalForms.py:446
+#: compensation/forms/modalForms.py:438
msgid "Select the unit"
msgstr "Einheit wählen"
-#: compensation/forms/modalForms.py:458
+#: compensation/forms/modalForms.py:450
msgid "Insert the amount"
msgstr "Menge eingeben"
-#: compensation/forms/modalForms.py:483
+#: compensation/forms/modalForms.py:475
msgid "New action"
msgstr "Neue Maßnahme"
-#: compensation/forms/modalForms.py:484
+#: compensation/forms/modalForms.py:476
msgid "Insert data for the new action"
msgstr "Geben Sie die Daten der neuen Maßnahme ein"
-#: compensation/models/action.py:22
+#: compensation/models/action.py:20
msgid "cm"
msgstr ""
-#: compensation/models/action.py:23
+#: compensation/models/action.py:21
msgid "m"
msgstr ""
-#: compensation/models/action.py:24
+#: compensation/models/action.py:22
msgid "km"
msgstr ""
-#: compensation/models/action.py:25
+#: compensation/models/action.py:23
msgid "m²"
msgstr ""
-#: compensation/models/action.py:26
+#: compensation/models/action.py:24
msgid "ha"
msgstr ""
-#: compensation/models/action.py:27
+#: compensation/models/action.py:25
msgid "Pieces"
msgstr "Stück"
@@ -685,9 +666,9 @@ msgid "Checked on {} by {}"
msgstr "Am {} von {} geprüft worden"
#: compensation/tables.py:160
-#: compensation/templates/compensation/detail/compensation/view.html:80
+#: compensation/templates/compensation/detail/compensation/view.html:81
#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:58
-#: compensation/templates/compensation/detail/eco_account/view.html:47
+#: compensation/templates/compensation/detail/eco_account/view.html:48
#: ema/tables.py:131 ema/templates/ema/detail/view.html:38
#: intervention/tables.py:157
#: intervention/templates/intervention/detail/view.html:85
@@ -710,7 +691,7 @@ msgid "Access not granted"
msgstr "Nicht freigegeben - Datensatz nur lesbar"
#: compensation/tables.py:212
-#: compensation/templates/compensation/detail/eco_account/view.html:35
+#: compensation/templates/compensation/detail/eco_account/view.html:36
#: konova/templates/konova/widgets/progressbar.html:3
msgid "Available"
msgstr "Verfügbar"
@@ -750,15 +731,46 @@ msgctxt "Compensation"
msgid "Amount"
msgstr "Menge"
-#: compensation/templates/compensation/detail/compensation/includes/actions.html:66
-#: compensation/templates/compensation/detail/eco_account/includes/actions.html:65
-#: ema/templates/ema/detail/includes/actions.html:63
+#: compensation/templates/compensation/detail/compensation/includes/actions.html:40
+#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:39
+#: compensation/templates/compensation/detail/compensation/includes/documents.html:39
+#: compensation/templates/compensation/detail/compensation/includes/states-after.html:41
+#: compensation/templates/compensation/detail/compensation/includes/states-before.html:41
+#: compensation/templates/compensation/detail/eco_account/includes/actions.html:39
+#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:38
+#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:41
+#: compensation/templates/compensation/detail/eco_account/includes/documents.html:38
+#: compensation/templates/compensation/detail/eco_account/includes/states-after.html:41
+#: compensation/templates/compensation/detail/eco_account/includes/states-before.html:41
+#: ema/templates/ema/detail/includes/actions.html:38
+#: ema/templates/ema/detail/includes/deadlines.html:38
+#: ema/templates/ema/detail/includes/documents.html:38
+#: ema/templates/ema/detail/includes/states-after.html:40
+#: ema/templates/ema/detail/includes/states-before.html:40
+#: intervention/templates/intervention/detail/includes/compensations.html:38
+#: intervention/templates/intervention/detail/includes/deductions.html:39
+#: intervention/templates/intervention/detail/includes/documents.html:39
+#: intervention/templates/intervention/detail/includes/payments.html:39
+#: intervention/templates/intervention/detail/includes/revocation.html:43
+#: templates/log.html:10 user/templates/user/team/index.html:32
+msgid "Action"
+msgstr "Aktionen"
+
+#: compensation/templates/compensation/detail/compensation/includes/actions.html:57
+#: compensation/templates/compensation/detail/eco_account/includes/actions.html:56
+#: ema/templates/ema/detail/includes/actions.html:54
+msgid "No action type details"
+msgstr "Keine Zusatzmerkmale"
+
+#: compensation/templates/compensation/detail/compensation/includes/actions.html:68
+#: compensation/templates/compensation/detail/eco_account/includes/actions.html:67
+#: ema/templates/ema/detail/includes/actions.html:65
msgid "Edit action"
msgstr "Maßnahme bearbeiten"
-#: compensation/templates/compensation/detail/compensation/includes/actions.html:69
-#: compensation/templates/compensation/detail/eco_account/includes/actions.html:68
-#: ema/templates/ema/detail/includes/actions.html:66
+#: compensation/templates/compensation/detail/compensation/includes/actions.html:71
+#: compensation/templates/compensation/detail/eco_account/includes/actions.html:70
+#: ema/templates/ema/detail/includes/actions.html:68
msgid "Remove action"
msgstr "Maßnahme entfernen"
@@ -887,6 +899,15 @@ msgstr "Fehlende Flächenmengen laut Ausgangszustand: "
msgid "Biotope type"
msgstr "Biotoptyp"
+#: compensation/templates/compensation/detail/compensation/includes/states-after.html:56
+#: compensation/templates/compensation/detail/compensation/includes/states-before.html:56
+#: compensation/templates/compensation/detail/eco_account/includes/states-after.html:56
+#: compensation/templates/compensation/detail/eco_account/includes/states-before.html:56
+#: ema/templates/ema/detail/includes/states-after.html:54
+#: ema/templates/ema/detail/includes/states-before.html:54
+msgid "No biotope type details"
+msgstr "Keine Zusatzbezeichnungen"
+
#: compensation/templates/compensation/detail/compensation/includes/states-after.html:62
#: compensation/templates/compensation/detail/compensation/includes/states-before.html:62
#: compensation/templates/compensation/detail/eco_account/includes/states-after.html:62
@@ -924,50 +945,50 @@ msgstr "Neuen Ausgangszustand hinzufügen"
msgid "Missing surfaces according to states after: "
msgstr "Fehlende Flächenmengen laut Zielzustand: "
-#: compensation/templates/compensation/detail/compensation/view.html:43
+#: compensation/templates/compensation/detail/compensation/view.html:44
msgid "Is CEF compensation"
msgstr "Ist CEF Maßnahme"
-#: compensation/templates/compensation/detail/compensation/view.html:46
-#: compensation/templates/compensation/detail/compensation/view.html:56
+#: compensation/templates/compensation/detail/compensation/view.html:47
+#: compensation/templates/compensation/detail/compensation/view.html:57
#: venv/lib/python3.7/site-packages/django/forms/widgets.py:710
msgid "Yes"
msgstr "Ja"
-#: compensation/templates/compensation/detail/compensation/view.html:48
-#: compensation/templates/compensation/detail/compensation/view.html:58
+#: compensation/templates/compensation/detail/compensation/view.html:49
+#: compensation/templates/compensation/detail/compensation/view.html:59
#: venv/lib/python3.7/site-packages/django/forms/widgets.py:711
msgid "No"
msgstr "Nein"
-#: compensation/templates/compensation/detail/compensation/view.html:53
+#: compensation/templates/compensation/detail/compensation/view.html:54
msgid "Is Coherence keeping compensation"
msgstr "Ist Kohärenzsicherungsmaßnahme"
-#: compensation/templates/compensation/detail/compensation/view.html:70
+#: compensation/templates/compensation/detail/compensation/view.html:71
#: intervention/templates/intervention/detail/view.html:75
msgid "Checked on "
msgstr "Geprüft am "
-#: compensation/templates/compensation/detail/compensation/view.html:70
-#: compensation/templates/compensation/detail/compensation/view.html:84
+#: compensation/templates/compensation/detail/compensation/view.html:71
+#: compensation/templates/compensation/detail/compensation/view.html:85
#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:56
-#: compensation/templates/compensation/detail/eco_account/view.html:51
+#: compensation/templates/compensation/detail/eco_account/view.html:52
#: ema/templates/ema/detail/view.html:42
#: intervention/templates/intervention/detail/view.html:75
#: intervention/templates/intervention/detail/view.html:89
msgid "by"
msgstr "von"
-#: compensation/templates/compensation/detail/compensation/view.html:84
-#: compensation/templates/compensation/detail/eco_account/view.html:51
+#: compensation/templates/compensation/detail/compensation/view.html:85
+#: compensation/templates/compensation/detail/eco_account/view.html:52
#: ema/templates/ema/detail/view.html:42
#: intervention/templates/intervention/detail/view.html:89
msgid "Recorded on "
msgstr "Verzeichnet am"
-#: compensation/templates/compensation/detail/compensation/view.html:91
-#: compensation/templates/compensation/detail/eco_account/view.html:74
+#: compensation/templates/compensation/detail/compensation/view.html:92
+#: compensation/templates/compensation/detail/eco_account/view.html:75
#: compensation/templates/compensation/report/compensation/report.html:24
#: compensation/templates/compensation/report/eco_account/report.html:41
#: ema/templates/ema/detail/view.html:61
@@ -977,16 +998,16 @@ msgstr "Verzeichnet am"
msgid "Last modified"
msgstr "Zuletzt bearbeitet"
-#: compensation/templates/compensation/detail/compensation/view.html:99
-#: compensation/templates/compensation/detail/eco_account/view.html:82
-#: ema/templates/ema/detail/view.html:76 intervention/forms/modalForms.py:56
+#: compensation/templates/compensation/detail/compensation/view.html:100
+#: compensation/templates/compensation/detail/eco_account/view.html:83
+#: ema/templates/ema/detail/view.html:76
#: intervention/templates/intervention/detail/view.html:116
msgid "Shared with"
msgstr "Freigegeben für"
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:15
#: ema/templates/ema/detail/includes/controls.html:15
-#: intervention/forms/modalForms.py:70
+#: intervention/forms/modalForms.py:71
#: intervention/templates/intervention/detail/includes/controls.html:15
msgid "Share"
msgstr "Freigabe"
@@ -1037,14 +1058,14 @@ msgstr "Abbuchung bearbeiten"
msgid "Remove Deduction"
msgstr "Abbuchung entfernen"
-#: compensation/templates/compensation/detail/eco_account/view.html:34
+#: compensation/templates/compensation/detail/eco_account/view.html:35
msgid "No surface deductable"
msgstr "Keine Flächenmenge für Abbuchungen eingegeben. Bitte bearbeiten."
-#: compensation/templates/compensation/detail/eco_account/view.html:57
-#: compensation/templates/compensation/detail/eco_account/view.html:61
-#: compensation/templates/compensation/detail/eco_account/view.html:65
-#: compensation/templates/compensation/detail/eco_account/view.html:69
+#: compensation/templates/compensation/detail/eco_account/view.html:58
+#: compensation/templates/compensation/detail/eco_account/view.html:62
+#: compensation/templates/compensation/detail/eco_account/view.html:66
+#: compensation/templates/compensation/detail/eco_account/view.html:70
#: ema/templates/ema/detail/view.html:48 ema/templates/ema/detail/view.html:52
#: ema/templates/ema/detail/view.html:56
#: intervention/templates/intervention/detail/view.html:30
@@ -1060,7 +1081,7 @@ msgstr "Keine Flächenmenge für Abbuchungen eingegeben. Bitte bearbeiten."
msgid "Missing"
msgstr "fehlt"
-#: compensation/templates/compensation/detail/eco_account/view.html:70
+#: compensation/templates/compensation/detail/eco_account/view.html:71
#: compensation/templates/compensation/report/eco_account/report.html:24
#: ema/templates/ema/detail/view.html:57
#: ema/templates/ema/report/report.html:24
@@ -1121,7 +1142,7 @@ msgstr "Daten zu den verantwortlichen Stellen"
msgid "Compensations - Overview"
msgstr "Kompensationen - Übersicht"
-#: compensation/views/compensation.py:151 konova/utils/message_templates.py:31
+#: compensation/views/compensation.py:151 konova/utils/message_templates.py:33
msgid "Compensation {} edited"
msgstr "Kompensation {} bearbeitet"
@@ -1130,12 +1151,12 @@ msgstr "Kompensation {} bearbeitet"
msgid "Edit {}"
msgstr "Bearbeite {}"
-#: compensation/views/compensation.py:240 compensation/views/eco_account.py:349
+#: compensation/views/compensation.py:240 compensation/views/eco_account.py:351
#: ema/views.py:194 intervention/views.py:531
msgid "Log"
msgstr "Log"
-#: compensation/views/compensation.py:584 compensation/views/eco_account.py:716
+#: compensation/views/compensation.py:584 compensation/views/eco_account.py:719
#: ema/views.py:551 intervention/views.py:677
msgid "Report {}"
msgstr "Bericht {}"
@@ -1156,32 +1177,32 @@ msgstr "Ökokonto {} bearbeitet"
msgid "Eco-account removed"
msgstr "Ökokonto entfernt"
-#: compensation/views/eco_account.py:370 ema/views.py:275
+#: compensation/views/eco_account.py:372 ema/views.py:275
#: intervention/views.py:630
msgid "{} unrecorded"
msgstr "{} entzeichnet"
-#: compensation/views/eco_account.py:370 ema/views.py:275
+#: compensation/views/eco_account.py:372 ema/views.py:275
#: intervention/views.py:630
msgid "{} recorded"
msgstr "{} verzeichnet"
-#: compensation/views/eco_account.py:789 ema/views.py:617
+#: compensation/views/eco_account.py:792 ema/views.py:617
#: intervention/views.py:428
msgid "{} has already been shared with you"
msgstr "{} wurde bereits für Sie freigegeben"
-#: compensation/views/eco_account.py:794 ema/views.py:622
+#: compensation/views/eco_account.py:797 ema/views.py:622
#: intervention/views.py:433
msgid "{} has been shared with you"
msgstr "{} ist nun für Sie freigegeben"
-#: compensation/views/eco_account.py:801 ema/views.py:629
+#: compensation/views/eco_account.py:804 ema/views.py:629
#: intervention/views.py:440
msgid "Share link invalid"
msgstr "Freigabelink ungültig"
-#: compensation/views/eco_account.py:824 ema/views.py:652
+#: compensation/views/eco_account.py:827 ema/views.py:652
#: intervention/views.py:463
msgid "Share settings updated"
msgstr "Freigabe Einstellungen aktualisiert"
@@ -1248,7 +1269,7 @@ msgstr "Mehrfachauswahl möglich"
#: intervention/forms/forms.py:86
#: intervention/templates/intervention/detail/view.html:48
#: intervention/templates/intervention/report/report.html:29
-#: intervention/utils/quality.py:46 konova/filters/mixins.py:363
+#: intervention/utils/quality.py:46 konova/filters/mixins.py:364
msgid "Registration office"
msgstr "Zulassungsbehörde"
@@ -1301,60 +1322,68 @@ msgstr "Freigabelink"
#: intervention/forms/modalForms.py:31
msgid "Send this link to users who you want to have writing access on the data"
-msgstr "Andere Nutzer erhalten über diesen Link Zugriff auf die Daten"
+msgstr "Einzelne Nutzer erhalten über diesen Link Zugriff auf die Daten"
#: intervention/forms/modalForms.py:41
-msgid "Add user to share with"
-msgstr "Nutzer direkt hinzufügen"
+msgid "Add team to share with"
+msgstr "Team hinzufügen"
#: intervention/forms/modalForms.py:43
msgid ""
+"Multiple selection possible - You can only select teams which do not already "
+"have access."
+msgstr ""
+"Mehrfachauswahl möglich - Sie können nur Teams wählen, für die der Eintrag "
+"noch nicht freigegeben wurde."
+
+#: intervention/forms/modalForms.py:55
+msgid "Add user to share with"
+msgstr "Nutzer einzeln hinzufügen"
+
+#: intervention/forms/modalForms.py:57
+msgid ""
"Multiple selection possible - You can only select users which do not already "
"have access. Enter the full username."
msgstr ""
"Mehrfachauswahl möglich - Sie können nur Nutzer wählen, für die der Eintrag "
"noch nicht freigegeben wurde. Geben Sie den ganzen Nutzernamen an."
-#: intervention/forms/modalForms.py:59
-msgid "Remove check to remove access for this user"
-msgstr "Wählen Sie die Nutzer ab, die keinen Zugriff mehr haben sollen"
-
-#: intervention/forms/modalForms.py:71
+#: intervention/forms/modalForms.py:72
msgid "Share settings for {}"
msgstr "Freigabe Einstellungen für {}"
-#: intervention/forms/modalForms.py:128
+#: intervention/forms/modalForms.py:151
msgid "Date of revocation"
msgstr "Datum des Widerspruchs"
-#: intervention/forms/modalForms.py:139
+#: intervention/forms/modalForms.py:162
#: intervention/templates/intervention/detail/includes/revocation.html:35
msgid "Document"
msgstr "Dokument"
-#: intervention/forms/modalForms.py:142
+#: intervention/forms/modalForms.py:165
msgid "Must be smaller than 15 Mb"
msgstr "Muss kleiner als 15 Mb sein"
-#: intervention/forms/modalForms.py:167
+#: intervention/forms/modalForms.py:190
#: intervention/templates/intervention/detail/includes/revocation.html:18
msgid "Add revocation"
msgstr "Widerspruch hinzufügen"
-#: intervention/forms/modalForms.py:224
+#: intervention/forms/modalForms.py:247
msgid "Checked intervention data"
msgstr "Eingriffsdaten geprüft"
-#: intervention/forms/modalForms.py:230
+#: intervention/forms/modalForms.py:253
msgid "Checked compensations data and payments"
msgstr "Kompensationen und Zahlungen geprüft"
-#: intervention/forms/modalForms.py:239
+#: intervention/forms/modalForms.py:262
#: intervention/templates/intervention/detail/includes/controls.html:19
msgid "Run check"
msgstr "Prüfung vornehmen"
-#: intervention/forms/modalForms.py:240 konova/forms.py:514
+#: intervention/forms/modalForms.py:263 konova/forms.py:514
msgid ""
"I, {} {}, confirm that all necessary control steps have been performed by "
"myself."
@@ -1362,23 +1391,23 @@ msgstr ""
"Ich, {} {}, bestätige, dass die notwendigen Kontrollschritte durchgeführt "
"wurden:"
-#: intervention/forms/modalForms.py:324
+#: intervention/forms/modalForms.py:347
msgid "Only recorded accounts can be selected for deductions"
msgstr "Nur verzeichnete Ökokonten können für Abbuchungen verwendet werden."
-#: intervention/forms/modalForms.py:351
+#: intervention/forms/modalForms.py:374
msgid "Only shared interventions can be selected"
msgstr "Nur freigegebene Eingriffe können gewählt werden"
-#: intervention/forms/modalForms.py:364
+#: intervention/forms/modalForms.py:387
msgid "New Deduction"
msgstr "Neue Abbuchung"
-#: intervention/forms/modalForms.py:365
+#: intervention/forms/modalForms.py:388
msgid "Enter the information for a new deduction from a chosen eco-account"
msgstr "Geben Sie die Informationen für eine neue Abbuchung ein."
-#: intervention/forms/modalForms.py:408
+#: intervention/forms/modalForms.py:431
msgid ""
"Eco-account {} is not recorded yet. You can only deduct from recorded "
"accounts."
@@ -1386,7 +1415,7 @@ msgstr ""
"Ökokonto {} ist noch nicht verzeichnet. Abbuchungen können nur von "
"verzeichneten Ökokonten erfolgen."
-#: intervention/forms/modalForms.py:418
+#: intervention/forms/modalForms.py:441
msgid ""
"The account {} has not enough surface for a deduction of {} m². There are "
"only {} m² left"
@@ -1581,15 +1610,15 @@ msgstr "Nach Flurstücknenner suchen"
msgid "Show unshared"
msgstr "Nicht freigegebene anzeigen"
-#: konova/filters/mixins.py:314
+#: konova/filters/mixins.py:315
msgid "Show recorded"
msgstr "Verzeichnete anzeigen"
-#: konova/filters/mixins.py:364
+#: konova/filters/mixins.py:365
msgid "Search for registration office"
msgstr "Nach Zulassungsbehörde suchen"
-#: konova/filters/mixins.py:396
+#: konova/filters/mixins.py:397
msgid "Search for conservation office"
msgstr "Nch Eintragungsstelle suchen"
@@ -1746,6 +1775,11 @@ msgstr "Neu"
msgid "Show"
msgstr "Anzeigen"
+#: konova/templates/konova/widgets/checkbox-tree-select.html:4
+#: templates/generic_index.html:56
+msgid "Search"
+msgstr "Suchen"
+
#: konova/templates/konova/widgets/generate-content-input.html:6
msgid "Generate new"
msgstr "Neu generieren"
@@ -1762,30 +1796,34 @@ msgstr "In Zwischenablage kopiert"
msgid "{} - Shared access removed"
msgstr "{} - Zugriff entzogen"
-#: konova/utils/mailer.py:91
+#: konova/utils/mailer.py:91 konova/utils/mailer.py:114
msgid "{} - Shared access given"
msgstr "{} - Zugriff freigegeben"
-#: konova/utils/mailer.py:114
+#: konova/utils/mailer.py:137
msgid "{} - Shared data recorded"
msgstr "{} - Freigegebene Daten verzeichnet"
-#: konova/utils/mailer.py:137
+#: konova/utils/mailer.py:160
msgid "{} - Shared data unrecorded"
msgstr "{} - Freigegebene Daten entzeichnet"
-#: konova/utils/mailer.py:160
+#: konova/utils/mailer.py:183
msgid "{} - Shared data deleted"
msgstr "{} - Freigegebene Daten gelöscht"
-#: konova/utils/mailer.py:183
+#: konova/utils/mailer.py:206
msgid "{} - Shared data checked"
msgstr "{} - Freigegebene Daten geprüft"
-#: konova/utils/mailer.py:204 templates/email/api/verify_token.html:4
+#: konova/utils/mailer.py:227 templates/email/api/verify_token.html:4
msgid "Request for new API token"
msgstr "Anfrage für neuen API Token"
+#: konova/utils/message_templates.py:10
+msgid "Ungrouped"
+msgstr "Ohne Zuordnung"
+
#: konova/utils/message_templates.py:11
msgid "There was an error on this form."
msgstr "Es gab einen Fehler im Formular."
@@ -1807,10 +1845,25 @@ msgstr ""
"der Zwischenzeit angelegt wurde, welcher diese Kennung nun bereits verwendet"
#: konova/utils/message_templates.py:15
+msgid ""
+"Only conservation or registration office users are allowed to remove entries."
+msgstr ""
+"Nur Mitarbeiter der Naturschutz- oder Zulassungsbehördengruppe dürfen "
+"Einträge entfernen"
+
+#: konova/utils/message_templates.py:16
+msgid "You need to be part of another user group."
+msgstr "Hierfür müssen Sie einer anderen Nutzergruppe angehören!"
+
+#: konova/utils/message_templates.py:17
+msgid "Status of Checked and Recorded reseted"
+msgstr "'Geprüft'/'Verzeichnet' wurde zurückgesetzt"
+
+#: konova/utils/message_templates.py:20
msgid "This data is not shared with you"
msgstr "Diese Daten sind für Sie nicht freigegeben"
-#: konova/utils/message_templates.py:16
+#: konova/utils/message_templates.py:21
msgid ""
"Remember: This data has not been shared with you, yet. This means you can "
"only read but can not edit or perform any actions like running a check or "
@@ -1820,23 +1873,15 @@ msgstr ""
"bedeutet, dass Sie nur lesenden Zugriff hierauf haben und weder bearbeiten, "
"noch Prüfungen durchführen oder verzeichnen können."
-#: konova/utils/message_templates.py:17
-msgid "You need to be part of another user group."
-msgstr "Hierfür müssen Sie einer anderen Nutzergruppe angehören!"
-
-#: konova/utils/message_templates.py:19
-msgid "Status of Checked and Recorded reseted"
-msgstr "'Geprüft'/'Verzeichnet' wurde zurückgesetzt"
-
-#: konova/utils/message_templates.py:22
+#: konova/utils/message_templates.py:24
msgid "Unsupported file type"
msgstr "Dateiformat nicht unterstützt"
-#: konova/utils/message_templates.py:23
+#: konova/utils/message_templates.py:25
msgid "File too large"
msgstr "Datei zu groß"
-#: konova/utils/message_templates.py:26
+#: konova/utils/message_templates.py:28
msgid ""
"Action canceled. Eco account is recorded or deductions exist. Only "
"conservation office member can perform this action."
@@ -1844,119 +1889,119 @@ msgstr ""
"Aktion abgebrochen. Ökokonto ist bereits verzeichnet oder Abbuchungen liegen "
"vor. Nur Eintragungsstellennutzer können diese Aktion jetzt durchführen."
-#: konova/utils/message_templates.py:29
+#: konova/utils/message_templates.py:31
msgid "Compensation {} added"
msgstr "Kompensation {} hinzugefügt"
-#: konova/utils/message_templates.py:30
+#: konova/utils/message_templates.py:32
msgid "Compensation {} removed"
msgstr "Kompensation {} entfernt"
-#: konova/utils/message_templates.py:32
+#: konova/utils/message_templates.py:34
msgid "Added compensation action"
msgstr "Maßnahme hinzugefügt"
-#: konova/utils/message_templates.py:33
+#: konova/utils/message_templates.py:35
msgid "Added compensation state"
msgstr "Zustand hinzugefügt"
-#: konova/utils/message_templates.py:36
+#: konova/utils/message_templates.py:38
msgid "State removed"
msgstr "Zustand gelöscht"
-#: konova/utils/message_templates.py:37
+#: konova/utils/message_templates.py:39
msgid "State edited"
msgstr "Zustand bearbeitet"
-#: konova/utils/message_templates.py:38
+#: konova/utils/message_templates.py:40
msgid "State added"
msgstr "Zustand hinzugefügt"
-#: konova/utils/message_templates.py:41
+#: konova/utils/message_templates.py:43
msgid "Action added"
msgstr "Maßnahme hinzugefügt"
-#: konova/utils/message_templates.py:42
+#: konova/utils/message_templates.py:44
msgid "Action edited"
msgstr "Maßnahme bearbeitet"
-#: konova/utils/message_templates.py:43
+#: konova/utils/message_templates.py:45
msgid "Action removed"
msgstr "Maßnahme entfernt"
-#: konova/utils/message_templates.py:46
+#: konova/utils/message_templates.py:48
msgid "Deduction added"
msgstr "Abbuchung hinzugefügt"
-#: konova/utils/message_templates.py:47
+#: konova/utils/message_templates.py:49
msgid "Deduction edited"
msgstr "Abbuchung bearbeitet"
-#: konova/utils/message_templates.py:48
+#: konova/utils/message_templates.py:50
msgid "Deduction removed"
msgstr "Abbuchung entfernt"
-#: konova/utils/message_templates.py:51
+#: konova/utils/message_templates.py:53
msgid "Deadline added"
msgstr "Frist/Termin hinzugefügt"
-#: konova/utils/message_templates.py:52
+#: konova/utils/message_templates.py:54
msgid "Deadline edited"
msgstr "Frist/Termin bearbeitet"
-#: konova/utils/message_templates.py:53
+#: konova/utils/message_templates.py:55
msgid "Deadline removed"
msgstr "Frist/Termin gelöscht"
-#: konova/utils/message_templates.py:56
+#: konova/utils/message_templates.py:58
msgid "Payment added"
msgstr "Zahlung hinzugefügt"
-#: konova/utils/message_templates.py:57
+#: konova/utils/message_templates.py:59
msgid "Payment edited"
msgstr "Zahlung bearbeitet"
-#: konova/utils/message_templates.py:58
+#: konova/utils/message_templates.py:60
msgid "Payment removed"
msgstr "Zahlung gelöscht"
-#: konova/utils/message_templates.py:61
+#: konova/utils/message_templates.py:63
msgid "Revocation added"
msgstr "Widerspruch hinzugefügt"
-#: konova/utils/message_templates.py:62
+#: konova/utils/message_templates.py:64
msgid "Revocation edited"
msgstr "Widerspruch bearbeitet"
-#: konova/utils/message_templates.py:63
+#: konova/utils/message_templates.py:65
msgid "Revocation removed"
msgstr "Widerspruch entfernt"
-#: konova/utils/message_templates.py:66
+#: konova/utils/message_templates.py:68
msgid "Document '{}' deleted"
msgstr "Dokument '{}' gelöscht"
-#: konova/utils/message_templates.py:67
+#: konova/utils/message_templates.py:69
msgid "Document added"
msgstr "Dokument hinzugefügt"
-#: konova/utils/message_templates.py:68
+#: konova/utils/message_templates.py:70
msgid "Document edited"
msgstr "Dokument bearbeitet"
-#: konova/utils/message_templates.py:71
+#: konova/utils/message_templates.py:73
msgid "Edited general data"
msgstr "Allgemeine Daten bearbeitet"
-#: konova/utils/message_templates.py:72
+#: konova/utils/message_templates.py:74
msgid "Added deadline"
msgstr "Frist/Termin hinzugefügt"
-#: konova/utils/message_templates.py:75
+#: konova/utils/message_templates.py:77
msgid "Geometry conflict detected with {}"
msgstr "Geometriekonflikt mit folgenden Einträgen erkannt: {}"
-#: konova/utils/message_templates.py:78
+#: konova/utils/message_templates.py:80
msgid "This intervention has {} revocations"
msgstr "Dem Eingriff liegen {} Widersprüche vor"
@@ -1976,7 +2021,7 @@ msgstr "{} wurde erfolgreich vom Nutzer {} geprüft! {}"
msgid "missing"
msgstr "fehlt"
-#: konova/views.py:96 templates/navbars/navbar.html:16
+#: konova/views.py:99 templates/navbars/navbar.html:16
msgid "Home"
msgstr "Home"
@@ -2047,7 +2092,9 @@ msgstr ""
#: templates/email/recording/shared_data_recorded.html:19
#: templates/email/recording/shared_data_unrecorded.html:19
#: templates/email/sharing/shared_access_given.html:20
+#: templates/email/sharing/shared_access_given_team.html:20
#: templates/email/sharing/shared_access_removed.html:20
+#: templates/email/sharing/shared_access_removed_team.html:20
msgid "Best regards"
msgstr "Beste Grüße"
@@ -2134,6 +2181,7 @@ msgstr ""
"zugehörigen Kompensationen automatisch entzeichnet worden sind."
#: templates/email/sharing/shared_access_given.html:4
+#: templates/email/sharing/shared_access_given_team.html:4
msgid "Access shared"
msgstr "Zugriff freigegeben"
@@ -2142,10 +2190,12 @@ msgid "the following dataset has just been shared with you"
msgstr "der folgende Datensatz wurde soeben für Sie freigegeben "
#: templates/email/sharing/shared_access_given.html:16
+#: templates/email/sharing/shared_access_given_team.html:16
msgid "This means you can now edit this dataset."
msgstr "Das bedeutet, dass Sie diesen Datensatz nun auch bearbeiten können."
#: templates/email/sharing/shared_access_given.html:17
+#: templates/email/sharing/shared_access_given_team.html:17
msgid ""
"The shared dataset appears now by default on your overview for this dataset "
"type."
@@ -2154,6 +2204,7 @@ msgstr ""
"Datensatztyp im KSP gelistet."
#: templates/email/sharing/shared_access_given.html:27
+#: templates/email/sharing/shared_access_given_team.html:27
msgid ""
"Please note: Shared access on an intervention means you automatically have "
"editing access to related compensations."
@@ -2162,7 +2213,17 @@ msgstr ""
"Sie automatisch auch Zugriff auf die zugehörigen Kompensationen erhalten "
"haben."
+#: templates/email/sharing/shared_access_given_team.html:8
+#: templates/email/sharing/shared_access_removed_team.html:8
+msgid "Hello team"
+msgstr "Hallo Team"
+
+#: templates/email/sharing/shared_access_given_team.html:10
+msgid "the following dataset has just been shared with your team"
+msgstr "der folgende Datensatz wurde soeben für Ihr Team freigegeben "
+
#: templates/email/sharing/shared_access_removed.html:4
+#: templates/email/sharing/shared_access_removed_team.html:4
msgid "Shared access removed"
msgstr "Freigegebener Zugriff entzogen"
@@ -2174,10 +2235,12 @@ msgstr ""
"entzogen: "
#: templates/email/sharing/shared_access_removed.html:16
+#: templates/email/sharing/shared_access_removed_team.html:16
msgid "However, you are still able to view the dataset content."
msgstr "Sie können den Datensatz aber immer noch im KSP einsehen."
#: templates/email/sharing/shared_access_removed.html:17
+#: templates/email/sharing/shared_access_removed_team.html:17
msgid ""
"Please use the provided search filter on the dataset`s overview pages to "
"find them."
@@ -2185,6 +2248,14 @@ msgstr ""
"Nutzen Sie hierzu einfach die entsprechenden Suchfilter auf den "
"Übersichtsseiten"
+#: templates/email/sharing/shared_access_removed_team.html:10
+msgid ""
+"your teams shared access, including editing, has been revoked for the "
+"dataset "
+msgstr ""
+"Ihrem Team wurde soeben der bearbeitende Zugriff auf den folgenden Datensatz "
+"entzogen: "
+
#: templates/email/signature.html:6
msgid "Please do not reply on this mail."
msgstr "Bitte antworten Sie nicht auf diese Mail."
@@ -2243,7 +2314,7 @@ msgstr "* sind Pflichtfelder."
msgid "New entry"
msgstr "Neuer Eintrag"
-#: templates/generic_index.html:41
+#: templates/generic_index.html:41 user/templates/user/team/index.html:22
msgid "New"
msgstr "Neu"
@@ -2251,10 +2322,6 @@ msgstr "Neu"
msgid "Search for keywords"
msgstr "Nach Schlagwörtern suchen"
-#: templates/generic_index.html:56
-msgid "Search"
-msgstr "Suchen"
-
#: templates/generic_index.html:57
msgid "Start search"
msgstr "Starte Suche"
@@ -2376,6 +2443,62 @@ msgstr "Neuen Token generieren"
msgid "A new token needs to be validated by an administrator!"
msgstr "Neue Tokens müssen durch Administratoren freigeschaltet werden!"
+#: user/forms.py:168 user/forms.py:172 user/forms.py:323 user/forms.py:328
+msgid "Team name"
+msgstr "Team Name"
+
+#: user/forms.py:179 user/forms.py:336 user/templates/user/team/index.html:30
+msgid "Description"
+msgstr "Beschreibung"
+
+#: user/forms.py:188
+msgid "Manage team members"
+msgstr "Mitglieder verwalten"
+
+#: user/forms.py:190
+msgid ""
+"Multiple selection possible - You can only select users which are not "
+"already a team member. Enter the full username or e-mail."
+msgstr ""
+"Mehrfachauswahl möglich - Sie können nur Nutzer wählen, die noch nicht "
+"Mitglieder dieses Teams sind. Geben Sie den ganzen Nutzernamen an."
+
+#: user/forms.py:204
+msgid "Create new team"
+msgstr "Neues Team anlegen"
+
+#: user/forms.py:205
+msgid ""
+"You will become the administrator for this group by default. You do not need "
+"to add yourself to the list of members."
+msgstr ""
+"Sie werden standardmäßig der Administrator dieses Teams. Sie müssen sich "
+"selbst nicht zur Liste der Mitglieder hinzufügen."
+
+#: user/forms.py:218 user/forms.py:279
+msgid "Name already taken. Try another."
+msgstr "Name bereits vergeben. Probieren Sie einen anderen."
+
+#: user/forms.py:249
+msgid "Admin"
+msgstr "Administrator"
+
+#: user/forms.py:250
+msgid "Administrators manage team details and members"
+msgstr "Administratoren verwalten die Teamdaten und Mitglieder"
+
+#: user/forms.py:263
+msgid "Selected admin ({}) needs to be a member of this team."
+msgstr "Gewählter Administrator ({}) muss ein Mitglied des Teams sein."
+
+#: user/forms.py:291 user/templates/user/team/index.html:51
+msgid "Edit team"
+msgstr "Team bearbeiten"
+
+#: user/forms.py:347
+msgid "Team"
+msgstr "Team"
+
#: user/models/user_action.py:22
msgid "Unrecorded"
msgstr "Entzeichnet"
@@ -2392,7 +2515,11 @@ msgstr "Gelöscht"
msgid "Show contact data"
msgstr "Zeige Kontaktdaten"
-#: user/templates/user/index.html:13
+#: user/templates/user/includes/team_data_modal_button.html:3
+msgid "Show team data"
+msgstr "Zeige Teamdaten"
+
+#: user/templates/user/index.html:13 user/templates/user/team/index.html:29
msgid "Name"
msgstr ""
@@ -2437,6 +2564,27 @@ msgstr "API token einsehen oder neu generieren"
msgid "API"
msgstr ""
+#: user/templates/user/index.html:66
+msgid "Manage teams"
+msgstr ""
+
+#: user/templates/user/index.html:69 user/templates/user/team/index.html:18
+#: user/views.py:167
+msgid "Teams"
+msgstr ""
+
+#: user/templates/user/team/index.html:20
+msgid "Add new team"
+msgstr "Neues Team hinzufügen"
+
+#: user/templates/user/team/index.html:31
+msgid "Members"
+msgstr "Mitglieder"
+
+#: user/templates/user/team/index.html:54
+msgid "Remove team"
+msgstr "Team entfernen"
+
#: user/templates/user/token.html:6
msgid "API settings"
msgstr "API Einstellungen"
@@ -2461,26 +2609,38 @@ msgstr "Token noch nicht freigeschaltet"
msgid "Valid until"
msgstr "Läuft ab am"
-#: user/views.py:31
+#: user/views.py:33
msgid "User settings"
msgstr "Einstellungen"
-#: user/views.py:57
+#: user/views.py:59
msgid "Notifications edited"
msgstr "Benachrichtigungen bearbeitet"
-#: user/views.py:69
+#: user/views.py:71
msgid "User notifications"
msgstr "Benachrichtigungen"
-#: user/views.py:92
+#: user/views.py:94
msgid "New token generated. Administrators need to validate."
msgstr "Neuer Token generiert. Administratoren sind informiert."
-#: user/views.py:103
+#: user/views.py:105
msgid "User API token"
msgstr "API Nutzer Token"
+#: user/views.py:178
+msgid "New team added"
+msgstr "Neues Team hinzugefügt"
+
+#: user/views.py:191
+msgid "Team edited"
+msgstr "Team bearbeitet"
+
+#: user/views.py:204
+msgid "Team removed"
+msgstr "Team gelöscht"
+
#: venv/lib/python3.7/site-packages/bootstrap4/components.py:17
#: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/form_errors.html:3
#: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/messages.html:4
@@ -3984,6 +4144,15 @@ msgstr ""
msgid "Unable to connect to qpid with SASL mechanism %s"
msgstr ""
+#~ msgid "your teams"
+#~ msgstr "Team entfernen"
+
+#~ msgid "Remove check to remove access for this user"
+#~ msgstr "Wählen Sie die Nutzer ab, die keinen Zugriff mehr haben sollen"
+
+#~ msgid "Select the action type"
+#~ msgstr "Maßnahmentyp wählen"
+
#~ msgid "No revocation"
#~ msgstr "Kein Widerspruch"
@@ -3993,9 +4162,6 @@ msgstr ""
#~ msgid "General data edited"
#~ msgstr "Allgemeine Daten bearbeitet"
-#~ msgid "Action type details"
-#~ msgstr "Zusatzmerkmale"
-
#~ msgid "On registered data edited"
#~ msgstr "Wenn meine freigegebenen Daten bearbeitet wurden"
diff --git a/templates/email/checking/shared_data_checked_team.html b/templates/email/checking/shared_data_checked_team.html
new file mode 100644
index 0000000..ee81381
--- /dev/null
+++ b/templates/email/checking/shared_data_checked_team.html
@@ -0,0 +1,28 @@
+{% load i18n %}
+
+
+
{% trans 'Shared data checked' %}
+
{{obj_identifier}}
+
+
+ {% trans 'Hello team' %} {{team.name}},
+
+ {% trans 'the following dataset has just been checked' %}
+
+ {{obj_identifier}}
+
+ {{obj_title}}
+
+ {% trans 'This means, the responsible registration office just confirmed the correctness of this dataset.' %}
+
+
+ {% trans 'Best regards' %}
+
+ KSP
+
+
+
+ {% include 'email/signature.html' %}
+
+
+
diff --git a/templates/email/deleting/shared_data_deleted_team.html b/templates/email/deleting/shared_data_deleted_team.html
new file mode 100644
index 0000000..cedb2a4
--- /dev/null
+++ b/templates/email/deleting/shared_data_deleted_team.html
@@ -0,0 +1,28 @@
+{% load i18n %}
+
+
+
{% trans 'Shared data deleted' %}
+
{{obj_identifier}}
+
+
+ {% trans 'Hello team' %} {{team.name}},
+
+ {% trans 'the following dataset has just been deleted' %}
+
+ {{obj_identifier}}
+
+ "{{obj_title}}"
+
+ {% trans 'If this should not have been happened, please contact us. See the signature for details.' %}
+
+
+ {% trans 'Best regards' %}
+
+ KSP
+
+
+
+ {% include 'email/signature.html' %}
+
+
+
diff --git a/templates/email/recording/shared_data_recorded_team.html b/templates/email/recording/shared_data_recorded_team.html
new file mode 100644
index 0000000..12efa8f
--- /dev/null
+++ b/templates/email/recording/shared_data_recorded_team.html
@@ -0,0 +1,33 @@
+{% load i18n %}
+
+
+
{% trans 'Shared data recorded' %}
+
{{obj_identifier}}
+
+
+ {% trans 'Hello team' %} {{team.name}},
+
+ {% trans 'the following dataset has just been recorded' %}
+
+ {{obj_identifier}}
+
+ "{{obj_title}}"
+
+ {% trans 'This means the data is now publicly available, e.g. in LANIS' %}
+
+
+ {% trans 'Best regards' %}
+
+ KSP
+
+
+
+
+ {% trans 'Please note: Recorded intervention means the compensations are recorded as well.' %}
+
+
+
+ {% include 'email/signature.html' %}
+
+
+
diff --git a/templates/email/recording/shared_data_unrecorded_team.html b/templates/email/recording/shared_data_unrecorded_team.html
new file mode 100644
index 0000000..6414155
--- /dev/null
+++ b/templates/email/recording/shared_data_unrecorded_team.html
@@ -0,0 +1,33 @@
+{% load i18n %}
+
+
+
{% trans 'Shared data unrecorded' %}
+
{{obj_identifier}}
+
+
+ {% trans 'Hello team' %} {{team.name}},
+
+ {% trans 'the following dataset has just been unrecorded' %}
+
+ {{obj_identifier}}
+
+ "{{obj_title}}"
+
+ {% trans 'This means the data is no longer publicly available.' %}
+
+
+ {% trans 'Best regards' %}
+
+ KSP
+
+
+
+
+ {% trans 'Please note: Unrecorded intervention means the compensations are unrecorded as well.' %}
+
+
+
+ {% include 'email/signature.html' %}
+
+
+
diff --git a/templates/email/sharing/shared_access_given_team.html b/templates/email/sharing/shared_access_given_team.html
new file mode 100644
index 0000000..990ba2d
--- /dev/null
+++ b/templates/email/sharing/shared_access_given_team.html
@@ -0,0 +1,34 @@
+{% load i18n %}
+
+
+
{% trans 'Access shared' %}
+
{{obj_identifier}}
+
+
+ {% trans 'Hello team' %} {{team.name}},
+
+ {% trans 'the following dataset has just been shared with your team' %}
+
+ {{obj_identifier}}
+
+ "{{obj_title}}"
+
+ {% trans 'This means you can now edit this dataset.' %}
+ {% trans 'The shared dataset appears now by default on your overview for this dataset type.' %}
+
+
+ {% trans 'Best regards' %}
+
+ KSP
+
+
+
+
+ {% trans 'Please note: Shared access on an intervention means you automatically have editing access to related compensations.' %}
+
+
+
+ {% include 'email/signature.html' %}
+
+
+
diff --git a/templates/email/sharing/shared_access_removed_team.html b/templates/email/sharing/shared_access_removed_team.html
new file mode 100644
index 0000000..5472ef7
--- /dev/null
+++ b/templates/email/sharing/shared_access_removed_team.html
@@ -0,0 +1,29 @@
+{% load i18n %}
+
+
+
{% trans 'Shared access removed' %}
+
{{obj_identifier}}
+
+
+ {% trans 'Hello team' %} {{team.name}},
+
+ {% trans 'your teams shared access, including editing, has been revoked for the dataset ' %}
+
+ {{obj_identifier}}
+
+ "{{obj_title}}"
+
+ {% trans 'However, you are still able to view the dataset content.' %}
+ {% trans 'Please use the provided search filter on the dataset`s overview pages to find them.' %}
+
+
+ {% trans 'Best regards' %}
+
+ KSP
+
+
+
+ {% include 'email/signature.html' %}
+
+
+
diff --git a/user/admin.py b/user/admin.py
index 3e60861..1aeacee 100644
--- a/user/admin.py
+++ b/user/admin.py
@@ -1,6 +1,6 @@
from django.contrib import admin
-from user.models import UserNotification, UserActionLogEntry, User
+from user.models import UserNotification, UserActionLogEntry, User, Team
class UserNotificationAdmin(admin.ModelAdmin):
@@ -64,7 +64,20 @@ class UserActionLogEntryAdmin(admin.ModelAdmin):
]
+class TeamAdmin(admin.ModelAdmin):
+ list_display = [
+ "name",
+ "description",
+ "admin",
+ ]
+ search_fields = [
+ "name",
+ "description",
+ ]
+
+
admin.site.register(User, UserAdmin)
+admin.site.register(Team, TeamAdmin)
# Outcommented for a cleaner admin backend on production
#admin.site.register(UserNotification, UserNotificationAdmin)
diff --git a/user/forms.py b/user/forms.py
index fd66a91..34c8fab 100644
--- a/user/forms.py
+++ b/user/forms.py
@@ -5,17 +5,17 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 08.07.21
"""
+from dal import autocomplete
from django import forms
-from django.db import IntegrityError
+from django.db import IntegrityError, transaction
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from api.models import APIUserToken
from intervention.inputs import GenerateInput
-from user.models import User
+from user.models import User, UserNotification, Team
-from konova.forms import BaseForm, BaseModalForm
-from user.models import UserNotification
+from konova.forms import BaseForm, BaseModalForm, RemoveModalForm
class UserNotificationForm(BaseForm):
@@ -160,3 +160,201 @@ class UserAPITokenForm(BaseForm):
user.api_token = new_token
user.save()
return new_token
+
+
+class NewTeamModalForm(BaseModalForm):
+ name = forms.CharField(
+ label_suffix="",
+ label=_("Team name"),
+ max_length=500,
+ widget=forms.TextInput(
+ attrs={
+ "placeholder": _("Team name"),
+ "class": "form-control",
+ }
+ )
+ )
+ description = forms.CharField(
+ label_suffix="",
+ label=_("Description"),
+ widget=forms.Textarea(
+ attrs={
+ "rows": 5,
+ "class": "form-control"
+ }
+ )
+ )
+ members = forms.ModelMultipleChoiceField(
+ label=_("Manage team members"),
+ label_suffix="",
+ help_text=_("Multiple selection possible - You can only select users which are not already a team member. Enter the full username or e-mail."),
+ required=True,
+ queryset=User.objects.all(),
+ widget=autocomplete.ModelSelect2Multiple(
+ url="share-user-autocomplete",
+ attrs={
+ "data-placeholder": _("Click for selection"),
+ "data-minimum-input-length": 3,
+ },
+ ),
+ )
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.form_title = _("Create new team")
+ self.form_caption = _("You will become the administrator for this group by default. You do not need to add yourself to the list of members.")
+ self.action_url = reverse("user:team-new")
+ self.cancel_redirect = reverse("user:team-index")
+
+ def _is_name_valid(self):
+ name = self.cleaned_data.get("name", None)
+ teams_with_same_name = Team.objects.filter(
+ name=name
+ )
+ name_valid = not teams_with_same_name.exists()
+ if not name_valid:
+ self.add_error(
+ "name",
+ _("Name already taken. Try another.")
+ )
+
+ return name_valid
+
+ def is_valid(self):
+ super_valid = super().is_valid()
+ name_valid = self._is_name_valid()
+ return super_valid and name_valid
+
+ def save(self):
+ with transaction.atomic():
+ team = Team.objects.create(
+ name=self.cleaned_data.get("name", None),
+ description=self.cleaned_data.get("description", None),
+ admin=self.user,
+ )
+ members = self.cleaned_data.get("members", User.objects.none())
+ if self.user.id not in members:
+ members = members.union(
+ User.objects.filter(
+ id=self.user.id
+ )
+ )
+ team.users.set(members)
+ return team
+
+
+class EditTeamModalForm(NewTeamModalForm):
+ admin = forms.ModelChoiceField(
+ label_suffix="",
+ label=_("Admin"),
+ help_text=_("Administrators manage team details and members"),
+ queryset=User.objects.none(),
+ empty_label=None,
+ )
+
+ def __is_admin_valid(self):
+ admin = self.cleaned_data.get("admin", None)
+ members = self.cleaned_data.get("members", None)
+ _is_valid = admin in members
+
+ if not _is_valid:
+ self.add_error(
+ "members",
+ _("Selected admin ({}) needs to be a member of this team.").format(admin.username)
+ )
+
+ return _is_valid
+
+ def _is_name_valid(self):
+ name = self.cleaned_data.get("name", None)
+ teams_with_same_name = Team.objects.filter(
+ name=name
+ ).exclude(
+ id=self.instance.id
+ )
+ name_valid = not teams_with_same_name.exists()
+ if not name_valid:
+ self.add_error(
+ "name",
+ _("Name already taken. Try another.")
+ )
+
+ return name_valid
+
+ def is_valid(self):
+ super_valid = super().is_valid()
+ admin_valid = self.__is_admin_valid()
+ return super_valid and admin_valid
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.form_title = _("Edit team")
+ self.action_url = reverse("user:team-edit", args=(self.instance.id,))
+ self.cancel_redirect = reverse("user:team-index")
+
+ members = self.instance.users.all()
+ self.fields["admin"].queryset = members
+
+ form_data = {
+ "members": members,
+ "name": self.instance.name,
+ "description": self.instance.description,
+ "admin": self.instance.admin,
+ }
+ self.load_initial_data(form_data)
+
+ def save(self):
+ with transaction.atomic():
+ self.instance.name = self.cleaned_data.get("name", None)
+ self.instance.description = self.cleaned_data.get("description", None)
+ self.instance.admin = self.cleaned_data.get("admin", None)
+ self.instance.save()
+ self.instance.users.set(self.cleaned_data.get("members", []))
+ return self.instance
+
+
+class RemoveTeamModalForm(RemoveModalForm):
+ pass
+
+
+class TeamDataForm(BaseModalForm):
+ name = forms.CharField(
+ label_suffix="",
+ label=_("Team name"),
+ max_length=500,
+ required=False,
+ widget=forms.TextInput(
+ attrs={
+ "placeholder": _("Team name"),
+ "class": "form-control",
+ }
+ )
+ )
+ description = forms.CharField(
+ label_suffix="",
+ required=False,
+ label=_("Description"),
+ widget=forms.Textarea(
+ attrs={
+ "rows": 5,
+ "class": "form-control"
+ }
+ )
+ )
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.form_title = _("Team")
+ self.form_caption = ""
+ self.render_submit = False
+ form_data = {
+ "name": self.instance.name,
+ "description": self.instance.description,
+ }
+ self.load_initial_data(
+ form_data,
+ [
+ "name",
+ "description"
+ ]
+ )
\ No newline at end of file
diff --git a/user/migrations/0003_team.py b/user/migrations/0003_team.py
new file mode 100644
index 0000000..6f7ac90
--- /dev/null
+++ b/user/migrations/0003_team.py
@@ -0,0 +1,29 @@
+# Generated by Django 3.1.3 on 2022-02-17 10:22
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('user', '0002_user_api_token'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Team',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('name', models.CharField(blank=True, max_length=500, null=True)),
+ ('description', models.TextField(blank=True, null=True)),
+ ('admin', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
+ ('users', models.ManyToManyField(blank=True, related_name='teams', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ ]
diff --git a/user/models/__init__.py b/user/models/__init__.py
index 7788d8e..06dbe73 100644
--- a/user/models/__init__.py
+++ b/user/models/__init__.py
@@ -5,6 +5,7 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.11.21
"""
-from .user_action import *
-from .user import *
-from .notification import *
+from .user_action import UserActionLogEntry, UserAction
+from .user import User
+from .notification import UserNotification, UserNotificationEnum
+from .team import Team
diff --git a/user/models/team.py b/user/models/team.py
new file mode 100644
index 0000000..e36c95b
--- /dev/null
+++ b/user/models/team.py
@@ -0,0 +1,95 @@
+from django.db import models
+
+from konova.models import UuidModel
+from konova.utils.mailer import Mailer
+
+
+class Team(UuidModel):
+ """ Groups users in self managed teams. Can be used for multi-sharing of data
+
+ """
+ name = models.CharField(max_length=500, null=True, blank=True)
+ description = models.TextField(null=True, blank=True)
+ users = models.ManyToManyField("user.User", blank=True, related_name="teams")
+ admin = models.ForeignKey("user.User", blank=True, null=True, related_name="+", on_delete=models.SET_NULL)
+
+ def __str__(self):
+ return self.name
+
+ def send_mail_shared_access_given_team(self, obj_identifier, obj_title):
+ """ Sends a mail to the team members in case of given shared access
+
+ Args:
+ obj_identifier ():
+ obj_title ():
+
+ Returns:
+
+ """
+ mailer = Mailer()
+ mailer.send_mail_shared_access_given_team(obj_identifier, obj_title, self)
+
+ def send_mail_shared_access_removed(self, obj_identifier, obj_title):
+ """ Sends a mail to the team members in case of removed shared access
+
+ Args:
+ obj_identifier ():
+ obj_title ():
+
+ Returns:
+
+ """
+ mailer = Mailer()
+ mailer.send_mail_shared_access_removed_team(obj_identifier, obj_title, self)
+
+ def send_mail_shared_data_unrecorded(self, obj_identifier, obj_title):
+ """ Sends a mail to the team members in case of unrecorded data
+
+ Args:
+ obj_identifier ():
+ obj_title ():
+
+ Returns:
+
+ """
+ mailer = Mailer()
+ mailer.send_mail_shared_data_unrecorded_team(obj_identifier, obj_title, self)
+
+ def send_mail_shared_data_recorded(self, obj_identifier, obj_title):
+ """ Sends a mail to the team members in case of unrecorded data
+
+ Args:
+ obj_identifier ():
+ obj_title ():
+
+ Returns:
+
+ """
+ mailer = Mailer()
+ mailer.send_mail_shared_data_recorded_team(obj_identifier, obj_title, self)
+
+ def send_mail_shared_data_checked(self, obj_identifier, obj_title):
+ """ Sends a mail to the team members in case of checked data
+
+ Args:
+ obj_identifier ():
+ obj_title ():
+
+ Returns:
+
+ """
+ mailer = Mailer()
+ mailer.send_mail_shared_data_checked_team(obj_identifier, obj_title, self)
+
+ def send_mail_shared_data_deleted(self, obj_identifier, obj_title):
+ """ Sends a mail to the team members in case of deleted data
+
+ Args:
+ obj_identifier ():
+ obj_title ():
+
+ Returns:
+
+ """
+ mailer = Mailer()
+ mailer.send_mail_shared_data_deleted_team(obj_identifier, obj_title, self)
diff --git a/user/templates/user/includes/contact_modal_button.html b/user/templates/user/includes/contact_modal_button.html
index 68ae53a..5cbd87f 100644
--- a/user/templates/user/includes/contact_modal_button.html
+++ b/user/templates/user/includes/contact_modal_button.html
@@ -1,6 +1,6 @@
{% load fontawesome_5 i18n %}
- {% fa5_icon 'id-card' %}
+ {% fa5_icon 'user' %}
{{user.username}}
\ No newline at end of file
diff --git a/user/templates/user/includes/team_data_modal_button.html b/user/templates/user/includes/team_data_modal_button.html
new file mode 100644
index 0000000..b41e63a
--- /dev/null
+++ b/user/templates/user/includes/team_data_modal_button.html
@@ -0,0 +1,6 @@
+{% load fontawesome_5 i18n %}
+
+
+ {% fa5_icon 'users' %}
+ {{team.name}}
+
\ No newline at end of file
diff --git a/user/templates/user/index.html b/user/templates/user/index.html
index 1193340..c31de94 100644
--- a/user/templates/user/index.html
+++ b/user/templates/user/index.html
@@ -62,6 +62,14 @@
+
diff --git a/user/templates/user/team/index.html b/user/templates/user/team/index.html
new file mode 100644
index 0000000..d2040a3
--- /dev/null
+++ b/user/templates/user/team/index.html
@@ -0,0 +1,69 @@
+{% extends 'base.html' %}
+{% load i18n fontawesome_5 %}
+
+{% 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.
+ Therefore the script linkages from form.media have been extracted and put inside dal/scripts.html to ensure
+ these scripts are loaded when needed.
+ {% endcomment %}
+ {% include 'dal/scripts.html' %}
+{% endblock %}
+
+{% block body %}
+
+
{% trans 'Teams' %}
+
+
+ {% fa5_icon 'plus' %}
+ {% trans 'New' %}
+
+
+
+
+
+
+ {% trans 'Name' %}
+ {% trans 'Description' %}
+ {% trans 'Members' %}
+ {% trans 'Action' %}
+
+
+
+ {% for team in teams %}
+
+ {{team.name}}
+
+
+ {{team.description}}
+
+
+
+ {% for member in team.users.all %}
+ {{member.username}}
+ {% endfor %}
+
+
+ {% if team.admin == user %}
+
+ {% fa5_icon 'edit' %}
+
+
+ {% fa5_icon 'trash' %}
+
+ {% endif %}
+
+
+ {% endfor %}
+
+
+
+
+
+{% with 'btn-modal' as btn_class %}
+ {% include 'modal/modal_form_script.html' %}
+{% endwith %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/user/urls.py b/user/urls.py
index e312a63..5c33427 100644
--- a/user/urls.py
+++ b/user/urls.py
@@ -15,5 +15,10 @@ urlpatterns = [
path("notifications/", notifications_view, name="notifications"),
path("token/api", api_token_view, name="api-token"),
path("contact/", contact_view, name="contact"),
+ path("team/", index_team_view, name="team-index"),
+ path("team/", data_team_view, name="team-data"),
+ path("team/new", new_team_view, name="team-new"),
+ path("team//edit", edit_team_view, name="team-edit"),
+ path("team//remove", remove_team_view, name="team-remove"),
]
\ No newline at end of file
diff --git a/user/views.py b/user/views.py
index ee6aee5..9afede1 100644
--- a/user/views.py
+++ b/user/views.py
@@ -1,17 +1,19 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required
+from django.urls import reverse
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.mailer import Mailer
from konova.utils.message_templates import FORM_INVALID
-from user.models import User
-from django.http import HttpRequest
+from user.models import User, Team
+from django.http import HttpRequest, Http404
from django.shortcuts import render, redirect, get_object_or_404
from django.utils.translation import gettext_lazy as _
from konova.contexts import BaseContext
from konova.decorators import any_group_check, default_group_required
-from user.forms import UserNotificationForm, UserContactForm, UserAPITokenForm
+from user.forms import UserNotificationForm, UserContactForm, UserAPITokenForm, NewTeamModalForm, EditTeamModalForm, \
+ RemoveTeamModalForm, TeamDataForm
@login_required
@@ -128,4 +130,77 @@ def contact_view(request: HttpRequest, id: str):
request,
template,
context
- )
\ No newline at end of file
+ )
+
+
+@login_required
+def data_team_view(request: HttpRequest, id: str):
+ """ Renders team data
+
+ Args:
+ request (HttpRequest): The incoming request
+ id (str): The team's id
+
+ Returns:
+
+ """
+ team = get_object_or_404(Team, id=id)
+ form = TeamDataForm(request.POST or None, instance=team, request=request)
+ template = "modal/modal_form.html"
+ context = {
+ "form": form,
+ }
+ context = BaseContext(request, context).context
+ return render(
+ request,
+ template,
+ context
+ )
+
+
+@login_required
+def index_team_view(request: HttpRequest):
+ template = "user/team/index.html"
+ user = request.user
+ context = {
+ "teams": user.teams.all(),
+ "tab_title": _("Teams"),
+ }
+ context = BaseContext(request, context).context
+ return render(request, template, context)
+
+
+@login_required
+def new_team_view(request: HttpRequest):
+ form = NewTeamModalForm(request.POST or None, request=request)
+ return form.process_request(
+ request,
+ _("New team added"),
+ redirect_url=reverse("user:team-index")
+ )
+
+
+@login_required
+def edit_team_view(request: HttpRequest, id: str):
+ team = get_object_or_404(Team, id=id)
+ if request.user != team.admin:
+ raise Http404()
+ form = EditTeamModalForm(request.POST or None, instance=team, request=request)
+ return form.process_request(
+ request,
+ _("Team edited"),
+ redirect_url=reverse("user:team-index")
+ )
+
+
+@login_required
+def remove_team_view(request: HttpRequest, id: str):
+ team = get_object_or_404(Team, id=id)
+ if request.user != team.admin:
+ raise Http404()
+ form = RemoveTeamModalForm(request.POST or None, instance=team, request=request)
+ return form.process_request(
+ request,
+ _("Team removed"),
+ redirect_url=reverse("user:team-index")
+ )