Refs #314 -- Read-only REST API (#513)

* initial commit

* API auth

* Hierarchical URLs

* Add session auth

* Strong hierarchy

* Add filters

* Add i18n fields, questions

* More viewsets and serializers

* Ticket download

* Add OrderPosition serializer

* View-level permissions

* More tests

* More tests

* Add basic API docs

* Add REST API to docs frontpage

* Tests for order endpoints

* Add invoice tests

* Voucher and waitinglist tests

* Doc draft

* order docs

* Docs on all viewsets

* Disable DRF docs, style sphinx, style browsable API

* Fix tests

* deprecated imports

* Test foo

* Attendee names

* Fix migration problems

* Remove browsable API, plugin integration

* Doc fixes
This commit is contained in:
Raphael Michel
2017-06-19 11:16:04 +02:00
committed by GitHub
parent 6df3a7d4b5
commit b2d4bea1d0
71 changed files with 4213 additions and 59 deletions

View File

View File

View File

@@ -0,0 +1,43 @@
from rest_framework.permissions import SAFE_METHODS, BasePermission
from pretix.base.models import Event
from pretix.base.models.organizer import Organizer, TeamAPIToken
class EventPermission(BasePermission):
model = TeamAPIToken
def has_permission(self, request, view):
if not request.user.is_authenticated and not isinstance(request.auth, TeamAPIToken):
if request.method in SAFE_METHODS and request.path.startswith('/api/v1/docs/'):
return True
return False
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken)
else request.user)
if 'event' in request.resolver_match.kwargs and 'organizer' in request.resolver_match.kwargs:
request.event = Event.objects.filter(
slug=request.resolver_match.kwargs['event'],
organizer__slug=request.resolver_match.kwargs['organizer'],
).select_related('organizer').first()
if not request.event or not perm_holder.has_event_permission(request.event.organizer, request.event):
return False
request.organizer = request.event.organizer
request.eventpermset = perm_holder.get_event_permission_set(request.organizer, request.event)
if hasattr(view, 'permission'):
if view.permission and view.permission not in request.eventpermset:
return False
elif 'organizer' in request.resolver_match.kwargs:
request.organizer = Organizer.objects.filter(
slug=request.resolver_match.kwargs['organizer'],
).first()
if not request.organizer or not perm_holder.has_organizer_permission(request.organizer):
return False
request.orgapermset = perm_holder.get_organizer_permission_set(request.organizer)
if hasattr(view, 'permission'):
if view.permission and view.permission not in request.orgapermset:
return False
return True

View File

@@ -0,0 +1,21 @@
from django.contrib.auth.models import AnonymousUser
from rest_framework import exceptions
from rest_framework.authentication import TokenAuthentication
from pretix.base.models.organizer import TeamAPIToken
class TeamTokenAuthentication(TokenAuthentication):
model = TeamAPIToken
def authenticate_credentials(self, key):
model = self.get_model()
try:
token = model.objects.select_related('team', 'team__organizer').get(token=key)
except model.DoesNotExist:
raise exceptions.AuthenticationFailed('Invalid token.')
if not token.active:
raise exceptions.AuthenticationFailed('Token inactive or deleted.')
return AnonymousUser(), token

View File

View File

@@ -0,0 +1,10 @@
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import Event
class EventSerializer(I18nAwareModelSerializer):
class Meta:
model = Event
fields = ('name', 'slug', 'live', 'currency', 'date_from',
'date_to', 'date_admission', 'is_public', 'presale_start',
'presale_end', 'location')

View File

@@ -0,0 +1,31 @@
from django.conf import settings
from i18nfield.fields import I18nCharField, I18nTextField
from rest_framework.fields import Field
from rest_framework.serializers import ModelSerializer
class I18nField(Field):
def __init__(self, **kwargs):
self.allow_blank = kwargs.pop('allow_blank', False)
self.trim_whitespace = kwargs.pop('trim_whitespace', True)
self.max_length = kwargs.pop('max_length', None)
self.min_length = kwargs.pop('min_length', None)
super().__init__(**kwargs)
def to_representation(self, value):
if value is None or value.data is None:
return None
if isinstance(value.data, dict):
return value.data
else:
return {
settings.LANGUAGE_CODE: str(value.data)
}
class I18nAwareModelSerializer(ModelSerializer):
pass
I18nAwareModelSerializer.serializer_field_mapping[I18nCharField] = I18nField
I18nAwareModelSerializer.serializer_field_mapping[I18nTextField] = I18nField

View File

@@ -0,0 +1,64 @@
from rest_framework import serializers
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import (
Item, ItemAddOn, ItemCategory, ItemVariation, Question, QuestionOption,
Quota,
)
class InlineItemVariationSerializer(I18nAwareModelSerializer):
class Meta:
model = ItemVariation
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price')
class InlineItemAddOnSerializer(serializers.ModelSerializer):
class Meta:
model = ItemAddOn
fields = ('addon_category', 'min_count', 'max_count',
'position')
class ItemSerializer(I18nAwareModelSerializer):
addons = InlineItemAddOnSerializer(many=True)
variations = InlineItemVariationSerializer(many=True)
class Meta:
model = Item
fields = ('id', 'category', 'name', 'active', 'description',
'default_price', 'free_price', 'tax_rate', 'admission',
'position', 'picture', 'available_from', 'available_until',
'require_voucher', 'hide_without_voucher', 'allow_cancel',
'min_per_order', 'max_per_order', 'has_variations',
'variations', 'addons')
class ItemCategorySerializer(I18nAwareModelSerializer):
class Meta:
model = ItemCategory
fields = ('id', 'name', 'description', 'position', 'is_addon')
class InlineQuestionOptionSerializer(I18nAwareModelSerializer):
class Meta:
model = QuestionOption
fields = ('id', 'answer')
class QuestionSerializer(I18nAwareModelSerializer):
options = InlineQuestionOptionSerializer(many=True)
class Meta:
model = Question
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position')
class QuotaSerializer(I18nAwareModelSerializer):
class Meta:
model = Quota
fields = ('id', 'name', 'size', 'items', 'variations')

View File

@@ -0,0 +1,110 @@
from rest_framework import serializers
from rest_framework.reverse import reverse
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import (
Checkin, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition,
)
from pretix.base.signals import register_ticket_outputs
class InvoiceAdddressSerializer(I18nAwareModelSerializer):
class Meta:
model = InvoiceAddress
fields = ('last_modified', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id')
class CheckinSerializer(I18nAwareModelSerializer):
class Meta:
model = Checkin
fields = ('datetime',)
class OrderDownloadsField(serializers.Field):
def to_representation(self, instance: Order):
if instance.status != Order.STATUS_PAID:
return []
request = self.context['request']
res = []
responses = register_ticket_outputs.send(instance.event)
for receiver, response in responses:
provider = response(instance.event)
if provider.is_enabled:
res.append({
'output': provider.identifier,
'url': reverse('api-v1:order-download', kwargs={
'organizer': instance.event.organizer.slug,
'event': instance.event.slug,
'code': instance.code,
'output': provider.identifier,
}, request=request)
})
return res
class PositionDownloadsField(serializers.Field):
def to_representation(self, instance: OrderPosition):
if instance.order.status != Order.STATUS_PAID:
return []
if instance.addon_to_id and not instance.order.event.settings.ticket_download_addons:
return []
if not instance.item.admission and not instance.order.event.settings.ticket_download_nonadm:
return []
request = self.context['request']
res = []
responses = register_ticket_outputs.send(instance.order.event)
for receiver, response in responses:
provider = response(instance.order.event)
if provider.is_enabled:
res.append({
'output': provider.identifier,
'url': reverse('api-v1:orderposition-download', kwargs={
'organizer': instance.order.event.organizer.slug,
'event': instance.order.event.slug,
'pk': instance.pk,
'output': provider.identifier,
}, request=request)
})
return res
class OrderPositionSerializer(I18nAwareModelSerializer):
checkins = CheckinSerializer(many=True)
downloads = PositionDownloadsField(source='*')
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
class Meta:
model = OrderPosition
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'checkins', 'downloads')
class OrderSerializer(I18nAwareModelSerializer):
invoice_address = InvoiceAdddressSerializer()
positions = OrderPositionSerializer(many=True)
downloads = OrderDownloadsField(source='*')
class Meta:
model = Order
fields = ('code', 'status', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'payment_fee', 'payment_fee_tax_rate', 'payment_fee_tax_value',
'total', 'comment', 'invoice_address', 'positions', 'downloads')
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
class Meta:
model = InvoiceLine
fields = ('description', 'gross_value', 'tax_value', 'tax_rate')
class InvoiceSerializer(I18nAwareModelSerializer):
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
refers = serializers.SlugRelatedField(slug_field='invoice_no', read_only=True)
lines = InlineInvoiceLineSerializer(many=True)
class Meta:
model = Invoice
fields = ('order', 'invoice_no', 'is_cancellation', 'invoice_from', 'invoice_to', 'date', 'refers', 'locale',
'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines')

