#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
This commit is contained in:
mpeltriaux 2022-02-18 13:52:27 +01:00
parent e152dfd4d7
commit edcf7b3c78
16 changed files with 278 additions and 133 deletions

View File

@ -85,6 +85,26 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
except KeyError as e: except KeyError as e:
self.fail(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): def test_get_compensation(self):
""" Tests api GET """ Tests api GET

View File

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

View File

@ -184,3 +184,24 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
self.assertEqual(put_body["intervention"], str(self.deduction.intervention.id)) 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["eco_account"], str(self.deduction.account.id))
self.assertEqual(put_body["surface"], self.deduction.surface) self.assertEqual(put_body["surface"], self.deduction.surface)
def test_update_share_intervention(self):
self.intervention.share_with_user(self.superuser)
url = reverse("api:v1:intervention-share", args=(str(self.intervention.id),))
json_file_path = "api/tests/v1/update/intervention_share_update_put_body.json"
with open(json_file_path) as json_file:
put_body = json.load(fp=json_file)
put_body["users"] = [self.user.username]
put_body["teams"] = [self.team.name]
self.assertFalse(self.intervention.is_shared_with(self.user))
self.assertEqual(0, self.intervention.shared_teams.count())
response = self._run_update_request(url, put_body)
self.assertEqual(response.status_code, 200, msg=response.content)
self.intervention.refresh_from_db()
self.assertEqual(1, self.intervention.shared_teams.count())
self.assertEqual(2, self.intervention.shared_users.count())
self.assertEqual(self.team.name, self.intervention.shared_teams.first().name)
self.assertTrue(self.intervention.is_shared_with(self.user))

View File

@ -19,7 +19,7 @@ from ema.models import Ema
from intervention.models import Intervention from intervention.models import Intervention
from konova.utils.message_templates import DATA_UNSHARED from konova.utils.message_templates import DATA_UNSHARED
from konova.utils.user_checks import is_default_group_only from konova.utils.user_checks import is_default_group_only
from user.models import User from user.models import User, Team
class AbstractAPIView(View): class AbstractAPIView(View):
@ -198,13 +198,21 @@ class AbstractModelShareAPIView(AbstractAPIView):
""" """
try: try:
users = self._get_shared_users_of_object(id) users = self._get_shared_users_of_object(id)
teams = self._get_shared_teams_of_object(id)
except Exception as e: except Exception as e:
return self._return_error_response(e) return self._return_error_response(e)
data = { data = {
"users": [ "users": [
user.username for user in users user.username for user in users
] ],
"teams": [
{
"id": team.id,
"name": team.name,
}
for team in teams
],
} }
return JsonResponse(data) return JsonResponse(data)
@ -258,6 +266,22 @@ class AbstractModelShareAPIView(AbstractAPIView):
users = obj.shared_users users = obj.shared_users
return 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): def _process_put_body(self, body: bytes, id: str):
""" Reads the body data, performs validity checks and sets the new users """ 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) obj = self.model.objects.get(id=id)
self._check_user_has_shared_access(obj) self._check_user_has_shared_access(obj)
new_users = json.loads(body.decode("utf-8")) content = json.loads(body.decode("utf-8"))
new_users = new_users.get("users", []) new_users = content.get("users", [])
if len(new_users) == 0: if len(new_users) == 0:
raise ValueError("Shared user list must not be empty!") raise ValueError("Shared user list must not be empty!")
new_teams = content.get("teams", [])
# Eliminate duplicates # Eliminate duplicates
new_users = list(dict.fromkeys(new_users)) new_users = list(dict.fromkeys(new_users))
new_teams = list(dict.fromkeys(new_teams))
# Make sure each of these names exist as a user # Make sure each of these names exist as a user
new_users_objs = [] new_users_objs = []
for user in new_users: for user in new_users:
new_users_objs.append(User.objects.get(username=user)) 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): 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! # 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( new_users_to_be_added = User.objects.filter(
@ -292,7 +323,16 @@ class AbstractModelShareAPIView(AbstractAPIView):
id__in=obj.shared_users id__in=obj.shared_users
) )
new_users_objs = obj.shared_users.union(new_users_to_be_added) 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_user_list(new_users_objs)
obj.share_with_team_list(new_teams_objs)
return True return True

