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 a20ac2da..cbd6f0b0 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 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"