View File

@@ -0,0 +1,8 @@
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import Organizer
class OrganizerSerializer(I18nAwareModelSerializer):
class Meta:
model = Organizer
fields = ('name', 'slug')

View File

@@ -0,0 +1,10 @@
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import Voucher
class VoucherSerializer(I18nAwareModelSerializer):
class Meta:
model = Voucher
fields = ('id', 'code', 'max_usages', 'redeemed', 'valid_until', 'block_quota',
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
'tag', 'comment')

View File

@@ -0,0 +1,9 @@
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import WaitingListEntry
class WaitingListSerializer(I18nAwareModelSerializer):
class Meta:
model = WaitingListEntry
fields = ('id', 'created', 'email', 'voucher', 'item', 'variation', 'locale')

View File

View File

@@ -0,0 +1,19 @@
{% extends "rest_framework/base.html" %}
{% load staticfiles %}
{% load compress %}
{% block bootstrap_theme %}
{% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static "rest_framework/scss/main.scss" %}" />
{% endcompress %}
{% endblock %}
{% block branding %}
<a class="navbar-brand" href="/api/v1/">pretix REST API</a>
{% endblock %}
{% block description %}
<div class="alert alert-info alert-docs-link">
<a href="https://docs.pretix.eu/en/latest/api/index.html">
You can find documentation on our REST API on docs.pretix.eu.
</a>
</div>
{% endblock %}

36
src/pretix/api/urls.py Normal file
View File

@@ -0,0 +1,36 @@
import importlib
from django.apps import apps
from django.conf.urls import include, url
from rest_framework import routers
from .views import event, item, order, organizer, voucher, waitinglist
router = routers.DefaultRouter()
router.register(r'organizers', organizer.OrganizerViewSet)
orga_router = routers.DefaultRouter()
orga_router.register(r'events', event.EventViewSet)
event_router = routers.DefaultRouter()
event_router.register(r'items', item.ItemViewSet)
event_router.register(r'categories', item.ItemCategoryViewSet)
event_router.register(r'questions', item.QuestionViewSet)
event_router.register(r'quotas', item.QuotaViewSet)
event_router.register(r'vouchers', voucher.VoucherViewSet)
event_router.register(r'orders', order.OrderViewSet)
event_router.register(r'orderpositions', order.OrderPositionViewSet)
event_router.register(r'invoices', order.InvoiceViewSet)
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
# Force import of all plugins to give them a chance to register URLs with the router
for app in apps.get_app_configs():
if hasattr(app, 'PretixPluginMeta'):
if importlib.util.find_spec(app.name + '.urls'):
importlib.import_module(app.name + '.urls')
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)),
]

View File

View File

@@ -0,0 +1,14 @@
from rest_framework import viewsets
from pretix.api.serializers.event import EventSerializer
from pretix.base.models import Event
class EventViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = EventSerializer
queryset = Event.objects.none()
lookup_field = 'slug'
lookup_url_kwarg = 'event'
def get_queryset(self):
return self.request.organizer.events.all()

View File

@@ -0,0 +1,67 @@
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import viewsets
from rest_framework.filters import OrderingFilter
from pretix.api.serializers.item import (
ItemCategorySerializer, ItemSerializer, QuestionSerializer,
QuotaSerializer,
)
from pretix.base.models import Item, ItemCategory, Question, Quota
class ItemFilter(FilterSet):
class Meta:
model = Item
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
class ItemViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = ItemSerializer
queryset = Item.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering_fields = ('id', 'position')
ordering = ('position', 'id')
filter_class = ItemFilter
def get_queryset(self):
return self.request.event.items.prefetch_related('variations', 'addons').all()
class ItemCategoryFilter(FilterSet):
class Meta:
model = ItemCategory
fields = ['is_addon']
class ItemCategoryViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = ItemCategorySerializer
queryset = ItemCategory.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
filter_class = ItemCategoryFilter
ordering_fields = ('id', 'position')
ordering = ('position', 'id')
def get_queryset(self):
return self.request.event.categories.all()
class QuestionViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = QuestionSerializer
queryset = Question.objects.none()
filter_backends = (OrderingFilter,)
ordering_fields = ('id', 'position')
ordering = ('position', 'id')
def get_queryset(self):
return self.request.event.questions.prefetch_related('options').all()
class QuotaViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = QuotaSerializer
queryset = Quota.objects.none()
filter_backends = (OrderingFilter,)
ordering_fields = ('id', 'size')
ordering = ('id',)
def get_queryset(self):
return self.request.event.quotas.all()

View File

