diff --git a/src/pretix/control/views/organizer.py b/src/pretix/control/views/organizer.py
index 94989dfc92..7730ba3b06 100644
--- a/src/pretix/control/views/organizer.py
+++ b/src/pretix/control/views/organizer.py
@@ -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)
diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py
index 74b85b9fcf..e422367a3c 100644
--- a/src/pretix/presale/views/order.py
+++ b/src/pretix/presale/views/order.py
@@ -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({
diff --git a/src/pretix/settings.py b/src/pretix/settings.py
index 8b39597460..b837c93f17 100644
--- a/src/pretix/settings.py
+++ b/src/pretix/settings.py
@@ -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"),
diff --git a/src/pretix/static/rest_framework/scss/_variables.scss b/src/pretix/static/rest_framework/scss/_variables.scss
new file mode 100644
index 0000000000..6d391c981d
--- /dev/null
+++ b/src/pretix/static/rest_framework/scss/_variables.scss
@@ -0,0 +1,2 @@
+$font-family-sans-serif: "Open Sans", "OpenSans", "Helvetica Neue", Helvetica, Arial, sans-serif !default;
+$brand-primary: #8E44B3 !default;
diff --git a/src/pretix/static/rest_framework/scss/main.scss b/src/pretix/static/rest_framework/scss/main.scss
new file mode 100644
index 0000000000..dff5584f08
--- /dev/null
+++ b/src/pretix/static/rest_framework/scss/main.scss
@@ -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;
+}
diff --git a/src/pretix/urls.py b/src/pretix/urls.py
index 607f942396..9988fd64aa 100644
--- a/src/pretix/urls.py
+++ b/src/pretix/urls.py
@@ -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[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 = [
diff --git a/src/requirements/production.txt b/src/requirements/production.txt
index a860d1ac12..69c7692bef 100644
--- a/src/requirements/production.txt
+++ b/src/requirements/production.txt
@@ -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
diff --git a/src/tests/api/__init__.py b/src/tests/api/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/tests/api/conftest.py b/src/tests/api/conftest.py
new file mode 100644
index 0000000000..02f90aa982
--- /dev/null
+++ b/src/tests/api/conftest.py
@@ -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
diff --git a/src/tests/api/test_auth.py b/src/tests/api/test_auth.py
new file mode 100644
index 0000000000..b6c6046461
--- /dev/null
+++ b/src/tests/api/test_auth.py
@@ -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
diff --git a/src/tests/api/test_events.py b/src/tests/api/test_events.py
new file mode 100644
index 0000000000..345d04f725
--- /dev/null
+++ b/src/tests/api/test_events.py
@@ -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
diff --git a/src/tests/api/test_items.py b/src/tests/api/test_items.py
new file mode 100644
index 0000000000..74826cd5f3
--- /dev/null
+++ b/src/tests/api/test_items.py
@@ -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
diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py
new file mode 100644
index 0000000000..4aaeea2e7b
--- /dev/null
+++ b/src/tests/api/test_orders.py
@@ -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
diff --git a/src/tests/api/test_organizers.py b/src/tests/api/test_organizers.py
new file mode 100644
index 0000000000..375e7272d9
--- /dev/null
+++ b/src/tests/api/test_organizers.py
@@ -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
diff --git a/src/tests/api/test_permissions.py b/src/tests/api/test_permissions.py
new file mode 100644
index 0000000000..cab6314551
--- /dev/null
+++ b/src/tests/api/test_permissions.py
@@ -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)
diff --git a/src/tests/api/test_vouchers.py b/src/tests/api/test_vouchers.py
new file mode 100644
index 0000000000..e2f89d92af
--- /dev/null
+++ b/src/tests/api/test_vouchers.py
@@ -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
diff --git a/src/tests/api/test_waitinglist.py b/src/tests/api/test_waitinglist.py
new file mode 100644
index 0000000000..effbefc73d
--- /dev/null
+++ b/src/tests/api/test_waitinglist.py
@@ -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
diff --git a/src/tests/control/test_teams.py b/src/tests/control/test_teams.py
index 520dddc4e7..a7a8b251cb 100644
--- a/src/tests/control/test_teams.py
+++ b/src/tests/control/test_teams.py
@@ -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')