Skip to content

Commit

Permalink
Merge pull request #631 from rdmorganiser/invite_api
Browse files Browse the repository at this point in the history
Add invite api (#540)
  • Loading branch information
jochenklar committed Jul 27, 2023
2 parents c682785 + bba261c commit 0b96c35
Show file tree
Hide file tree
Showing 15 changed files with 608 additions and 59 deletions.
4 changes: 4 additions & 0 deletions rdmo/accounts/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,5 +133,9 @@
('projects', 'integration', 'change_integration'),
('projects', 'integration', 'delete_integration'),
('projects', 'integration', 'view_integration'),
('projects', 'invite', 'add_invite'),
('projects', 'invite', 'change_invite'),
('projects', 'invite', 'delete_invite'),
('projects', 'invite', 'view_invite'),
))
)
28 changes: 28 additions & 0 deletions rdmo/projects/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,25 @@ def filter_user(self, user):
return self.none()


class InviteQuerySet(models.QuerySet):

def filter_current_site(self):
return self.filter(project__site=settings.SITE_ID)

def filter_user(self, user):
if user.is_authenticated:
if user.has_perm('projects.view_invite'):
return self.all()
elif is_site_manager(user):
return self.filter_current_site()
else:
from .models import Project
projects = Project.objects.filter(memberships__user=user, memberships__role='owner')
return self.filter(project__in=projects)
else:
return self.none()


class SnapshotQuerySet(models.QuerySet):

def filter_current_site(self):
Expand Down Expand Up @@ -161,6 +180,15 @@ def filter_user(self, user):
return self.get_queryset().filter_user(user)


class InviteManager(CurrentSiteManagerMixin, models.Manager):

def get_queryset(self):
return InviteQuerySet(self.model, using=self._db)

def filter_user(self, user):
return self.get_queryset().filter_user(user)


class SnapshotManager(CurrentSiteManagerMixin, models.Manager):

def get_queryset(self):
Expand Down
3 changes: 3 additions & 0 deletions rdmo/projects/models/invite.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
from django.utils.translation import gettext_lazy as _

from ..constants import ROLE_CHOICES
from ..managers import InviteManager


class Invite(models.Model):

key_salt = 'rdmo.projects.models.invite.Invite'

objects = InviteManager()

project = models.ForeignKey(
'Project', on_delete=models.CASCADE, related_name='invites',
verbose_name=_('Project'),
Expand Down
78 changes: 76 additions & 2 deletions rdmo/projects/serializers/v1/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers

from rdmo.services.validators import ProviderValidator

from ...models import (Integration, IntegrationOption, Issue, IssueResource,
Membership, Project, Snapshot, Value)
from ...models import (Integration, IntegrationOption, Invite, Issue,
IssueResource, Membership, Project, Snapshot, Value)
from ...validators import ValueValidator


Expand Down Expand Up @@ -127,6 +128,65 @@ def update(self, integration, validated_data):
return integration


class ProjectInviteSerializer(serializers.ModelSerializer):

timestamp = serializers.DateTimeField(read_only=True)

class Meta:
model = Invite
fields = (
'id',
'user',
'email',
'role',
'timestamp'
)

def validate_user(self, value):
if self.context['view'].project.memberships.filter(user=value).exists():
raise serializers.ValidationError(_('The user is already a member of the project.'))
return value

def validate_email(self, value):
if self.context['view'].project.memberships.filter(user__email=value).exists():
raise serializers.ValidationError(_('A user with that email is already a member of the project.'))
return value

def validate(self, data):
user = data.get('user')
email = data.get('email')

if not user and not email:
raise serializers.ValidationError(_('Either user or email needs to be provided'))
elif user and email:
raise serializers.ValidationError(_('User and email are mutually exclusive'))
elif user:
data['email'] = user.email
elif email:
usermodel = get_user_model()
try:
data['user'] = usermodel.objects.get(email=email)
except usermodel.DoesNotExist:
data['user'] = None

return data

def create(self, validated_data):
invite = super().create(validated_data)
invite.make_token()
invite.save()
return invite


class ProjectInviteUpdateSerializer(serializers.ModelSerializer):

class Meta:
model = Invite
fields = (
'role',
)


class ProjectIssueResourceSerializer(serializers.ModelSerializer):

integration = serializers.PrimaryKeyRelatedField(read_only=True)
Expand Down Expand Up @@ -217,6 +277,20 @@ class Meta:
)