@@ -0,0 +1,181 @@
import django_filters
from django.db.models import Q
from django.http import FileResponse
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import viewsets
from rest_framework.decorators import detail_route
from rest_framework.exceptions import APIException, NotFound, PermissionDenied
from rest_framework.filters import OrderingFilter
from pretix.api.serializers.order import (
InvoiceSerializer, OrderPositionSerializer, OrderSerializer,
)
from pretix.base.models import Invoice, Order, OrderPosition
from pretix.base.services.invoices import invoice_pdf
from pretix.base.services.tickets import (
get_cachedticket_for_order, get_cachedticket_for_position,
)
from pretix.base.signals import register_ticket_outputs
class OrderFilter(FilterSet):
class Meta:
model = Order
fields = ['code', 'status', 'email', 'locale']
class OrderViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = OrderSerializer
queryset = Order.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering = ('datetime',)
ordering_fields = ('datetime', 'code', 'status')
filter_class = OrderFilter
lookup_field = 'code'
permission = 'can_view_orders'
def get_queryset(self):
return self.request.event.orders.prefetch_related(
'positions', 'positions__checkins', 'positions__item',
).select_related(
'invoice_address'
)
def _get_output_provider(self, identifier):
responses = register_ticket_outputs.send(self.request.event)
for receiver, response in responses:
prov = response(self.request.event)
if prov.identifier == identifier:
return prov
raise NotFound('Unknown output provider.')
@detail_route(url_name='download', url_path='download/(?P<output>[^/]+)')
def download(self, request, output, **kwargs):
provider = self._get_output_provider(output)
order = self.get_object()
if order.status != Order.STATUS_PAID:
raise PermissionDenied("Downloads are not available for unpaid orders.")
ct = get_cachedticket_for_order(order, provider.identifier)
if not ct.file:
raise RetryException()
else:
resp = FileResponse(ct.file.file, content_type=ct.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
self.request.event.slug.upper(), order.code,
provider.identifier, ct.extension
)
return resp
class OrderPositionFilter(FilterSet):
order = django_filters.CharFilter(name='order', lookup_expr='code')
has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs')
attendee_name = django_filters.CharFilter(method='attendee_name_qs')
def has_checkin_qs(self, queryset, name, value):
return queryset.filter(checkins__isnull=not value)
def attendee_name_qs(self, queryset, name, value):
return queryset.filter(Q(attendee_name=value) | Q(addon_to__attendee_name=value))
class Meta:
model = OrderPosition
fields = ['item', 'variation', 'attendee_name', 'secret', 'order', 'order__status', 'has_checkin',
'addon_to']
class OrderPositionViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = OrderPositionSerializer
queryset = OrderPosition.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering = ('order__datetime', 'positionid')
ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',)
filter_class = OrderPositionFilter
permission = 'can_view_orders'
def get_queryset(self):
return OrderPosition.objects.filter(order__event=self.request.event).prefetch_related(
'checkins',
).select_related(
'item', 'order', 'order__event', 'order__event__organizer'
)
def _get_output_provider(self, identifier):
responses = register_ticket_outputs.send(self.request.event)
for receiver, response in responses:
prov = response(self.request.event)
if prov.identifier == identifier:
return prov
raise NotFound('Unknown output provider.')
@detail_route(url_name='download', url_path='download/(?P<output>[^/]+)')
def download(self, request, output, **kwargs):
provider = self._get_output_provider(output)
pos = self.get_object()
if pos.order.status != Order.STATUS_PAID:
raise PermissionDenied("Downloads are not available for unpaid orders.")
if pos.addon_to_id and not request.event.settings.ticket_download_addons:
raise PermissionDenied("Downloads are not enabled for add-on products.")
if not pos.item.admission and not request.event.settings.ticket_download_nonadm:
raise PermissionDenied("Downloads are not enabled for non-admission products.")
ct = get_cachedticket_for_position(pos, provider.identifier)
if not ct.file:
raise RetryException()
else:
resp = FileResponse(ct.file.file, content_type=ct.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
self.request.event.slug.upper(), pos.order.code, pos.positionid,
provider.identifier, ct.extension
)
return resp
class InvoiceFilter(FilterSet):
refers = django_filters.CharFilter(name='refers', lookup_expr='invoice_no__iexact')
order = django_filters.CharFilter(name='order', lookup_expr='code__iexact')
class Meta:
model = Invoice
fields = ['order', 'invoice_no', 'is_cancellation', 'refers', 'locale']
class RetryException(APIException):
status_code = 409
default_detail = 'The requested resource is not ready, please retry later.'
default_code = 'retry_later'
class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = InvoiceSerializer
queryset = Invoice.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering = ('invoice_no',)
ordering_fields = ('invoice_no', 'date')
filter_class = InvoiceFilter
lookup_field = 'invoice_no'
lookup_url_kwarg = 'invoice_no'
permission = 'can_view_orders'
def get_queryset(self):
return self.request.event.invoices.prefetch_related('lines').select_related('order')
@detail_route()
def download(self, request, **kwargs):
invoice = self.get_object()
if not invoice.file:
invoice_pdf(invoice.pk)
invoice.refresh_from_db()
if not invoice.file:
raise RetryException()
resp = FileResponse(invoice.file.file, content_type='application/pdf')
resp['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(invoice.number)
return resp

View File

@@ -0,0 +1,20 @@
from rest_framework import viewsets
from pretix.api.serializers.organizer import OrganizerSerializer
from pretix.base.models import Organizer
class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = OrganizerSerializer
queryset = Organizer.objects.none()
lookup_field = 'slug'
lookup_url_kwarg = 'organizer'
def get_queryset(self):
if self.request.user.is_authenticated():
if self.request.user.is_superuser:
return Organizer.objects.all()
else:
return Organizer.objects.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
else:
return Organizer.objects.filter(pk=self.request.auth.team.organizer_id)

View File

@@ -0,0 +1,40 @@
from django.db.models import F, Q
from django.utils.timezone import now
from django_filters.rest_framework import (
BooleanFilter, DjangoFilterBackend, FilterSet,
)
from rest_framework import viewsets
from rest_framework.filters import OrderingFilter
from pretix.api.serializers.voucher import VoucherSerializer
from pretix.base.models import Voucher
class VoucherFilter(FilterSet):
active = BooleanFilter(method='filter_active')
class Meta:
model = Voucher
fields = ['code', 'max_usages', 'redeemed', 'block_quota', 'allow_ignore_quota',
'price_mode', 'value', 'item', 'variation', 'quota', 'tag']
def filter_active(self, queryset, name, value):
if value:
return queryset.filter(Q(redeemed__lt=F('max_usages')) &
(Q(valid_until__isnull=True) | Q(valid_until__gt=now())))
else:
return queryset.filter(Q(redeemed__gte=F('max_usages')) |
(Q(valid_until__isnull=False) & Q(valid_until__lte=now())))
class VoucherViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = VoucherSerializer
queryset = Voucher.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering = ('id',)
ordering_fields = ('id', 'code', 'max_usages', 'valid_until', 'value')
filter_class = VoucherFilter
permission = 'can_view_vouchers'
def get_queryset(self):
return self.request.event.vouchers.all()

View File

@@ -0,0 +1,31 @@
import django_filters
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import viewsets
from rest_framework.filters import OrderingFilter
from pretix.api.serializers.waitinglist import WaitingListSerializer
from pretix.base.models import WaitingListEntry
class WaitingListFilter(FilterSet):
has_voucher = django_filters.rest_framework.BooleanFilter(method='has_voucher_qs')
def has_voucher_qs(self, queryset, name, value):
return queryset.filter(voucher__isnull=not value)
class Meta:
model = WaitingListEntry
fields = ['item', 'variation', 'email', 'locale', 'has_voucher']
class WaitingListViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = WaitingListSerializer
queryset = WaitingListEntry.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering = ('created',)
ordering_fields = ('id', 'created', 'email', 'item')
filter_class = WaitingListFilter
permission = 'can_view_orders'
def get_queryset(self):
return self.request.event.waitinglistentries.all()

View File

@@ -161,6 +161,9 @@ def _merge_csp(a, b):
class SecurityMiddleware(MiddlewareMixin):
CSP_EXEMPT = (
'/api/v1/docs/',
)
def process_response(self, request, resp):
if settings.DEBUG and resp.status_code >= 400:
@@ -199,6 +202,7 @@ class SecurityMiddleware(MiddlewareMixin):
else:
staticdomain += " " + settings.SITE_URL
dynamicdomain += " " + settings.SITE_URL
if hasattr(request, 'organizer') and request.organizer:
domain = get_domain(request.organizer)
if domain:
@@ -207,5 +211,6 @@ class SecurityMiddleware(MiddlewareMixin):
domain = '%s:%d' % (domain, siteurlsplit.port)
dynamicdomain += " " + domain
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain)
if request.path not in self.CSP_EXEMPT:
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain)
return resp

View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-06-02 09:48
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.organizer
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0061_auto_20170521_0942'),
]
operations = [
migrations.CreateModel(
name='TeamAPIToken',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=190)),
('active', models.BooleanField(default=True)),
('token', models.CharField(default=pretix.base.models.organizer.generate_api_token, max_length=64)),
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tokens', to='pretixbase.Team')),
],
),
]

View File

@@ -1,8 +1,8 @@
import string
from datetime import date
from decimal import Decimal
from django.db import DatabaseError, models, transaction
from django.utils import timezone
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
@@ -15,6 +15,10 @@ def invoice_filename(instance, filename: str) -> str:
)
def today():
return timezone.now().date()
class Invoice(models.Model):
"""
Represents an invoice that is issued because of an order. Because invoices are legally required
@@ -56,7 +60,7 @@ class Invoice(models.Model):
refers = models.ForeignKey('Invoice', related_name='refered', null=True, blank=True)
invoice_from = models.TextField()
invoice_to = models.TextField()
date = models.DateField(default=date.today)
date = models.DateField(default=today)
locale = models.CharField(max_length=50, default='en')
introductory_text = models.TextField(blank=True)
additional_text = models.TextField(blank=True)

View File

@@ -72,6 +72,10 @@ def generate_invite_token():
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
def generate_api_token():
return get_random_string(length=64, allowed_chars=string.ascii_lowercase + string.digits)
class Team(LoggedModel):
"""
A team is a collection of people given certain access rights to one or more events of an organizer.
@@ -175,6 +179,10 @@ class Team(LoggedModel):
else:
return self.limit_events.filter(pk=event.pk).exists()
@property
def active_tokens(self):
return self.tokens.filter(active=True)
class Meta:
verbose_name = _("Team")
verbose_name_plural = _("Teams")
@@ -200,3 +208,81 @@ class TeamInvite(models.Model):
return _("Invite to team '{team}' for '{email}'").format(
team=str(self.team), email=self.email
)
class TeamAPIToken(models.Model):
"""
A TeamAPIToken represents an API token that has the same access level as the team it belongs to.
:param team: The team the person is invited to
:type team: Team
:param name: A human-readable name for the token
:type name: str
:param active: Whether or not this token is active
:type active: bool
:param token: The secret required to submit to the API
:type token: str
"""
team = models.ForeignKey(Team, related_name="tokens", on_delete=models.CASCADE)
name = models.CharField(max_length=190)
active = models.BooleanField(default=True)
token = models.CharField(default=generate_api_token, max_length=64)
def get_event_permission_set(self, organizer, event) -> set:
"""
Gets a set of permissions (as strings) that a token holds for a particular event
:param organizer: The organizer of the event
:param event: The event to check
:return: set of permissions
"""
has_event_access = (self.team.all_events and organizer == self.team.organizer) or (
event in self.team.limit_events.all()
)
return self.team.permission_set() if has_event_access else set()
def get_organizer_permission_set(self, organizer) -> set:
"""
Gets a set of permissions (as strings) that a token holds for a particular organizer
:param organizer: The organizer of the event
:return: set of permissions
"""
return self.team.permission_set() if self.team.organizer == organizer else set()
def has_event_permission(self, organizer, event, perm_name=None) -> bool:
"""
Checks if this token is part of a team that grants access of type ``perm_name``
to the event ``event``.
:param organizer: The organizer of the event
:param event: The event to check
:param perm_name: The permission, e.g. ``can_change_teams``
:return: bool
"""
has_event_access = (self.team.all_events and organizer == self.team.organizer) or (
event in self.team.limit_events.all()
)
return has_event_access and (not perm_name or self.team.has_permission(perm_name))
def has_organizer_permission(self, organizer, perm_name=None):
"""
Checks if this token is part of a team that grants access of type ``perm_name``
to the organizer ``organizer``.
:param organizer: The organizer to check
:param perm_name: The permission, e.g. ``can_change_teams``
:return: bool
"""
return organizer == self.team.organizer and (not perm_name or self.team.has_permission(perm_name))
def get_events_with_any_permission(self):
"""
Returns a queryset of events the token has any permissions to.
:return: Iterable of Events
"""
if self.team.all_events:
return self.team.organizer.events.all()
else:
return self.team.limit_events.all()

