#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:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user