forked from CGM_Public/pretix_original
* 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:
0
src/pretix/api/__init__.py
Normal file
0
src/pretix/api/__init__.py
Normal file
0
src/pretix/api/auth/__init__.py
Normal file
0
src/pretix/api/auth/__init__.py
Normal file
43
src/pretix/api/auth/permission.py
Normal file
43
src/pretix/api/auth/permission.py
Normal 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
|
||||
21
src/pretix/api/auth/token.py
Normal file
21
src/pretix/api/auth/token.py
Normal 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
|
||||
0
src/pretix/api/serializers/__init__.py
Normal file
0
src/pretix/api/serializers/__init__.py
Normal file
10
src/pretix/api/serializers/event.py
Normal file
10
src/pretix/api/serializers/event.py
Normal 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')
|
||||
31
src/pretix/api/serializers/i18n.py
Normal file
31
src/pretix/api/serializers/i18n.py
Normal 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
|
||||
64
src/pretix/api/serializers/item.py
Normal file
64
src/pretix/api/serializers/item.py
Normal 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')
|
||||
110
src/pretix/api/serializers/order.py
Normal file
110
src/pretix/api/serializers/order.py
Normal 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')
|
||||
8
src/pretix/api/serializers/organizer.py
Normal file
8
src/pretix/api/serializers/organizer.py
Normal 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')
|
||||
10
src/pretix/api/serializers/voucher.py
Normal file
10
src/pretix/api/serializers/voucher.py
Normal 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')
|
||||
9
src/pretix/api/serializers/waitinglist.py
Normal file
9
src/pretix/api/serializers/waitinglist.py
Normal 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')
|
||||
0
src/pretix/api/templates/__init__.py
Normal file
0
src/pretix/api/templates/__init__.py
Normal file
19
src/pretix/api/templates/rest_framework/api.html
Normal file
19
src/pretix/api/templates/rest_framework/api.html
Normal 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
36
src/pretix/api/urls.py
Normal 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)),
|
||||
]
|
||||
0
src/pretix/api/views/__init__.py
Normal file
0
src/pretix/api/views/__init__.py
Normal file
14
src/pretix/api/views/event.py
Normal file
14
src/pretix/api/views/event.py
Normal 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()
|
||||
67
src/pretix/api/views/item.py
Normal file
67
src/pretix/api/views/item.py
Normal 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()
|
||||
181
src/pretix/api/views/order.py
Normal file
181
src/pretix/api/views/order.py
Normal 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
|
||||
20
src/pretix/api/views/organizer.py
Normal file
20
src/pretix/api/views/organizer.py
Normal 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)
|
||||
40
src/pretix/api/views/voucher.py
Normal file
40
src/pretix/api/views/voucher.py
Normal 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()
|
||||
31
src/pretix/api/views/waitinglist.py
Normal file
31
src/pretix/api/views/waitinglist.py
Normal 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()
|
||||
@@ -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
|
||||
|
||||
28
src/pretix/base/migrations/0062_auto_20170602_0948.py
Normal file
28
src/pretix/base/migrations/0062_auto_20170602_0948.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -232,7 +232,7 @@
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert {{ message.tags }}">
|
||||
{{ message }}
|
||||
{{ message|linebreaksbr }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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"),
|
||||
|
||||
2
src/pretix/static/rest_framework/scss/_variables.scss
Normal file
2
src/pretix/static/rest_framework/scss/_variables.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
$font-family-sans-serif: "Open Sans", "OpenSans", "Helvetica Neue", Helvetica, Arial, sans-serif !default;
|
||||
$brand-primary: #8E44B3 !default;
|
||||
10
src/pretix/static/rest_framework/scss/main.scss
Normal file
10
src/pretix/static/rest_framework/scss/main.scss
Normal 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;
|
||||
}
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
0
src/tests/api/__init__.py
Normal file
0
src/tests/api/__init__.py
Normal file
47
src/tests/api/conftest.py
Normal file
47
src/tests/api/conftest.py
Normal 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
|
||||
53
src/tests/api/test_auth.py
Normal file
53
src/tests/api/test_auth.py
Normal 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
|
||||
32
src/tests/api/test_events.py
Normal file
32
src/tests/api/test_events.py
Normal 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
268
src/tests/api/test_items.py
Normal 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
|
||||
321
src/tests/api/test_orders.py
Normal file
321
src/tests/api/test_orders.py
Normal 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
|
||||
20
src/tests/api/test_organizers.py
Normal file
20
src/tests/api/test_organizers.py
Normal 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
|
||||
109
src/tests/api/test_permissions.py
Normal file
109
src/tests/api/test_permissions.py
Normal 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)
|
||||
201
src/tests/api/test_vouchers.py
Normal file
201
src/tests/api/test_vouchers.py
Normal 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
|
||||
103
src/tests/api/test_waitinglist.py
Normal file
103
src/tests/api/test_waitinglist.py
Normal 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
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user