View File

@@ -1,14 +1,13 @@
import copy
import tempfile
from collections import defaultdict
from datetime import date
from decimal import Decimal
from django.contrib.staticfiles import finders
from django.core.files.base import ContentFile
from django.db import transaction
from django.utils import timezone
from django.utils.formats import date_format, localize
from django.utils.timezone import now
from django.utils.translation import pgettext, ugettext as _
from i18nfield.strings import LazyI18nString
from reportlab.lib import pagesizes
@@ -108,7 +107,7 @@ def generate_cancellation(invoice: Invoice):
cancellation.invoice_no = None
cancellation.refers = invoice
cancellation.is_cancellation = True
cancellation.date = date.today()
cancellation.date = timezone.now().date()
cancellation.payment_provider_text = ''
cancellation.save()
@@ -135,7 +134,7 @@ def generate_invoice(order: Order):
invoice = Invoice(
order=order,
event=order.event,
date=date.today(),
date=timezone.now().date(),
locale=locale
)
invoice = build_invoice(invoice)
@@ -430,11 +429,11 @@ def build_preview_invoice_pdf(event):
locale = event.settings.locale
with rolledback_transaction(), language(locale):
order = event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
expires=now(), code="PREVIEW", total=119)
order = event.orders.create(status=Order.STATUS_PENDING, datetime=timezone.now(),
expires=timezone.now(), code="PREVIEW", total=119)
invoice = Invoice(
order=order, event=event, invoice_no="PREVIEW",
date=date.today(), locale=locale
date=timezone.now().date(), locale=locale
)
invoice.invoice_from = event.settings.get('invoice_address_from')

View File

@@ -1,4 +1,5 @@
import os
from datetime import timedelta
from django.core.files.base import ContentFile
from django.utils.timezone import now
@@ -85,3 +86,43 @@ def preview(event: int, provider: str):
prov = response(event)
if prov.identifier == provider:
return prov.generate(p)
def get_cachedticket_for_position(pos, identifier):
try:
ct = CachedTicket.objects.filter(
order_position=pos, provider=identifier
).last()
except CachedTicket.DoesNotExist:
ct = None
if not ct:
ct = CachedTicket.objects.create(
order_position=pos, provider=identifier,
extension='', type='', file=None)
generate.apply_async(args=(pos.id, identifier))
if not ct.file:
if now() - ct.created > timedelta(minutes=5):
generate.apply_async(args=(pos.id, identifier))
return ct
def get_cachedticket_for_order(order, identifier):
try:
ct = CachedCombinedTicket.objects.filter(
order=order, provider=identifier
).last()
except CachedCombinedTicket.DoesNotExist:
ct = None
if not ct:
ct = CachedCombinedTicket.objects.create(
order=order, provider=identifier,
extension='', type='', file=None)
generate_order.apply_async(args=(order.id, identifier))
if not ct.file:
if now() - ct.created > timedelta(minutes=5):
generate_order.apply_async(args=(order.id, identifier))
return ct

View File

@@ -169,6 +169,12 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
if logentry.action_type == 'pretix.team.invite.deleted':
return _('The invite for {user} has been revoked.').format(user=data.get('email'))
if logentry.action_type == 'pretix.team.token.created':
return _('The token "{name}" has been created.').format(name=data.get('name'))
if logentry.action_type == 'pretix.team.token.deleted':
return _('The token "{name}" has been revoked.').format(name=data.get('name'))
if logentry.action_type == 'pretix.user.settings.changed':
text = str(_('Your account settings have been changed.'))
if 'email' in data:

View File

@@ -232,7 +232,7 @@
{% if messages %}
{% for message in messages %}
<div class="alert {{ message.tags }}">
{{ message }}
{{ message|linebreaksbr }}
</div>
{% endfor %}
{% endif %}

View File

@@ -10,6 +10,7 @@
{% trans "Edit" %}
</a>
</h2>
<h3>{% trans "Team members" %}</h3>
<form action="" method="post">
{% csrf_token %}
<!-- Trick browsers into taking this as a default -->
@@ -18,7 +19,7 @@
<thead>
<tr>
<th>{% trans "Member" %}</th>
<th></th>
<th width="150"></th>
</tr>
</thead>
<tbody>
@@ -70,6 +71,47 @@
</tfoot>
</table>
</form>
<h3>{% trans "API tokens" %}</h3>
<form action="" method="post">
{% csrf_token %}
<!-- Trick browsers into taking this as a default -->
<button type="submit" class="btn btn-primary btn-sm btn-block nearly-gone"></button>
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th width="150"></th>
</tr>
</thead>
<tbody>
{% for t in team.active_tokens %}
<tr>
<td>
{{ t.name }}
</td>
<td class="text-right">
<button type="submit" name="remove-token" value="{{ t.id }}"
class="btn btn-danger btn-sm btn-block">
<i class="fa fa-times"></i> {% trans "Remove" %}
</button>
</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td>
{% bootstrap_field add_token_form.name layout='inline' %}<br>
</td>
<td class="text-right">
<button type="submit" class="btn btn-primary btn-sm btn-block">
<i class="fa fa-plus"></i> {% trans "Add" %}
</button>
</td>
</tr>
</tfoot>
</table>
</form>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">

View File