View File

@ -59,8 +59,9 @@ class CheckboxCompensationTableFilter(CheckboxTableFilter):
""" """
if not value: if not value:
return queryset.filter( 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: else:
return queryset 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: def filter_only_show_unrecorded(self, queryset, name, value) -> QuerySet:
""" Filters queryset depending on value of 'show_recorded' setting """ Filters queryset depending on value of 'show_recorded' setting

View File

@ -8,7 +8,7 @@ Created on: 16.11.21
import shutil import shutil
from django.contrib import messages 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 import models, transaction
from django.db.models import QuerySet, Sum from django.db.models import QuerySet, Sum
from django.http import HttpRequest from django.http import HttpRequest
@ -299,7 +299,7 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
# Compensations inherit their shared state from the interventions # Compensations inherit their shared state from the interventions
return self.intervention.is_shared_with(user) 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 """ Adds user to list of shared access users
Args: Args:
@ -308,10 +308,9 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
Returns: 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 """ Sets the list of shared access users
Args: Args:
@ -322,6 +321,28 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
""" """
self.intervention.users.set(user_list) 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 @property
def shared_users(self) -> QuerySet: def shared_users(self) -> QuerySet:
""" Shortcut for fetching the users which have shared access on this object """ 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() 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: def get_documents(self) -> QuerySet:
""" Getter for all documents of a compensation """ Getter for all documents of a compensation

View File

@ -233,7 +233,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.eco_account.set_recorded(self.superuser) self.eco_account.set_recorded(self.superuser)
self.intervention.share_with_user(self.superuser) self.intervention.share_with_user(self.superuser)
self.eco_account.refresh_from_db() 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( deduction = EcoAccountDeduction.objects.create(
intervention=self.intervention, intervention=self.intervention,

View File

@ -9,7 +9,7 @@ from dal import autocomplete
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from konova.utils.message_templates import DEDUCTION_ADDED, REVOCATION_ADDED, DEDUCTION_REMOVED, DEDUCTION_EDITED, \ 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 User, Team
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
from django.db import transaction 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=_("Add team to share with"),
label_suffix="", label_suffix="",
help_text=_("Multiple selection possible - You can only select teams which do not already have access."), 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=_("Add user to share with"),
label_suffix="", 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 users which do not already have access. Enter the full username."),
@ -63,21 +63,8 @@ class ShareModalForm(BaseModalForm):
"data-placeholder": _("Click for selection"), "data-placeholder": _("Click for selection"),
"data-minimum-input-length": 3, "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): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -91,6 +78,48 @@ class ShareModalForm(BaseModalForm):
self._init_fields() 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): def _init_fields(self):
""" Wraps initializing of fields """ Wraps initializing of fields
@ -105,39 +134,12 @@ class ShareModalForm(BaseModalForm):
self.share_link 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 = { form_data = {
"team_select": self.instance.teams.all() "teams": self.instance.teams.all(),
"users": self.instance.users.all(),
} }
self.load_initial_data(form_data) 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): def save(self):
self.instance.update_sharing_user(self) self.instance.update_sharing_user(self)

View File

@ -76,19 +76,14 @@ class ShareUserAutocomplete(Select2QuerySetView):
def get_queryset(self): def get_queryset(self):
if self.request.user.is_anonymous: if self.request.user.is_anonymous:
return User.objects.none() return User.objects.none()
exclude_user_ids = self.forwarded.get("users", []) qs = User.objects.all()
_exclude = {"id__in": exclude_user_ids}
qs = User.objects.all().exclude(
**_exclude
).order_by(
"username"
)
if self.q: if self.q:
# Due to privacy concerns only a full username match will return the proper user entry # Due to privacy concerns only a full username match will return the proper user entry
qs = qs.filter( qs = qs.filter(
Q(username=self.q) | Q(username=self.q) |
Q(email=self.q) Q(email=self.q)
).distinct() ).distinct()
qs = qs.order_by("username")
return qs return qs
@ -99,11 +94,13 @@ class ShareTeamAutocomplete(Select2QuerySetView):
def get_queryset(self): def get_queryset(self):
if self.request.user.is_anonymous: if self.request.user.is_anonymous:
return Team.objects.none() return Team.objects.none()
qs = Team.objects.all()
if self.q: if self.q:
# Due to privacy concerns only a full username match will return the proper user entry # Due to privacy concerns only a full username match will return the proper user entry
qs = Team.objects.filter( qs = qs.filter(
Q(name__icontains=self.q) name__icontains=self.q
).order_by( )
qs = qs.order_by(
"name" "name"
) )
return qs return qs

View File

@ -297,8 +297,9 @@ class ShareableTableFilterMixin(django_filters.FilterSet):
""" """
if not value: if not value:
return queryset.filter( 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: else:
return queryset return queryset

View File

@ -501,30 +501,37 @@ class ShareableObjectMixin(models.Model):
form_data = form.cleaned_data form_data = form.cleaned_data
# Fetch selected teams and find out which user IDs are in removed teams -> mails need to be sent # 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( removed_team_users = self.teams.all().exclude(
id__in=accessing_teams id__in=accessing_teams
).values_list("users__id", flat=True) ).values_list("users__id", flat=True)
accessing_team_users = User.objects.filter(
new_accessing_users = list(form_data["user_select"].values_list("id", flat=True)) id__in=accessing_teams.values_list("users", flat=True)
keep_accessing_users = form_data["users"]
accessing_users = keep_accessing_users + new_accessing_users
users = User.objects.filter(
id__in=accessing_users
) )
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( removed_users = self.users.all().exclude(
id__in=accessing_users id__in=accessing_users
).values_list("id", flat=True) ).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) removed_users = removed_users.union(removed_team_users)
# Send mails # Send mails
for user_id in removed_users: for user_id in removed_users:
celery_send_mail_shared_access_removed.delay(self.identifier, self.title, user_id) 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) celery_send_mail_shared_access_given.delay(self.identifier, self.title, user_id)
# Set new shared users # Set new shared users
self.share_with_user_list(users) self.share_with_user_list(accessing_users)
self.share_with_team_list(accessing_teams) self.share_with_team_list(accessing_teams)
@property @property
@ -536,6 +543,15 @@ class ShareableObjectMixin(models.Model):
""" """
return self.users.all() 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 @abstractmethod
def get_share_url(self): def get_share_url(self):
""" Returns the share url for the object """ Returns the share url for the object