class InviteSerializer(serializers.ModelSerializer):

class Meta:
model = Invite
fields = (
'id',
'project',
'user',
'email',
'role',
'timestamp'
)


class IssueResourceSerializer(serializers.ModelSerializer):

integration = serializers.PrimaryKeyRelatedField(read_only=True)
Expand Down
30 changes: 20 additions & 10 deletions rdmo/projects/tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@
from django.core.management import call_command
from django.core.management.base import CommandError

from rdmo.projects.models import Project

projects_without_owner = [6, 7, 8, 9, 11]

prune_assignments = [
('guest', "['owner', 'guest', 'author', 'manager']", [6]),
('author', "['owner', 'author', 'manager']", [6, 9]),
('manager', "['owner', 'manager']", [6, 8, 9]),
('owner', "['owner']", [6, 7, 8, 9])
('guest', "['owner', 'guest', 'author', 'manager']", [6, 11]),
('author', "['owner', 'author', 'manager']", [6, 9, 11]),
('manager', "['owner', 'manager']", [6, 8, 9, 11]),
('owner', "['owner']", projects_without_owner)
]

def get_prune_output(projects, remove=False):
project_output = ""
for proj in projects:
project_output += "Prune Test (id=%i)\n" % proj
project_output += '%s (id=%s)\n' % (proj.title, proj.id)
if remove:
project_output += "...removing...OK\n"
return project_output
Expand All @@ -32,31 +36,37 @@ def test_prune_projects_error(db, settings):
@pytest.mark.parametrize('min_role,role_list,projects', prune_assignments)
def test_prune_projects_output(db, settings, min_role, role_list, projects):
stdout, stderr = io.StringIO(), io.StringIO()


instances = Project.objects.filter(id__in=projects).all()
call_command('prune_projects', '--min_role', min_role, stdout=stdout, stderr=stderr)

assert stdout.getvalue() == \
"Found projects without %s:\n%s" % (role_list, get_prune_output(projects))
"Found projects without %s:\n%s" % (role_list, get_prune_output(instances))
assert not stderr.getvalue()


def test_prune_projects_output2(db, settings):
stdout, stderr = io.StringIO(), io.StringIO()

instances = Project.objects.filter(id__in=projects_without_owner)
call_command('prune_projects', stdout=stdout, stderr=stderr)

assert stdout.getvalue() == \
"Found projects without ['owner']:\n%s" % (get_prune_output([6, 7, 8, 9]))
"Found projects without ['owner']:\n%s" % (get_prune_output(instances))
assert not stderr.getvalue()


def test_prune_projects_remove(db, settings):
stdout, stderr = io.StringIO(), io.StringIO()

instances = list(Project.objects.filter(id__in=projects_without_owner).all()).copy()

call_command('prune_projects', '--remove', stdout=stdout, stderr=stderr)

assert stdout.getvalue() == \
"Found projects without ['owner']:\n%s" % (get_prune_output([6, 7, 8, 9], True))
std_output = stdout.getvalue()
prune_output = "Found projects without ['owner']:\n%s" % (get_prune_output(instances, True))

assert std_output == prune_output
assert not stderr.getvalue()

stdout, stderr = io.StringIO(), io.StringIO()
Expand Down
43 changes: 29 additions & 14 deletions rdmo/projects/tests/test_view_membership.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import pytest

from django.core import mail
from django.urls import reverse

from ..models import Invite, Membership
from rdmo.projects.models import Invite, Membership