@@ -13,6 +13,7 @@ from django.views.generic import (
)
from pretix.base.models import Organizer, Team, TeamInvite, User
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.services.mail import SendMailException, mail
from pretix.control.forms.organizer import (
OrganizerForm, OrganizerSettingsForm, OrganizerUpdateForm, TeamForm,
@@ -39,6 +40,10 @@ class InviteForm(forms.Form):
user = forms.EmailField(required=False, label=_('User'))
class TokenForm(forms.Form):
name = forms.CharField(required=False, label=_('Token name'))
class OrganizerDetailViewMixin:
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
@@ -309,11 +314,18 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
@cached_property
def add_form(self):
return InviteForm(data=self.request.POST if self.request.method == "POST" else None)
return InviteForm(data=(self.request.POST
if self.request.method == "POST" and "user" in self.request.POST else None))
@cached_property
def add_token_form(self):
return TokenForm(data=(self.request.POST
if self.request.method == "POST" and "name" in self.request.POST else None))
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['add_form'] = self.add_form
ctx['add_token_form'] = self.add_token_form
return ctx
def _send_invite(self, instance):
@@ -380,7 +392,24 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
messages.success(self.request, _('The invite has been revoked.'))
return redirect(self.get_success_url())
elif self.add_form.is_valid() and self.add_form.has_changed():
elif 'remove-token' in request.POST:
try:
token = self.object.tokens.get(pk=request.POST.get('remove-token'))
except TeamAPIToken.DoesNotExist:
messages.error(self.request, _('Invalid token selected.'))
return redirect(self.get_success_url())
else:
token.active = False
token.save()
self.object.log_action(
'pretix.team.token.deleted', user=self.request.user, data={
'name': token.name
}
)
messages.success(self.request, _('The token has been revoked.'))
return redirect(self.get_success_url())
elif "user" in self.request.POST and self.add_form.is_valid() and self.add_form.has_changed():
try:
user = User.objects.get(email=self.add_form.cleaned_data['user'])
@@ -414,6 +443,18 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
messages.success(self.request, _('The new member has been added to the team.'))
return redirect(self.get_success_url())
elif "name" in self.request.POST and self.add_token_form.is_valid() and self.add_token_form.has_changed():
token = self.object.tokens.create(name=self.add_token_form.cleaned_data['name'])
self.object.log_action(
'pretix.team.token.created', user=self.request.user, data={
'name': self.add_token_form.cleaned_data['name'],
'id': token.pk
}
)
messages.success(self.request, _('A new API token has been created with the following secret: {}\n'
'Please copy this secret to a safe place. You will not be able to '
'view it again here.').format(token.token))
return redirect(self.get_success_url())
else:
messages.error(self.request, _('Your changes could not be saved.'))
return self.get(request, *args, **kwargs)

View File

@@ -1,5 +1,3 @@
from datetime import timedelta
from django.contrib import messages
from django.db import transaction
from django.db.models import Sum
@@ -11,13 +9,15 @@ from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView, View
from pretix.base.models import CachedTicket, Invoice, Order, OrderPosition
from pretix.base.models.orders import CachedCombinedTicket, InvoiceAddress
from pretix.base.models.orders import InvoiceAddress
from pretix.base.payment import PaymentException
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
)
from pretix.base.services.orders import cancel_order
from pretix.base.services.tickets import generate, generate_order
from pretix.base.services.tickets import (
get_cachedticket_for_order, get_cachedticket_for_position,
)
from pretix.base.signals import (
register_payment_providers, register_ticket_outputs,
)
@@ -554,22 +554,7 @@ class OrderDownload(EventViewMixin, OrderDetailMixin, View):
return self._download_order()
def _download_order(self):
try:
ct = CachedCombinedTicket.objects.filter(
order=self.order, provider=self.output.identifier
).last()
except CachedCombinedTicket.DoesNotExist:
ct = None
if not ct:
ct = CachedCombinedTicket.objects.create(
order=self.order, provider=self.output.identifier,
extension='', type='', file=None)
generate_order.apply_async(args=(self.order.id, self.output.identifier))
if not ct.file:
if now() - ct.created > timedelta(minutes=5):
generate_order.apply_async(args=(self.order.id, self.output.identifier))
ct = get_cachedticket_for_order(self.order, self.output.identifier)
if 'ajax' in self.request.GET:
return JsonResponse({
@@ -587,22 +572,7 @@ class OrderDownload(EventViewMixin, OrderDetailMixin, View):
return resp
def _download_position(self):
try:
ct = CachedTicket.objects.filter(
order_position=self.order_position, provider=self.output.identifier
).last()
except CachedTicket.DoesNotExist:
ct = None
if not ct:
ct = CachedTicket.objects.create(
order_position=self.order_position, provider=self.output.identifier,
extension='', type='', file=None)
generate.apply_async(args=(self.order_position.id, self.output.identifier))
if not ct.file:
if now() - ct.created > timedelta(minutes=5):
generate.apply_async(args=(self.order_position.id, self.output.identifier))
ct = get_cachedticket_for_position(self.order_position, self.output.identifier)
if 'ajax' in self.request.GET:
return JsonResponse({

View File

@@ -189,6 +189,9 @@ INSTALLED_APPS = [
'pretix.control',
'pretix.presale',
'pretix.multidomain',
'pretix.api',
'rest_framework',
'django_filters',
'compressor',
'bootstrap3',
'djangoformsetjs',
@@ -233,6 +236,23 @@ if config.has_option('sentry', 'dsn'):
}
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'pretix.api.auth.permission.EventPermission',
],
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning',
'PAGE_SIZE': 50,
'DEFAULT_AUTHENTICATION_CLASSES': (
'pretix.api.auth.token.TeamTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
),
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
),
'UNICODE_JSON': False
}
CORE_MODULES = {
("pretix", "base"),
("pretix", "presale"),

View File

@@ -0,0 +1,2 @@
$font-family-sans-serif: "Open Sans", "OpenSans", "Helvetica Neue", Helvetica, Arial, sans-serif !default;
$brand-primary: #8E44B3 !default;

View File

@@ -0,0 +1,10 @@
@import "_variables.scss";
@import "../../pretixbase/scss/colors.scss";
@import "../../bootstrap/scss/_bootstrap.scss";
@import "../../pretixbase/scss/webfont.scss";
.alert-docs-link {
text-align: center;
font-weight: bold;
font-size: 20px;
}

View File

@@ -1,5 +1,6 @@
from django.conf import settings
from django.conf.urls import include, url
from django.views.generic import RedirectView
import pretix.control.urls
import pretix.presale.urls
@@ -15,6 +16,8 @@ base_patterns = [
url(r'^jsi18n/(?P<lang>[a-zA-Z-_]+)/$', js_catalog.js_catalog, name='javascript-catalog'),
url(r'^metrics$', metrics.serve_metrics,
name='metrics'),
url(r'^api/v1/', include('pretix.api.urls', namespace='api-v1')),
url(r'^api/$', RedirectView.as_view(url='/api/v1/'), name='redirect-api-version')
]
control_patterns = [

View File

@@ -1,11 +1,13 @@
# Functional requirements
Django>=1.11.*
djangorestframework==3.6.*
python-dateutil
pytz
django-bootstrap3==8.2.*
django-formset-js-improved==0.5.0.1
django-compressor==2.1.1
django-hierarkey==1.0.*
django-hierarkey==1.0.*,>=1.0.2
django-filter==1.0.*
reportlab==3.2.*
PyPDF2==1.26.*
easy-thumbnails==2.4.*
@@ -29,6 +31,9 @@ markdown
bleach==2.*
raven
django-i18nfield>=1.0.1
# API docs
coreapi==2.3.*
pygments
# Stripe
stripe==1.22.*
# PayPal

View File

47
src/tests/api/conftest.py Normal file
View File

@@ -0,0 +1,47 @@
from datetime import datetime
import pytest
from pytz import UTC
from rest_framework.test import APIClient
from pretix.base.models import Event, Organizer, Team, User
@pytest.fixture
def client():
return APIClient()
@pytest.fixture
def organizer():
return Organizer.objects.create(name='Dummy', slug='dummy')
@pytest.fixture
def event(organizer):
return Event.objects.create(
organizer=organizer, name='Dummy', slug='dummy',
date_from=datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC),
plugins='pretix.plugins.banktransfer,pretix.plugins.ticketoutputpdf'
)
@pytest.fixture
def team(organizer):
return Team.objects.create(organizer=organizer)
@pytest.fixture
def user():
return User.objects.create_user('dummy@dummy.dummy', 'dummy')
@pytest.fixture
def token_client(client, team):
team.can_view_orders = True
team.can_view_vouchers = True
team.all_events = True
team.save()
t = team.tokens.create(name='Foo')
client.credentials(HTTP_AUTHORIZATION='Token ' + t.token)
return client

View File

@@ -0,0 +1,53 @@
import pytest
from pretix.base.models import Organizer
@pytest.mark.django_db
def test_no_auth(client):
resp = client.get('/api/v1/organizers/')
assert resp.status_code == 401
@pytest.mark.django_db
def test_session_auth_no_teams(client, user):
client.login(email=user.email, password='dummy')
resp = client.get('/api/v1/organizers/')
assert resp.status_code == 200
assert len(resp.data['results']) == 0
@pytest.mark.django_db
def test_session_auth_with_teams(client, user, team):
team.members.add(user)
Organizer.objects.create(name='Other dummy', slug='dummy')
client.login(email=user.email, password='dummy')
resp = client.get('/api/v1/organizers/')
assert resp.status_code == 200
assert len(resp.data['results']) == 1
@pytest.mark.django_db
def test_token_invalid(client):
client.credentials(HTTP_AUTHORIZATION='Token ABCDE')
resp = client.get('/api/v1/organizers/')
assert resp.status_code == 401
@pytest.mark.django_db
def test_token_auth_valid(client, team):
Organizer.objects.create(name='Other dummy', slug='dummy')
t = team.tokens.create(name='Foo')
client.credentials(HTTP_AUTHORIZATION='Token ' + t.token)
resp = client.get('/api/v1/organizers/')
assert resp.status_code == 200
assert len(resp.data['results']) == 1
@pytest.mark.django_db
def test_token_auth_inactive(client, team):
Organizer.objects.create(name='Other dummy', slug='dummy')
t = team.tokens.create(name='Foo', active=False)
client.credentials(HTTP_AUTHORIZATION='Token ' + t.token)
resp = client.get('/api/v1/organizers/')
assert resp.status_code == 401

View File

@@ -0,0 +1,32 @@
import pytest
TEST_EVENT_RES = {
"name": {"en": "Dummy"},
"live": False,
"currency": "EUR",
"date_from": "2017-12-27T10:00:00Z",
"date_to": None,
"date_admission": None,
"is_public": False,
"presale_start": None,
"presale_end": None,
"location": None,
"slug": "dummy",
}
@pytest.mark.django_db
def test_event_list(token_client, organizer, event):
resp = token_client.get('/api/v1/organizers/{}/events/'.format(organizer.slug))
assert resp.status_code == 200
print(resp.data)
assert TEST_EVENT_RES == dict(resp.data['results'][0])
@pytest.mark.django_db
def test_event_detail(token_client, organizer, event, team):
team.all_events = True
team.save()
resp = token_client.get('/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert TEST_EVENT_RES == resp.data

268
src/tests/api/test_items.py Normal file
View File

@@ -0,0 +1,268 @@
from decimal import Decimal
import pytest
@pytest.fixture
def category(event):
return event.categories.create(name="Tickets")
TEST_CATEGORY_RES = {
"name": {"en": "Tickets"},
"description": {"en": ""},
"position": 0,
"is_addon": False
}
@pytest.mark.django_db
def test_category_list(token_client, organizer, event, team, category):
res = dict(TEST_CATEGORY_RES)
res["id"] = category.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/categories/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/categories/?is_addon=false'.format(
organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/categories/?is_addon=true'.format(
organizer.slug, event.slug))
assert resp.status_code == 200
assert [] == resp.data['results']
category.is_addon = True
category.save()
res["is_addon"] = True
resp = token_client.get('/api/v1/organizers/{}/events/{}/categories/?is_addon=true'.format(
organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
@pytest.mark.django_db
def test_category_detail(token_client, organizer, event, team, category):
res = dict(TEST_CATEGORY_RES)
res["id"] = category.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/categories/{}/'.format(organizer.slug, event.slug,
category.pk))
assert resp.status_code == 200
assert res == resp.data
@pytest.fixture
def item(event):
return event.items.create(name="Budget Ticket", default_price=23)
TEST_ITEM_RES = {
"name": {"en": "Budget Ticket"},
"default_price": "23.00",
"category": None,
"active": True,
"description": None,
"free_price": False,
"tax_rate": "0.00",
"admission": False,
"position": 0,
"picture": None,
"available_from": None,
"available_until": None,
"require_voucher": False,
"hide_without_voucher": False,
"allow_cancel": True,
"min_per_order": None,
"max_per_order": None,
"has_variations": False,
"variations": [],
"addons": []
}
@pytest.mark.django_db
def test_item_list(token_client, organizer, event, team, item):
res = dict(TEST_ITEM_RES)
res["id"] = item.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/?active=true'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/?active=false'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/?category=1'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/?admission=true'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/?admission=false'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
item.admission = True
item.save()
res['admission'] = True
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/?admission=true'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/?admission=false'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/?tax_rate=0'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/?tax_rate=19'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/?free_price=true'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [] == resp.data['results']
@pytest.mark.django_db
def test_item_detail(token_client, organizer, event, team, item):
res = dict(TEST_ITEM_RES)
res["id"] = item.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/'.format(organizer.slug, event.slug,
item.pk))
assert resp.status_code == 200
assert res == resp.data
@pytest.mark.django_db
def test_item_detail_variations(token_client, organizer, event, team, item):
var = item.variations.create(value="Children")
res = dict(TEST_ITEM_RES)
res["id"] = item.pk
res["variations"] = [{
"id": var.pk,
"value": {"en": "Children"},
"default_price": None,
"price": Decimal("23.00"),
"active": True,
"description": None,
"position": 0,
}]
res["has_variations"] = True
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/'.format(organizer.slug, event.slug,
item.pk))
assert resp.status_code == 200
assert res['variations'] == resp.data['variations']
@pytest.mark.django_db
def test_item_detail_addons(token_client, organizer, event, team, item, category):
item.addons.create(addon_category=category)
res = dict(TEST_ITEM_RES)
res["id"] = item.pk
res["addons"] = [{
"addon_category": category.pk,
"min_count": 0,
"max_count": 1,
"position": 0
}]
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/'.format(organizer.slug, event.slug,
item.pk))
assert resp.status_code == 200
assert res == resp.data
@pytest.fixture
def quota(event, item):
q = event.quotas.create(name="Budget Quota", size=200)
q.items.add(item)
return q
TEST_QUOTA_RES = {
"name": "Budget Quota",
"size": 200,
"items": [],
"variations": []
}
@pytest.mark.django_db
def test_quota_list(token_client, organizer, event, quota, item):
res = dict(TEST_QUOTA_RES)
res["id"] = quota.pk
res["items"] = [item.pk]
resp = token_client.get('/api/v1/organizers/{}/events/{}/quotas/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
@pytest.mark.django_db
def test_quota_detail(token_client, organizer, event, quota, item):
res = dict(TEST_QUOTA_RES)
res["id"] = quota.pk
res["items"] = [item.pk]
resp = token_client.get('/api/v1/organizers/{}/events/{}/quotas/{}/'.format(organizer.slug, event.slug,
quota.pk))
assert resp.status_code == 200
assert res == resp.data
@pytest.fixture
def question(event, item):
q = event.questions.create(question="T-Shirt size", type="C")
q.items.add(item)
q.options.create(answer="XL")
return q
TEST_QUESTION_RES = {
"question": {"en": "T-Shirt size"},
"type": "C",
"required": False,
"items": [],
"position": 0,
"options": [
{
"id": 0,
"answer": {"en": "XL"}
}
]
}
@pytest.mark.django_db
def test_question_list(token_client, organizer, event, question, item):
res = dict(TEST_QUESTION_RES)
res["id"] = question.pk
res["items"] = [item.pk]
res["options"][0]["id"] = question.options.first().pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/questions/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
@pytest.mark.django_db
def test_question_detail(token_client, organizer, event, question, item):
res = dict(TEST_QUESTION_RES)
res["id"] = question.pk
res["items"] = [item.pk]
res["options"][0]["id"] = question.options.first().pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/questions/{}/'.format(organizer.slug, event.slug,
question.pk))
assert resp.status_code == 200
assert res == resp.data

