#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:
2022-02-18 13:52:27 +01:00
parent 6dac847d22
commit aa675aa046
16 changed files with 278 additions and 133 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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")