View File

@ -71,6 +71,7 @@ class AutocompleteTestCase(BaseTestCase):
"codes-registration-office-autocomplete", "codes-registration-office-autocomplete",
"codes-conservation-office-autocomplete", "codes-conservation-office-autocomplete",
"share-user-autocomplete", "share-user-autocomplete",
"share-team-autocomplete",
] ]
for test in tests: for test in tests:
self.client.login(username=self.superuser.username, password=self.superuser_pw) self.client.login(username=self.superuser.username, password=self.superuser_pw)

View File

@ -9,7 +9,7 @@ import datetime
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID
from ema.models import Ema 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.auth.models import Group
from django.contrib.gis.geos import MultiPolygon, Polygon from django.contrib.gis.geos import MultiPolygon, Polygon
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
@ -65,6 +65,7 @@ class BaseTestCase(TestCase):
self.create_dummy_states() self.create_dummy_states()
self.create_dummy_action() self.create_dummy_action()
self.codes = self.create_dummy_codes() self.codes = self.create_dummy_codes()
self.team = self.create_dummy_team()
# Set the default group as only group for the user # Set the default group as only group for the user
default_group = self.groups.get(name=DEFAULT_GROUP) default_group = self.groups.get(name=DEFAULT_GROUP)
@ -251,6 +252,24 @@ class BaseTestCase(TestCase):
]) ])
return codes 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 @staticmethod
def create_dummy_geometry() -> MultiPolygon: def create_dummy_geometry() -> MultiPolygon:
""" Creates some geometry """ Creates some geometry