View File

@@ -0,0 +1,321 @@
import datetime
from decimal import Decimal
from unittest import mock
import pytest
from pytz import UTC
from pretix.base.models import InvoiceAddress, Order, OrderPosition
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice,
)
@pytest.fixture
def item(event):
return event.items.create(name="Budget Ticket", default_price=23)
@pytest.fixture
def order(event, item):
testtime = datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=UTC)
with mock.patch('django.utils.timezone.now') as mock_now:
mock_now.return_value = testtime
o = Order.objects.create(
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, secret="k24fiuwvu8kxz3y1",
datetime=datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=UTC),
expires=datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=UTC),
total=23, payment_provider='banktransfer', locale='en'
)
InvoiceAddress.objects.create(order=o, company="Sample company")
OrderPosition.objects.create(
order=o,
item=item,
variation=None,
price=Decimal("23"),
attendee_name="Peter",
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w"
)
return o
TEST_ORDERPOSITION_RES = {
"id": 1,
"order": "FOO",
"positionid": 1,
"item": 1,
"variation": None,
"price": "23.00",
"attendee_name": "Peter",
"attendee_email": None,
"voucher": None,
"tax_rate": "0.00",
"tax_value": "0.00",
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": None,
"checkins": [],
"downloads": []
}
TEST_ORDER_RES = {
"code": "FOO",
"status": "n",
"secret": "k24fiuwvu8kxz3y1",
"email": "dummy@dummy.test",
"locale": "en",
"datetime": "2017-12-01T10:00:00Z",
"expires": "2017-12-10T10:00:00Z",
"payment_date": None,
"payment_provider": "banktransfer",
"payment_fee": "0.00",
"payment_fee_tax_rate": "0.00",
"payment_fee_tax_value": "0.00",
"total": "23.00",
"comment": "",
"invoice_address": {
"last_modified": "2017-12-01T10:00:00Z",
"company": "Sample company",
"name": "",
"street": "",
"zipcode": "",
"city": "",
"country": "",
"vat_id": ""
},
"positions": [TEST_ORDERPOSITION_RES],
"downloads": []
}
@pytest.mark.django_db
def test_order_list(token_client, organizer, event, order, item):
res = dict(TEST_ORDER_RES)
res["positions"][0]["id"] = order.positions.first().pk
res["positions"][0]["item"] = item.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?code=FOO'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?code=BAR'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?status=n'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?status=p'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orders/?email=dummy@dummy.test'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orders/?email=foo@example.org'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?locale=en'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?locale=de'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
@pytest.mark.django_db
def test_order_detail(token_client, organizer, event, order, item):
res = dict(TEST_ORDER_RES)
res["positions"][0]["id"] = order.positions.first().pk
res["positions"][0]["item"] = item.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/{}/'.format(organizer.slug, event.slug,
order.code))
assert resp.status_code == 200
assert res == resp.data
order.status = 'p'
order.save()
event.settings.ticketoutput_pdf__enabled = True
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/{}/'.format(organizer.slug, event.slug,
order.code))
assert len(resp.data['downloads']) == 1
assert len(resp.data['positions'][0]['downloads']) == 1
@pytest.mark.django_db
def test_orderposition_list(token_client, organizer, event, order, item):
var = item.variations.create(value="Children")
res = dict(TEST_ORDERPOSITION_RES)
op = order.positions.first()
op.variation = var
op.save()
res["id"] = op.pk
res["item"] = item.pk
res["variation"] = var.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?order__status=n'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?order__status=p'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?item={}'.format(organizer.slug, event.slug, item.pk))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?item={}'.format(organizer.slug, event.slug, item.pk + 1))
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?variation={}'.format(organizer.slug, event.slug, var.pk))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?variation={}'.format(organizer.slug, event.slug, var.pk + 1))
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?attendee_name=Peter'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?attendee_name=Mark'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?secret=z3fsn8jyufm5kpk768q69gkbyr5f4h6w'.format(
organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?secret=abc123'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?order=FOO'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?order=BAR'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=false'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=true'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
order.positions.first().checkins.create(datetime=datetime.datetime(2017, 12, 26, 10, 0, 0, tzinfo=UTC))
res['checkins'] = [{'datetime': '2017-12-26T10:00:00Z'}]
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=true'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
@pytest.mark.django_db
def test_orderposition_detail(token_client, organizer, event, order, item):
res = dict(TEST_ORDERPOSITION_RES)
op = order.positions.first()
res["id"] = op.pk
res["item"] = item.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format(organizer.slug, event.slug,
op.pk))
assert resp.status_code == 200
assert res == resp.data
order.status = 'p'
order.save()
event.settings.ticketoutput_pdf__enabled = True
resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format(organizer.slug, event.slug,
op.pk))
assert len(resp.data['downloads']) == 1
@pytest.fixture
def invoice(order):
testtime = datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=UTC)
with mock.patch('django.utils.timezone.now') as mock_now:
mock_now.return_value = testtime
return generate_invoice(order)
TEST_INVOICE_RES = {
"order": "FOO",
"invoice_no": "00001",
"is_cancellation": False,
"invoice_from": "",
"invoice_to": "Sample company",
"date": "2017-12-10",
"refers": None,
"locale": "en",
"introductory_text": "",
"additional_text": "",
"payment_provider_text": "",
"footer_text": "",
"lines": [
{
"description": "Budget Ticket",
"gross_value": "23.00",
"tax_value": "0.00",
"tax_rate": "0.00"
}
]
}
@pytest.mark.django_db
def test_invoice_list(token_client, organizer, event, order, invoice):
res = dict(TEST_INVOICE_RES)
resp = token_client.get('/api/v1/organizers/{}/events/{}/invoices/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/invoices/?order=FOO'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/invoices/?order=BAR'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/invoices/?invoice_no={}'.format(
organizer.slug, event.slug, invoice.invoice_no))
assert [res] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/invoices/?invoice_no=XXX'.format(
organizer.slug, event.slug))
assert [] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/invoices/?locale=en'.format(
organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/invoices/?locale=de'.format(
organizer.slug, event.slug))
assert [] == resp.data['results']
ic = generate_cancellation(invoice)
resp = token_client.get('/api/v1/organizers/{}/events/{}/invoices/?is_cancellation=false'.format(
organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/invoices/?is_cancellation=true'.format(
organizer.slug, event.slug))
assert len(resp.data['results']) == 1
assert resp.data['results'][0]['invoice_no'] == ic.invoice_no
resp = token_client.get('/api/v1/organizers/{}/events/{}/invoices/?refers={}'.format(
organizer.slug, event.slug, invoice.invoice_no))
assert len(resp.data['results']) == 1
assert resp.data['results'][0]['invoice_no'] == ic.invoice_no
resp = token_client.get('/api/v1/organizers/{}/events/{}/invoices/?refers={}'.format(
organizer.slug, event.slug, ic.invoice_no))
assert [] == resp.data['results']
@pytest.mark.django_db
def test_invoice_detail(token_client, organizer, event, invoice):
res = dict(TEST_INVOICE_RES)
resp = token_client.get('/api/v1/organizers/{}/events/{}/invoices/{}/'.format(organizer.slug, event.slug,
invoice.invoice_no))
assert resp.status_code == 200
assert res == resp.data

