Add an API for teams (#1562)

* Add Team resource to API

* Add team memer endpoints

* Add team invites endpoint

* Add token endpoints
This commit is contained in:
Raphael Michel
2020-01-25 15:22:50 +01:00
committed by GitHub
parent 57453a5b00
commit 5e61342ff5
8 changed files with 1257 additions and 7 deletions

View File

@@ -1,14 +1,19 @@
from decimal import Decimal
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import get_language, ugettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import CompatibleJSONField
from pretix.base.models import GiftCard, Organizer, SeatingPlan
from pretix.base.auth import get_auth_backends
from pretix.base.models import (
GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
)
from pretix.base.models.seating import SeatingPlanLayoutValidator
from pretix.base.services.mail import SendMailException, mail
from pretix.helpers.urls import build_absolute_uri
class OrganizerSerializer(I18nAwareModelSerializer):
@@ -36,16 +41,129 @@ class GiftCardSerializer(I18nAwareModelSerializer):
qs = GiftCard.objects.filter(
secret=s
).filter(
Q(issuer=self.context["organizer"]) | Q(issuer__gift_card_collector_acceptance__collector=self.context["organizer"])
Q(issuer=self.context["organizer"]) | Q(
issuer__gift_card_collector_acceptance__collector=self.context["organizer"])
)
if self.instance:
qs = qs.exclude(pk=self.instance.pk)
if qs.exists():
raise ValidationError(
{'secret': _('A gift card with the same secret already exists in your or an affiliated organizer account.')}
{'secret': _(
'A gift card with the same secret already exists in your or an affiliated organizer account.')}
)
return data
class Meta:
model = GiftCard
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode')
class EventSlugField(serializers.SlugRelatedField):
def get_queryset(self):
return self.context['organizer'].events.all()
class TeamSerializer(serializers.ModelSerializer):
limit_events = EventSlugField(slug_field='slug', many=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class Meta:
model = Team
fields = (
'id', 'name', 'all_events', 'limit_events', 'can_create_events', 'can_change_teams',
'can_change_organizer_settings', 'can_manage_gift_cards', 'can_change_event_settings',
'can_change_items', 'can_view_orders', 'can_change_orders', 'can_view_vouchers',
'can_change_vouchers'
)
def validate(self, data):
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
if full_data.get('limit_events') and full_data.get('all_events'):
raise ValidationError('Do not set both limit_events and all_events.')
return data
class TeamInviteSerializer(serializers.ModelSerializer):
class Meta:
model = TeamInvite
fields = (
'id', 'email'
)
def _send_invite(self, instance):
try:
mail(
instance.email,
_('pretix account invitation'),
'pretixcontrol/email/invitation.txt',
{
'user': self,
'organizer': self.context['organizer'].name,
'team': instance.team.name,
'url': build_absolute_uri('control:auth.invite', kwargs={
'token': instance.token
})
},
event=None,
locale=get_language() # TODO: expose?
)
except SendMailException:
pass # Already logged
def create(self, validated_data):
if 'email' in validated_data:
try:
user = User.objects.get(email__iexact=validated_data['email'])
except User.DoesNotExist:
if self.context['team'].invites.filter(email__iexact=validated_data['email']).exists():
raise ValidationError(_('This user already has been invited for this team.'))
if 'native' not in get_auth_backends():
raise ValidationError('Users need to have a pretix account before they can be invited.')
invite = self.context['team'].invites.create(email=validated_data['email'])
self._send_invite(invite)
invite.team.log_action(
'pretix.team.invite.created',
data={
'email': validated_data['email']
},
**self.context['log_kwargs']
)
return invite
else:
if self.context['team'].members.filter(pk=user.pk).exists():
raise ValidationError(_('This user already has permissions for this team.'))
self.context['team'].members.add(user)
self.context['team'].log_action(
'pretix.team.member.added',
data={
'email': user.email,
'user': user.pk,
},
**self.context['log_kwargs']
)
return TeamInvite(email=user.email)
else:
raise ValidationError('No email address given.')
class TeamAPITokenSerializer(serializers.ModelSerializer):
active = serializers.BooleanField(default=True, read_only=True)
class Meta:
model = TeamAPIToken
fields = (
'id', 'name', 'active'
)
class TeamMemberSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (
'id', 'email', 'fullname', 'require_2fa'
)

View File

@@ -20,6 +20,12 @@ orga_router.register(r'subevents', event.SubEventViewSet)
orga_router.register(r'webhooks', webhooks.WebHookViewSet)
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
orga_router.register(r'giftcards', organizer.GiftCardViewSet)
orga_router.register(r'teams', organizer.TeamViewSet)
team_router = routers.DefaultRouter()
team_router.register(r'members', organizer.TeamMemberViewSet)
team_router.register(r'invites', organizer.TeamInviteViewSet)
team_router.register(r'tokens', organizer.TeamAPITokenViewSet)
event_router = routers.DefaultRouter()
event_router.register(r'subevents', event.SubEventViewSet)
@@ -62,6 +68,7 @@ urlpatterns = [
url(r'^', include(router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/', include(orga_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/teams/(?P<team>[^/]+)/', include(team_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/items/(?P<item>[^/]+)/', include(item_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/questions/(?P<question>[^/]+)/',
include(question_router.urls)),

View File

@@ -1,16 +1,23 @@
from decimal import Decimal
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
from rest_framework import filters, serializers, status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import MethodNotAllowed, PermissionDenied
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
from rest_framework.response import Response
from pretix.api.models import OAuthAccessToken
from pretix.api.serializers.organizer import (
GiftCardSerializer, OrganizerSerializer, SeatingPlanSerializer,
TeamAPITokenSerializer, TeamInviteSerializer, TeamMemberSerializer,
TeamSerializer,
)
from pretix.base.models import (
GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
)
from pretix.base.models import GiftCard, Organizer, SeatingPlan
from pretix.helpers.dicts import merge_dicts
@@ -55,6 +62,7 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
ctx['organizer'] = self.request.organizer
return ctx
@transaction.atomic()
def perform_create(self, serializer):
inst = serializer.save(organizer=self.request.organizer)
self.request.organizer.log_action(
@@ -64,6 +72,7 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
data=merge_dicts(self.request.data, {'id': inst.pk})
)
@transaction.atomic()
def perform_update(self, serializer):
if serializer.instance.events.exists() or serializer.instance.subevents.exists():
raise PermissionDenied('This plan can not be changed while it is in use for an event.')
@@ -76,6 +85,7 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
)
return inst
@transaction.atomic()
def perform_destroy(self, instance):
if instance.events.exists() or instance.subevents.exists():
raise PermissionDenied('This plan can not be deleted while it is in use for an event.')
@@ -153,3 +163,169 @@ class GiftCardViewSet(viewsets.ModelViewSet):
def perform_destroy(self, instance):
raise MethodNotAllowed("Gift cards cannot be deleted.")
class TeamViewSet(viewsets.ModelViewSet):
serializer_class = TeamSerializer
queryset = Team.objects.none()
permission = 'can_change_teams'
write_permission = 'can_change_teams'
def get_queryset(self):
return self.request.organizer.teams.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
return ctx
@transaction.atomic()
def perform_create(self, serializer):
inst = serializer.save(organizer=self.request.organizer)
inst.log_action(
'pretix.team.created',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': inst.pk})
)
@transaction.atomic()
def perform_update(self, serializer):
inst = serializer.save()
inst.log_action(
'pretix.team.changed',
user=self.request.user,
auth=self.request.auth,
data=self.request.data
)
return inst
def perform_destroy(self, instance):
instance.log_action('pretix.team.deleted', user=self.request.user, auth=self.request.auth)
instance.delete()
class TeamMemberViewSet(DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = TeamMemberSerializer
queryset = User.objects.none()
permission = 'can_change_teams'
write_permission = 'can_change_teams'
@cached_property
def team(self):
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
def get_queryset(self):
return self.team.members.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
return ctx
@transaction.atomic()
def perform_destroy(self, instance):
self.team.members.remove(instance)
self.team.log_action(
'pretix.team.member.removed', user=self.request.user, auth=self.request.auth, data={
'email': instance.email,
'user': instance.pk
}
)
class TeamInviteViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = TeamInviteSerializer
queryset = TeamInvite.objects.none()
permission = 'can_change_teams'
write_permission = 'can_change_teams'
@cached_property
def team(self):
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
def get_queryset(self):
return self.team.invites.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
ctx['team'] = self.team
ctx['log_kwargs'] = {
'user': self.request.user,
'auth': self.request.auth,
}
return ctx
@transaction.atomic()
def perform_destroy(self, instance):
self.team.log_action(
'pretix.team.invite.deleted', user=self.request.user, auth=self.request.auth, data={
'email': instance.email,
}
)
instance.delete()
@transaction.atomic()
def perform_create(self, serializer):
serializer.save(team=self.team)
class TeamAPITokenViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = TeamAPITokenSerializer
queryset = TeamAPIToken.objects.none()
permission = 'can_change_teams'
write_permission = 'can_change_teams'
@cached_property
def team(self):
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
def get_queryset(self):
return self.team.tokens.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
ctx['team'] = self.team
ctx['log_kwargs'] = {
'user': self.request.user,
'auth': self.request.auth,
}
return ctx
@transaction.atomic()
def perform_destroy(self, instance):
instance.active = False
instance.save()
self.team.log_action(
'pretix.team.token.deleted', user=self.request.user, auth=self.request.auth, data={
'name': instance.name,
}
)
@transaction.atomic()
def perform_create(self, serializer):
instance = serializer.save(team=self.team)
self.team.log_action(
'pretix.team.token.created', auth=self.request.auth, user=self.request.user, data={
'name': instance.name,
'id': instance.pk
}
)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
d = serializer.data
d['token'] = serializer.instance.token
return Response(d, status=status.HTTP_201_CREATED, headers=headers)
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
self.perform_destroy(instance)
serializer = self.get_serializer_class()(instance)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_200_OK, headers=headers)