users = (
('owner', 'owner'),
Expand All @@ -25,6 +26,8 @@

membership_roles = ('owner', 'manager', 'author', 'guest')

excluded_invite_ids = [1, 2]


@pytest.mark.parametrize('username,password', users)
@pytest.mark.parametrize('project_id', projects)
Expand All @@ -42,10 +45,13 @@ def test_membership_create_get(db, client, username, password, project_id):
assert response.status_code == 302


@pytest.mark.parametrize('PROJECT_SEND_INVITE', [False, True])
@pytest.mark.parametrize('username,password', users)
@pytest.mark.parametrize('project_id', projects)
@pytest.mark.parametrize('membership_role', membership_roles)
def test_membership_create_post(db, client, username, password, project_id, membership_role):
def test_membership_create_post(db, client, settings,
username, password, project_id, membership_role, PROJECT_SEND_INVITE):
settings.PROJECT_SEND_INVITE = PROJECT_SEND_INVITE
client.login(username=username, password=password)

url = reverse('membership_create', args=[project_id])
Expand All @@ -57,23 +63,28 @@ def test_membership_create_post(db, client, username, password, project_id, memb

if project_id in add_membership_permission_map.get(username, []):
assert response.status_code == 302
assert Invite.objects.get(project_id=project_id, user__username='user', email='user@example.com', role=membership_role)
assert not Membership.objects.filter(project_id=project_id, user__username='user', role=membership_role).exists()
assert len(mail.outbox) == 1
assert Invite.objects.get(project_id=project_id, user__username='user',
email='user@example.com', role=membership_role)
assert not Membership.objects.filter(project_id=project_id,
user__username='user', role=membership_role).exists()
assert len(mail.outbox) == int(PROJECT_SEND_INVITE)
else:
if password:
assert response.status_code == 403
else:
assert response.status_code == 302

assert not Invite.objects.exists()
assert not Invite.objects.exclude(id__in=excluded_invite_ids).exists()
assert len(mail.outbox) == 0


@pytest.mark.parametrize('PROJECT_SEND_INVITE', [False, True])
@pytest.mark.parametrize('username,password', users)
@pytest.mark.parametrize('project_id', projects)
@pytest.mark.parametrize('membership_role', membership_roles)
def test_membership_create_post_mail(db, client, username, password, project_id, membership_role):
def test_membership_create_post_mail(db, client, settings,
username, password, project_id, membership_role, PROJECT_SEND_INVITE):
settings.PROJECT_SEND_INVITE = PROJECT_SEND_INVITE
client.login(username=username, password=password)

url = reverse('membership_create', args=[project_id])
Expand All @@ -84,16 +95,20 @@ def test_membership_create_post_mail(db, client, username, password, project_id,
response = client.post(url, data)

if project_id in add_membership_permission_map.get(username, []):
assert response.status_code == 302
assert Invite.objects.get(project_id=project_id, user=None, email='someuser@example.com', role=membership_role)
assert len(mail.outbox) == 1
if not PROJECT_SEND_INVITE:
assert response.status_code == 200
assert not Invite.objects.filter(project_id=project_id, user=None, email='someuser@example.com', role=membership_role)
else:
assert response.status_code == 302
assert Invite.objects.get(project_id=project_id, user=None, email='someuser@example.com', role=membership_role)
assert len(mail.outbox) == int(PROJECT_SEND_INVITE)
else:
if password:
assert response.status_code == 403
else:
assert response.status_code == 302

assert not Invite.objects.exists()
assert not Invite.objects.exclude(id__in=excluded_invite_ids).exists()
assert len(mail.outbox) == 0


Expand All @@ -110,7 +125,7 @@ def test_membership_create_post_error(db, client, username, password, membership
}
response = client.post(url, data)

assert not Invite.objects.exists(), Invite.objects.all()
assert not Invite.objects.exclude(id__in=excluded_invite_ids).exists()
assert len(mail.outbox) == 0

if project_id in add_membership_permission_map.get(username, []):
Expand All @@ -135,7 +150,7 @@ def test_membership_create_post_mail_error(db, client, username, password, membe
}
response = client.post(url, data)

assert not Invite.objects.exists(), Invite.objects.all()
assert not Invite.objects.exclude(id__in=excluded_invite_ids).exists()
assert len(mail.outbox) == 0

if project_id in add_membership_permission_map.get(username, []):
Expand Down Expand Up @@ -165,7 +180,7 @@ def test_membership_create_post_silent(db, client, username, password, membershi
assert response.status_code == 302

if username == 'site':
assert not Invite.objects.exists()
assert not Invite.objects.exclude(id__in=excluded_invite_ids).exists()
assert Membership.objects.get(project_id=project_id, user__username='user', role=membership_role)
assert len(mail.outbox) == 0
else:
Expand Down
4 changes: 2 additions & 2 deletions rdmo/projects/tests/test_view_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
'manager': [1, 3, 5, 7],
'author': [1, 3, 5, 8],
'guest': [1, 3, 5, 9],
'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
}

change_project_permission_map = {
Expand Down
Loading

0 comments on commit 0b96c35

Please sign in to comment.