View File

@@ -0,0 +1,20 @@
import pytest
TEST_ORGANIZER_RES = {
"name": "Dummy",
"slug": "dummy"
}
@pytest.mark.django_db
def test_organizer_list(token_client, organizer):
resp = token_client.get('/api/v1/organizers/')
assert resp.status_code == 200
assert TEST_ORGANIZER_RES in resp.data['results']
@pytest.mark.django_db
def test_organizer_detail(token_client, organizer):
resp = token_client.get('/api/v1/organizers/{}/'.format(organizer.slug))
assert resp.status_code == 200
assert TEST_ORGANIZER_RES == resp.data

View File

@@ -0,0 +1,109 @@
import pytest
from pretix.base.models import Organizer
event_urls = [
'categories/',
'invoices/',
'items/',
'orders/',
'orderpositions/',
'questions/',
'quotas/',
'vouchers/',
'waitinglistentries/',
]
event_permission_urls = [
('get', 'can_view_orders', 'orders/', 200),
('get', 'can_view_orders', 'orderpositions/', 200),
('get', 'can_view_vouchers', 'vouchers/', 200),
('get', 'can_view_orders', 'invoices/', 200),
('get', 'can_view_orders', 'waitinglistentries/', 200),
]
@pytest.fixture
def token_client(client, team):
team.can_view_orders = True
team.can_view_vouchers = True
team.save()
t = team.tokens.create(name='Foo')
client.credentials(HTTP_AUTHORIZATION='Token ' + t.token)
return client
@pytest.mark.django_db
def test_organizer_allowed(token_client, organizer):
resp = token_client.get('/api/v1/organizers/{}/events/'.format(organizer.slug))
assert resp.status_code == 200
@pytest.mark.django_db
def test_organizer_not_allowed(token_client, organizer):
o2 = Organizer.objects.create(slug='o2', name='Organizer 2')
resp = token_client.get('/api/v1/organizers/{}/events/'.format(o2.slug))
assert resp.status_code == 403
@pytest.mark.django_db
def test_organizer_not_existing(token_client, organizer):
resp = token_client.get('/api/v1/organizers/{}/events/'.format('o2'))
assert resp.status_code == 403
@pytest.mark.django_db
@pytest.mark.parametrize("url", event_urls)
def test_event_allowed_all_events(token_client, team, organizer, event, url):
team.all_events = True
team.save()
resp = token_client.get('/api/v1/organizers/{}/events/{}/{}'.format(organizer.slug, event.slug, url))
assert resp.status_code == 200
@pytest.mark.django_db
@pytest.mark.parametrize("url", event_urls)
def test_event_allowed_limit_events(token_client, organizer, team, event, url):
team.all_events = False
team.save()
team.limit_events.add(event)
resp = token_client.get('/api/v1/organizers/{}/events/{}/{}'.format(organizer.slug, event.slug, url))
assert resp.status_code == 200
@pytest.mark.django_db
@pytest.mark.parametrize("url", event_urls)
def test_event_not_allowed(token_client, organizer, team, event, url):
team.all_events = False
team.save()
resp = token_client.get('/api/v1/organizers/{}/events/{}/{}'.format(organizer.slug, event.slug, url))
assert resp.status_code == 403
@pytest.mark.django_db
@pytest.mark.parametrize("url", event_urls)
def test_event_not_existing(token_client, organizer, url, event):
resp = token_client.get('/api/v1/organizers/{}/events/{}/{}'.format(organizer.slug, event.slug, url))
assert resp.status_code == 403
@pytest.mark.django_db
@pytest.mark.parametrize("urlset", event_permission_urls)
def test_token_event_permission_allowed(token_client, team, organizer, event, urlset):
team.all_events = True
setattr(team, urlset[1], True)
team.save()
resp = getattr(token_client, urlset[0])('/api/v1/organizers/{}/events/{}/{}'.format(
organizer.slug, event.slug, urlset[2]))
assert resp.status_code == urlset[3]
@pytest.mark.django_db
@pytest.mark.parametrize("urlset", event_permission_urls)
def test_token_event_permission_not_allowed(token_client, team, organizer, event, urlset):
team.all_events = True
setattr(team, urlset[1], False)
team.save()
resp = getattr(token_client, urlset[0])('/api/v1/organizers/{}/events/{}/{}'.format(
organizer.slug, event.slug, urlset[2]))
assert resp.status_code in (404, 403)