View File

@ -12,11 +12,13 @@ FORM_INVALID = _("There was an error on this form.")
PARAMS_INVALID = _("Invalid parameters") PARAMS_INVALID = _("Invalid parameters")
INTERVENTION_INVALID = _("There are errors in this intervention.") 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") 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 = _("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.") 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 # FILES
FILE_TYPE_UNSUPPORTED = _("Unsupported file type") FILE_TYPE_UNSUPPORTED = _("Unsupported file type")

Binary file not shown.

View File

@ -7,8 +7,8 @@
#: compensation/forms/modalForms.py:47 compensation/forms/modalForms.py:63 #: compensation/forms/modalForms.py:47 compensation/forms/modalForms.py:63
#: compensation/forms/modalForms.py:356 compensation/forms/modalForms.py:463 #: compensation/forms/modalForms.py:356 compensation/forms/modalForms.py:463
#: intervention/forms/forms.py:54 intervention/forms/forms.py:156 #: intervention/forms/forms.py:54 intervention/forms/forms.py:156
#: intervention/forms/forms.py:168 intervention/forms/modalForms.py:148 #: intervention/forms/forms.py:168 intervention/forms/modalForms.py:154
#: intervention/forms/modalForms.py:161 intervention/forms/modalForms.py:174 #: intervention/forms/modalForms.py:167 intervention/forms/modalForms.py:180
#: konova/filters/mixins.py:53 konova/filters/mixins.py:54 #: konova/filters/mixins.py:53 konova/filters/mixins.py:54
#: konova/filters/mixins.py:81 konova/filters/mixins.py:82 #: konova/filters/mixins.py:81 konova/filters/mixins.py:82
#: konova/filters/mixins.py:94 konova/filters/mixins.py:95 #: konova/filters/mixins.py:94 konova/filters/mixins.py:95
@ -26,7 +26,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -221,7 +221,7 @@ msgstr "Abbuchungen"
#: compensation/templates/compensation/detail/eco_account/includes/states-before.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-after.html:36
#: ema/templates/ema/detail/includes/states-before.html:36 #: ema/templates/ema/detail/includes/states-before.html:36
#: intervention/forms/modalForms.py:359 #: intervention/forms/modalForms.py:365
msgid "Surface" msgid "Surface"
msgstr "Fläche" msgstr "Fläche"
@ -284,8 +284,8 @@ msgid "Type"
msgstr "Typ" msgstr "Typ"
#: analysis/templates/analysis/reports/includes/old_data/amount.html:24 #: analysis/templates/analysis/reports/includes/old_data/amount.html:24
#: compensation/tables.py:89 intervention/forms/modalForms.py:370 #: compensation/tables.py:89 intervention/forms/modalForms.py:376
#: intervention/forms/modalForms.py:377 intervention/tables.py:88 #: intervention/forms/modalForms.py:383 intervention/tables.py:88
#: intervention/templates/intervention/detail/view.html:19 #: intervention/templates/intervention/detail/view.html:19
#: konova/templates/konova/includes/quickstart/interventions.html:4 #: konova/templates/konova/includes/quickstart/interventions.html:4
#: templates/navbars/navbar.html:22 #: templates/navbars/navbar.html:22
@ -295,7 +295,7 @@ msgstr "Eingriff"
#: analysis/templates/analysis/reports/includes/old_data/amount.html:34 #: analysis/templates/analysis/reports/includes/old_data/amount.html:34
#: compensation/tables.py:266 #: compensation/tables.py:266
#: compensation/templates/compensation/detail/eco_account/view.html:20 #: 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 #: konova/templates/konova/includes/quickstart/ecoaccounts.html:4
#: templates/navbars/navbar.html:34 #: templates/navbars/navbar.html:34
msgid "Eco-account" 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/actions.html:34
#: ema/templates/ema/detail/includes/deadlines.html:34 #: ema/templates/ema/detail/includes/deadlines.html:34
#: ema/templates/ema/detail/includes/documents.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/documents.html:34
#: intervention/templates/intervention/detail/includes/payments.html:34 #: intervention/templates/intervention/detail/includes/payments.html:34
#: intervention/templates/intervention/detail/includes/revocation.html:38 #: intervention/templates/intervention/detail/includes/revocation.html:38
@ -484,7 +484,7 @@ msgid "Due on which date"
msgstr "Zahlung wird an diesem Datum erwartet" msgstr "Zahlung wird an diesem Datum erwartet"
#: compensation/forms/modalForms.py:64 compensation/forms/modalForms.py:357 #: 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" msgid "Additional comment, maximum {} letters"
msgstr "Zusätzlicher Kommentar, maximal {} Zeichen" msgstr "Zusätzlicher Kommentar, maximal {} Zeichen"
@ -512,7 +512,7 @@ msgstr "Zusatzbezeichnung"
msgid "Select an additional biotope type" msgid "Select an additional biotope type"
msgstr "Zusatzbezeichnung wählen" 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²" msgid "in m²"
msgstr "" msgstr ""
@ -540,7 +540,7 @@ msgstr "Fristart wählen"
#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:31 #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:31
#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:31 #: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:31
#: ema/templates/ema/detail/includes/deadlines.html:31 #: ema/templates/ema/detail/includes/deadlines.html:31
#: intervention/forms/modalForms.py:147 #: intervention/forms/modalForms.py:153
msgid "Date" msgid "Date"
msgstr "Datum" msgstr "Datum"
@ -1000,14 +1000,14 @@ msgstr "Zuletzt bearbeitet"
#: compensation/templates/compensation/detail/compensation/view.html:100 #: compensation/templates/compensation/detail/compensation/view.html:100
#: compensation/templates/compensation/detail/eco_account/view.html:83 #: 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 #: intervention/templates/intervention/detail/view.html:116
msgid "Shared with" msgid "Shared with"
msgstr "Freigegeben für" msgstr "Freigegeben für"
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:15 #: compensation/templates/compensation/detail/eco_account/includes/controls.html:15
#: ema/templates/ema/detail/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 #: intervention/templates/intervention/detail/includes/controls.html:15
msgid "Share" msgid "Share"
msgstr "Freigabe" msgstr "Freigabe"
@ -1348,46 +1348,48 @@ msgstr ""
"Mehrfachauswahl möglich - Sie können nur Nutzer wählen, für die der Eintrag " "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." "noch nicht freigegeben wurde. Geben Sie den ganzen Nutzernamen an."
#: intervention/forms/modalForms.py:73 #: intervention/forms/modalForms.py:72
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
msgid "Share settings for {}" msgid "Share settings for {}"
msgstr "Freigabe Einstellungen für {}" 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" msgid "Date of revocation"
msgstr "Datum des Widerspruchs" msgstr "Datum des Widerspruchs"
#: intervention/forms/modalForms.py:160 #: intervention/forms/modalForms.py:166
#: intervention/templates/intervention/detail/includes/revocation.html:35 #: intervention/templates/intervention/detail/includes/revocation.html:35
msgid "Document" msgid "Document"
msgstr "Dokument" msgstr "Dokument"
#: intervention/forms/modalForms.py:163 #: intervention/forms/modalForms.py:169
msgid "Must be smaller than 15 Mb" msgid "Must be smaller than 15 Mb"
msgstr "Muss kleiner als 15 Mb sein" 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 #: intervention/templates/intervention/detail/includes/revocation.html:18
msgid "Add revocation" msgid "Add revocation"
msgstr "Widerspruch hinzufügen" msgstr "Widerspruch hinzufügen"
#: intervention/forms/modalForms.py:245 #: intervention/forms/modalForms.py:251
msgid "Checked intervention data" msgid "Checked intervention data"
msgstr "Eingriffsdaten geprüft" msgstr "Eingriffsdaten geprüft"
#: intervention/forms/modalForms.py:251 #: intervention/forms/modalForms.py:257
msgid "Checked compensations data and payments" msgid "Checked compensations data and payments"
msgstr "Kompensationen und Zahlungen geprüft" msgstr "Kompensationen und Zahlungen geprüft"
#: intervention/forms/modalForms.py:260 #: intervention/forms/modalForms.py:266
#: intervention/templates/intervention/detail/includes/controls.html:19 #: intervention/templates/intervention/detail/includes/controls.html:19
msgid "Run check" msgid "Run check"
msgstr "Prüfung vornehmen" msgstr "Prüfung vornehmen"
#: intervention/forms/modalForms.py:261 konova/forms.py:514 #: intervention/forms/modalForms.py:267 konova/forms.py:514
msgid "" msgid ""
"I, {} {}, confirm that all necessary control steps have been performed by " "I, {} {}, confirm that all necessary control steps have been performed by "
"myself." "myself."
@ -1395,23 +1397,23 @@ msgstr ""
"Ich, {} {}, bestätige, dass die notwendigen Kontrollschritte durchgeführt " "Ich, {} {}, bestätige, dass die notwendigen Kontrollschritte durchgeführt "
"wurden:" "wurden:"
#: intervention/forms/modalForms.py:345 #: intervention/forms/modalForms.py:351
msgid "Only recorded accounts can be selected for deductions" msgid "Only recorded accounts can be selected for deductions"
msgstr "Nur verzeichnete Ökokonten können für Abbuchungen verwendet werden." 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" msgid "Only shared interventions can be selected"
msgstr "Nur freigegebene Eingriffe können gewählt werden" msgstr "Nur freigegebene Eingriffe können gewählt werden"
#: intervention/forms/modalForms.py:385 #: intervention/forms/modalForms.py:391
msgid "New Deduction" msgid "New Deduction"
msgstr "Neue Abbuchung" 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" msgid "Enter the information for a new deduction from a chosen eco-account"
msgstr "Geben Sie die Informationen für eine neue Abbuchung ein." msgstr "Geben Sie die Informationen für eine neue Abbuchung ein."
#: intervention/forms/modalForms.py:429 #: intervention/forms/modalForms.py:435
msgid "" msgid ""
"Eco-account {} is not recorded yet. You can only deduct from recorded " "Eco-account {} is not recorded yet. You can only deduct from recorded "
"accounts." "accounts."
@ -1419,7 +1421,7 @@ msgstr ""
"Ökokonto {} ist noch nicht verzeichnet. Abbuchungen können nur von " "Ökokonto {} ist noch nicht verzeichnet. Abbuchungen können nur von "
"verzeichneten Ökokonten erfolgen." "verzeichneten Ökokonten erfolgen."
#: intervention/forms/modalForms.py:439 #: intervention/forms/modalForms.py:445
msgid "" msgid ""
"The account {} has not enough surface for a deduction of {} m². There are " "The account {} has not enough surface for a deduction of {} m². There are "
"only {} m² left" "only {} m² left"
@ -4107,6 +4109,9 @@ msgstr ""
msgid "Unable to connect to qpid with SASL mechanism %s" msgid "Unable to connect to qpid with SASL mechanism %s"
msgstr "" 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" #~ msgid "Select the action type"
#~ msgstr "Maßnahmentyp wählen" #~ msgstr "Maßnahmentyp wählen"