From aa675aa0465b9855bfb0bcdc0a4964361ad5dae0 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 18 Feb 2022 13:52:27 +0100 Subject: [PATCH] #101 Team sharing tests * adds tests for team sharing * extends the API for team sharing support * adds shared_teams property shortcut for ShareableObjectMixin * adds full support for team-based sharing to all views and functions * simplifies ShareModalForm * adds/updates translations --- api/tests/v1/get/test_api_get.py | 20 ++++ .../intervention_share_update_put_body.json | 8 ++ api/tests/v1/update/test_api_update.py | 21 ++++ api/views/views.py | 48 ++++++++- compensation/filters.py | 23 +---- compensation/models/compensation.py | 40 +++++++- .../tests/ecoaccount/test_workflow.py | 2 +- intervention/forms/modalForms.py | 92 +++++++++--------- konova/autocompletes.py | 19 ++-- konova/filters/mixins.py | 5 +- konova/models/object.py | 34 +++++-- konova/tests/test_autocompletes.py | 1 + konova/tests/test_views.py | 21 +++- konova/utils/message_templates.py | 8 +- locale/de/LC_MESSAGES/django.mo | Bin 39754 -> 39816 bytes locale/de/LC_MESSAGES/django.po | 69 +++++++------ 16 files changed, 278 insertions(+), 133 deletions(-) create mode 100644 api/tests/v1/update/intervention_share_update_put_body.json diff --git a/api/tests/v1/get/test_api_get.py b/api/tests/v1/get/test_api_get.py index 5bfd67a1..953b0f69 100644 --- a/api/tests/v1/get/test_api_get.py +++ b/api/tests/v1/get/test_api_get.py @@ -85,6 +85,26 @@ 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 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 00000000..c160aeea --- /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 8689fbe3..500aec24 100644 --- a/api/tests/v1/update/test_api_update.py +++ b/api/tests/v1/update/test_api_update.py @@ -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/views/views.py b/api/views/views.py index db8b8f9e..75d764e8 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): @@ -198,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) data = { "users": [ user.username for user in users - ] + ], + "teams": [ + { + "id": team.id, + "name": team.name, + } + for team in teams + ], } return JsonResponse(data) @@ -258,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 @@ -271,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( @@ -292,7 +323,16 @@ class AbstractModelShareAPIView(AbstractAPIView): id__in=obj.shared_users ) new_users_objs = obj.shared_users.union(new_users_to_be_added) + + 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/compensation/filters.py b/compensation/filters.py index d028e3ce..b6377092 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/models/compensation.py b/compensation/models/compensation.py index 7a10aa0b..ea69cefc 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 @@ -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/tests/ecoaccount/test_workflow.py b/compensation/tests/ecoaccount/test_workflow.py index 1b350e9b..03b4996a 100644 --- a/compensation/tests/ecoaccount/test_workflow.py +++ b/compensation/tests/ecoaccount/test_workflow.py @@ -233,7 +233,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase): self.eco_account.set_recorded(self.superuser) self.intervention.share_with_user(self.superuser) self.eco_account.refresh_from_db() - self.assertIn(self.superuser, self.intervention.is_shared_with(self.superuser)) + self.assertTrue(self.superuser, self.intervention.is_shared_with(self.superuser)) deduction = EcoAccountDeduction.objects.create( intervention=self.intervention, diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py index 8e44ac2e..39479388 100644 --- a/intervention/forms/modalForms.py +++ b/intervention/forms/modalForms.py @@ -9,7 +9,7 @@ from dal import autocomplete from django.core.exceptions import ObjectDoesNotExist from konova.utils.message_templates import DEDUCTION_ADDED, REVOCATION_ADDED, DEDUCTION_REMOVED, DEDUCTION_EDITED, \ - REVOCATION_EDITED + REVOCATION_EDITED, ENTRY_REMOVE_MISSING_PERMISSION from user.models import User, Team from user.models import UserActionLogEntry from django.db import transaction @@ -37,7 +37,7 @@ class ShareModalForm(BaseModalForm): } ) ) - team_select = forms.ModelMultipleChoiceField( + teams = forms.ModelMultipleChoiceField( label=_("Add team to share with"), label_suffix="", help_text=_("Multiple selection possible - You can only select teams which do not already have access."), @@ -51,7 +51,7 @@ class ShareModalForm(BaseModalForm): }, ), ) - user_select = forms.ModelMultipleChoiceField( + users = forms.ModelMultipleChoiceField( label=_("Add user to share with"), label_suffix="", help_text=_("Multiple selection possible - You can only select users which do not already have access. Enter the full username."), @@ -63,21 +63,8 @@ class ShareModalForm(BaseModalForm): "data-placeholder": _("Click for selection"), "data-minimum-input-length": 3, }, - forward=["users"] ), ) - users = forms.MultipleChoiceField( - label=_("Shared with"), - label_suffix="", - required=True, - help_text=_("Remove check to remove access for this user"), - widget=forms.CheckboxSelectMultiple( - attrs={ - "class": "list-unstyled", - } - ), - choices=[] - ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -91,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 @@ -105,39 +134,12 @@ 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() - self._add_teams_to_field() - - def _add_teams_to_field(self): form_data = { - "team_select": self.instance.teams.all() + "teams": self.instance.teams.all(), + "users": self.instance.users.all(), } self.load_initial_data(form_data) - 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 - ) - def save(self): self.instance.update_sharing_user(self) diff --git a/konova/autocompletes.py b/konova/autocompletes.py index 9b1be2d3..5ecc50d6 100644 --- a/konova/autocompletes.py +++ b/konova/autocompletes.py @@ -76,19 +76,14 @@ class ShareUserAutocomplete(Select2QuerySetView): 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 @@ -99,13 +94,15 @@ class ShareTeamAutocomplete(Select2QuerySetView): 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 = Team.objects.filter( - Q(name__icontains=self.q) - ).order_by( - "name" + qs = qs.filter( + name__icontains=self.q ) + qs = qs.order_by( + "name" + ) return qs diff --git a/konova/filters/mixins.py b/konova/filters/mixins.py index 5625ff36..5a07e3c2 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/models/object.py b/konova/models/object.py index e6c27a66..92b3ccae 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -501,30 +501,37 @@ class ShareableObjectMixin(models.Model): form_data = form.cleaned_data # Fetch selected teams and find out which user IDs are in removed teams -> mails need to be sent - accessing_teams = form_data["team_select"] + accessing_teams = form_data["teams"] removed_team_users = self.teams.all().exclude( id__in=accessing_teams ).values_list("users__id", flat=True) - - new_accessing_users = list(form_data["user_select"].values_list("id", flat=True)) - keep_accessing_users = form_data["users"] - accessing_users = keep_accessing_users + new_accessing_users - users = User.objects.filter( - id__in=accessing_users + accessing_team_users = User.objects.filter( + id__in=accessing_teams.values_list("users", flat=True) ) + new_team_users = accessing_team_users.exclude( + id__in=self.shared_users + ).values_list("id", flat=True) + + # Fetch selected users + accessing_users = form_data["users"] removed_users = self.users.all().exclude( id__in=accessing_users ).values_list("id", flat=True) + new_users = accessing_users.exclude( + id__in=self.shared_users + ).values_list("id", flat=True) + + new_users = new_users.union(new_team_users) removed_users = removed_users.union(removed_team_users) # Send mails for user_id in removed_users: celery_send_mail_shared_access_removed.delay(self.identifier, self.title, user_id) - for user_id in new_accessing_users: + 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_user_list(users) + self.share_with_user_list(accessing_users) self.share_with_team_list(accessing_teams) @property @@ -536,6 +543,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/tests/test_autocompletes.py b/konova/tests/test_autocompletes.py index 7d9e5088..95a3508d 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 6218a6c0..ff99ea3c 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/utils/message_templates.py b/konova/utils/message_templates.py index 5809b3b6..6c0c6148 100644 --- a/konova/utils/message_templates.py +++ b/konova/utils/message_templates.py @@ -12,11 +12,13 @@ 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") +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.") -MISSING_GROUP_PERMISSION = _("You need to be part of another user group.") - -CHECKED_RECORDED_RESET = _("Status of Checked and Recorded reseted") # FILES FILE_TYPE_UNSUPPORTED = _("Unsupported file type") diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index a20ac2daaaa222c759f4d6a6e75bd9e7c9424ee3..cbd6f0b09ac2c43691e3e31e27cb6e98f171d3df 100644 GIT binary patch delta 6607 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 delta 6562 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 diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index aba79815..bb96bab4 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ #: 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:148 -#: intervention/forms/modalForms.py:161 intervention/forms/modalForms.py:174 +#: intervention/forms/forms.py:168 intervention/forms/modalForms.py:154 +#: intervention/forms/modalForms.py:167 intervention/forms/modalForms.py:180 #: 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 @@ -26,7 +26,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-02-18 09:35+0100\n" +"POT-Creation-Date: 2022-02-18 12:35+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -221,7 +221,7 @@ msgstr "Abbuchungen" #: 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:359 +#: intervention/forms/modalForms.py:365 msgid "Surface" msgstr "Fläche" @@ -284,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:370 -#: intervention/forms/modalForms.py:377 intervention/tables.py:88 +#: compensation/tables.py:89 intervention/forms/modalForms.py:376 +#: intervention/forms/modalForms.py:383 intervention/tables.py:88 #: intervention/templates/intervention/detail/view.html:19 #: konova/templates/konova/includes/quickstart/interventions.html:4 #: templates/navbars/navbar.html:22 @@ -295,7 +295,7 @@ msgstr "Eingriff" #: analysis/templates/analysis/reports/includes/old_data/amount.html:34 #: compensation/tables.py:266 #: compensation/templates/compensation/detail/eco_account/view.html:20 -#: intervention/forms/modalForms.py:343 intervention/forms/modalForms.py:350 +#: intervention/forms/modalForms.py:349 intervention/forms/modalForms.py:356 #: konova/templates/konova/includes/quickstart/ecoaccounts.html:4 #: templates/navbars/navbar.html:34 msgid "Eco-account" @@ -364,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:173 +#: intervention/forms/forms.py:180 intervention/forms/modalForms.py:179 #: intervention/templates/intervention/detail/includes/documents.html:34 #: intervention/templates/intervention/detail/includes/payments.html:34 #: intervention/templates/intervention/detail/includes/revocation.html:38 @@ -484,7 +484,7 @@ msgid "Due on which date" msgstr "Zahlung wird an diesem Datum erwartet" #: compensation/forms/modalForms.py:64 compensation/forms/modalForms.py:357 -#: intervention/forms/modalForms.py:175 konova/forms.py:395 +#: intervention/forms/modalForms.py:181 konova/forms.py:395 msgid "Additional comment, maximum {} letters" msgstr "Zusätzlicher Kommentar, maximal {} Zeichen" @@ -512,7 +512,7 @@ msgstr "Zusatzbezeichnung" msgid "Select an additional biotope type" msgstr "Zusatzbezeichnung wählen" -#: compensation/forms/modalForms.py:197 intervention/forms/modalForms.py:361 +#: compensation/forms/modalForms.py:197 intervention/forms/modalForms.py:367 msgid "in m²" msgstr "" @@ -540,7 +540,7 @@ msgstr "Fristart wählen" #: 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:147 +#: intervention/forms/modalForms.py:153 msgid "Date" msgstr "Datum" @@ -1000,14 +1000,14 @@ msgstr "Zuletzt bearbeitet" #: 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/forms/modalForms.py:70 +#: 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:84 +#: intervention/forms/modalForms.py:71 #: intervention/templates/intervention/detail/includes/controls.html:15 msgid "Share" msgstr "Freigabe" @@ -1348,46 +1348,48 @@ 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:73 -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:85 +#: intervention/forms/modalForms.py:72 msgid "Share settings for {}" msgstr "Freigabe Einstellungen für {}" -#: intervention/forms/modalForms.py:149 +#: intervention/forms/modalForms.py:105 intervention/forms/modalForms.py:111 +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" + +#: intervention/forms/modalForms.py:155 msgid "Date of revocation" msgstr "Datum des Widerspruchs" -#: intervention/forms/modalForms.py:160 +#: intervention/forms/modalForms.py:166 #: intervention/templates/intervention/detail/includes/revocation.html:35 msgid "Document" msgstr "Dokument" -#: intervention/forms/modalForms.py:163 +#: intervention/forms/modalForms.py:169 msgid "Must be smaller than 15 Mb" msgstr "Muss kleiner als 15 Mb sein" -#: intervention/forms/modalForms.py:188 +#: intervention/forms/modalForms.py:194 #: intervention/templates/intervention/detail/includes/revocation.html:18 msgid "Add revocation" msgstr "Widerspruch hinzufügen" -#: intervention/forms/modalForms.py:245 +#: intervention/forms/modalForms.py:251 msgid "Checked intervention data" msgstr "Eingriffsdaten geprüft" -#: intervention/forms/modalForms.py:251 +#: intervention/forms/modalForms.py:257 msgid "Checked compensations data and payments" msgstr "Kompensationen und Zahlungen geprüft" -#: intervention/forms/modalForms.py:260 +#: intervention/forms/modalForms.py:266 #: intervention/templates/intervention/detail/includes/controls.html:19 msgid "Run check" msgstr "Prüfung vornehmen" -#: intervention/forms/modalForms.py:261 konova/forms.py:514 +#: intervention/forms/modalForms.py:267 konova/forms.py:514 msgid "" "I, {} {}, confirm that all necessary control steps have been performed by " "myself." @@ -1395,23 +1397,23 @@ msgstr "" "Ich, {} {}, bestätige, dass die notwendigen Kontrollschritte durchgeführt " "wurden:" -#: intervention/forms/modalForms.py:345 +#: intervention/forms/modalForms.py:351 msgid "Only recorded accounts can be selected for deductions" msgstr "Nur verzeichnete Ökokonten können für Abbuchungen verwendet werden." -#: intervention/forms/modalForms.py:372 +#: intervention/forms/modalForms.py:378 msgid "Only shared interventions can be selected" msgstr "Nur freigegebene Eingriffe können gewählt werden" -#: intervention/forms/modalForms.py:385 +#: intervention/forms/modalForms.py:391 msgid "New Deduction" msgstr "Neue Abbuchung" -#: intervention/forms/modalForms.py:386 +#: intervention/forms/modalForms.py:392 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:429 +#: intervention/forms/modalForms.py:435 msgid "" "Eco-account {} is not recorded yet. You can only deduct from recorded " "accounts." @@ -1419,7 +1421,7 @@ msgstr "" "Ökokonto {} ist noch nicht verzeichnet. Abbuchungen können nur von " "verzeichneten Ökokonten erfolgen." -#: intervention/forms/modalForms.py:439 +#: intervention/forms/modalForms.py:445 msgid "" "The account {} has not enough surface for a deduction of {} m². There are " "only {} m² left" @@ -4107,6 +4109,9 @@ msgstr "" msgid "Unable to connect to qpid with SASL mechanism %s" msgstr "" +#~ 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"