View File

@@ -0,0 +1,201 @@
import datetime
import pytest
from django.utils import timezone
@pytest.fixture
def item(event):
return event.items.create(name="Budget Ticket", default_price=23)
@pytest.fixture
def voucher(event, item):
return event.vouchers.create(item=item, price_mode='set', value=12, tag='Foo')
@pytest.fixture
def quota(event, item):
q = event.quotas.create(name="Budget Quota", size=200)
q.items.add(item)
return q
TEST_VOUCHER_RES = {
'id': 1,
'code': '43K6LKM37FBVR2YG',
'max_usages': 1,
'redeemed': 0,
'valid_until': None,
'block_quota': False,
'allow_ignore_quota': False,
'price_mode': 'set',
'value': '12.00',
'item': 1,
'variation': None,
'quota': None,
'tag': 'Foo',
'comment': ''
}
@pytest.mark.django_db
def test_voucher_list(token_client, organizer, event, voucher, item, quota):
res = dict(TEST_VOUCHER_RES)
res['item'] = item.pk
res['id'] = voucher.pk
res['code'] = voucher.code
resp = token_client.get('/api/v1/organizers/{}/events/{}/vouchers/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?code={}'.format(organizer.slug, event.slug, voucher.code)
)
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?code=ABC'.format(organizer.slug, event.slug)
)
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?max_usages=1'.format(organizer.slug, event.slug)
)
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?max_usages=2'.format(organizer.slug, event.slug)
)
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?redeemed=0'.format(organizer.slug, event.slug)
)
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?redeemed=1'.format(organizer.slug, event.slug)
)
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?block_quota=false'.format(organizer.slug, event.slug)
)
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?block_quota=true'.format(organizer.slug, event.slug)
)
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?allow_ignore_quota=false'.format(organizer.slug, event.slug)
)
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?allow_ignore_quota=true'.format(organizer.slug, event.slug)
)
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?price_mode=set'.format(organizer.slug, event.slug)
)
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?price_mode=percent'.format(organizer.slug, event.slug)
)
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?value=12.00'.format(organizer.slug, event.slug)
)
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?value=10.00'.format(organizer.slug, event.slug)
)
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?item={}'.format(organizer.slug, event.slug, item.pk)
)
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?item={}'.format(organizer.slug, event.slug, item.pk + 1)
)
assert [] == resp.data['results']
var = item.variations.create(value='VIP')
voucher.variation = var
voucher.save()
res['variation'] = var.pk
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?variation={}'.format(organizer.slug, event.slug, var.pk)
)
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?variation={}'.format(organizer.slug, event.slug, var.pk + 1)
)
assert [] == resp.data['results']
voucher.variation = None
voucher.item = None
voucher.quota = quota
voucher.save()
res['variation'] = None
res['item'] = None
res['quota'] = quota.pk
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?quota={}'.format(organizer.slug, event.slug, quota.pk)
)
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?quota={}'.format(organizer.slug, event.slug, quota.pk + 1)
)
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?tag=Foo'.format(organizer.slug, event.slug)
)
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?tag=bar'.format(organizer.slug, event.slug)
)
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?active=true'.format(organizer.slug, event.slug)
)
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?active=false'.format(organizer.slug, event.slug)
)
assert [] == resp.data['results']
voucher.redeemed = 1
voucher.save()
res['redeemed'] = 1
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?active=false'.format(organizer.slug, event.slug)
)
assert [res] == resp.data['results']
voucher.redeemed = 0
voucher.valid_until = (timezone.now() - datetime.timedelta(days=1)).replace(microsecond=0)
voucher.save()
res['valid_until'] = voucher.valid_until.isoformat().replace('+00:00', 'Z')
res['redeemed'] = 0
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?active=false'.format(organizer.slug, event.slug)
)
assert [res] == resp.data['results']
@pytest.mark.django_db
def test_voucher_detail(token_client, organizer, event, voucher, item):
res = dict(TEST_VOUCHER_RES)
res['item'] = item.pk
res['id'] = voucher.pk
res['code'] = voucher.code
resp = token_client.get('/api/v1/organizers/{}/events/{}/vouchers/{}/'.format(organizer.slug, event.slug,
voucher.pk))
assert resp.status_code == 200
assert res == resp.data

View File

@@ -0,0 +1,103 @@
import datetime
from unittest import mock
import pytest
from pytz import UTC
from pretix.base.models import WaitingListEntry
@pytest.fixture
def item(event):
return event.items.create(name="Budget Ticket", default_price=23)
@pytest.fixture
def wle(event, item):
testtime = datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=UTC)
with mock.patch('django.utils.timezone.now') as mock_now:
mock_now.return_value = testtime
return WaitingListEntry.objects.create(event=event, item=item, email="waiting@example.org", locale="en")
TEST_WLE_RES = {
"id": 1,
"created": "2017-12-01T10:00:00Z",
"email": "waiting@example.org",
"voucher": None,
"item": 2,
"variation": None,
"locale": "en"
}
@pytest.mark.django_db
def test_wle_list(token_client, organizer, event, wle, item):
var = item.variations.create(value="Children")
res = dict(TEST_WLE_RES)
wle.variation = var
wle.save()
res["id"] = wle.pk
res["item"] = item.pk
res["variation"] = var.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/waitinglistentries/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/waitinglistentries/?item={}'.format(organizer.slug, event.slug, item.pk))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/waitinglistentries/?item={}'.format(organizer.slug, event.slug, item.pk + 1))
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/waitinglistentries/?variation={}'.format(organizer.slug, event.slug, var.pk))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/waitinglistentries/?variation={}'.format(organizer.slug, event.slug, var.pk + 1))
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/waitinglistentries/?email=waiting@example.org'.format(
organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/waitinglistentries/?email=foo@bar.sample'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/waitinglistentries/?locale=en'.format(
organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/waitinglistentries/?locale=de'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/waitinglistentries/?has_voucher=false'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/waitinglistentries/?has_voucher=true'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
v = event.vouchers.create(item=item, price_mode='set', value=12, tag='Foo')
wle.voucher = v
wle.save()
res['voucher'] = v.pk
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/waitinglistentries/?has_voucher=true'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
@pytest.mark.django_db
def test_wle_detail(token_client, organizer, event, wle, item):
res = dict(TEST_WLE_RES)
res["id"] = wle.pk
res["item"] = item.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/waitinglistentries/{}/'.format(organizer.slug, event.slug,
wle.pk))
assert resp.status_code == 200
assert res == resp.data

View File

@@ -76,6 +76,33 @@ def test_team_create_invite(event, admin_user, admin_team, client):
assert len(djmail.outbox) == 1
@pytest.mark.django_db
def test_team_create_token(event, admin_user, admin_team, client):
client.login(email='dummy@dummy.dummy', password='dummy')
djmail.outbox = []
resp = client.post('/control/organizer/dummy/team/{}/'.format(admin_team.pk), {
'name': 'Test token'
}, follow=True)
assert 'Test token' in resp.rendered_content
assert admin_team.tokens.first().name == 'Test token'
assert admin_team.tokens.first().token in resp.rendered_content
@pytest.mark.django_db
def test_team_remove_token(event, admin_user, admin_team, client):
client.login(email='dummy@dummy.dummy', password='dummy')
tk = admin_team.tokens.create(name='Test token')
resp = client.post('/control/organizer/dummy/team/{}/'.format(admin_team.pk), {
'remove-token': str(tk.pk)
}, follow=True)
assert tk.token not in resp.rendered_content
assert 'Test token' in resp.rendered_content
tk.refresh_from_db()
assert not tk.active
@pytest.mark.django_db
def test_team_revoke_invite(event, admin_user, admin_team, client):
client.login(email='dummy@dummy.dummy', password='dummy')