This commit is contained in:
Raphael Michel
2015-02-17 20:13:48 +01:00
125 changed files with 800 additions and 281 deletions

View File

@@ -0,0 +1,8 @@
from django.apps import AppConfig
class PretixBaseConfig(AppConfig):
name = 'pretix.base'
label = 'pretixbase'
default_app_config = 'pretix.base.PretixBaseConfig'

134
src/pretix/base/admin.py Normal file
View File

@@ -0,0 +1,134 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import ugettext as _
from django import forms
from pretix.base.models import (
User, Organizer, OrganizerPermission, Event, EventPermission,
Property, PropertyValue, Item, ItemVariation, ItemCategory
)
class PretixUserCreationForm(forms.ModelForm):
"""
A form that creates a user, with no privileges, from the given username and
password.
"""
error_messages = {
'password_mismatch': _("The two password fields didn't match."),
}
password1 = forms.CharField(label=_("Password"),
widget=forms.PasswordInput)
password2 = forms.CharField(label=_("Password confirmation"),
widget=forms.PasswordInput,
help_text=_("Enter the same password as above, for verification."))
class Meta:
model = User
fields = ("email", "username", "event")
def clean_password2(self):
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError(
self.error_messages['password_mismatch'],
code='password_mismatch',
)
return password2
def save(self, commit=True):
user = super(PretixUserCreationForm, self).save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
return user
class PretixUserAdmin(UserAdmin):
fieldsets = (
(None, {'fields': ('identifier', 'event', 'username', 'password')}),
(_('Personal info'), {'fields': ('familyname', 'givenname', 'email')}),
(_('Locale'), {'fields': ('locale', 'timezone')}),
(_('Permissions'), {'fields': ('is_active', 'is_staff',
'groups', 'user_permissions')}),
)
list_display = ('identifier', 'event', 'username', 'email', 'givenname', 'familyname', 'is_staff')
search_fields = ('identifier', 'username', 'givenname', 'familyname', 'email')
ordering = ('identifier',)
list_filter = ('is_staff', 'is_active', 'groups')
add_form = PretixUserCreationForm
class OrganizerPermissionInline(admin.TabularInline):
model = OrganizerPermission
extra = 2
class OrganizerAdmin(admin.ModelAdmin):
model = Organizer
inlines = [OrganizerPermissionInline]
list_display = ('name', 'slug')
search_fields = ('name', 'slug')
class EventPermissionInline(admin.TabularInline):
model = EventPermission
extra = 2
class EventAdmin(admin.ModelAdmin):
model = Event
inlines = [EventPermissionInline]
list_display = ('name', 'slug', 'organizer', 'date_from')
search_fields = ('name', 'slug')
list_filter = ('date_from', 'locale', 'currency')
class PropertyValueInline(admin.StackedInline):
model = PropertyValue
extra = 4
class PropertyAdmin(admin.ModelAdmin):
model = Property
inlines = [PropertyValueInline]
list_display = ('name', 'event')
search_fields = ('name', 'event')
class ItemCategoryAdmin(admin.ModelAdmin):
model = ItemCategory
list_display = ('name', 'event')
search_fields = ('name', 'event')
class ItemVariationInline(admin.TabularInline):
model = ItemVariation
extra = 4
class ItemAdmin(admin.ModelAdmin):
model = Item
inlines = [ItemVariationInline]
list_display = ('name', 'event', 'category')
search_fields = ('name', 'event', 'category', 'short_description')
admin.site.register(User, PretixUserAdmin)
admin.site.register(Organizer, OrganizerAdmin)
admin.site.register(Event, EventAdmin)
admin.site.register(Property, PropertyAdmin)
admin.site.register(Item, ItemAdmin)
admin.site.register(ItemCategory, ItemCategoryAdmin)

88
src/pretix/base/cache.py Normal file
View File

@@ -0,0 +1,88 @@
import time
import hashlib
from django.core.cache import caches
from pretix.base.models import Event
class EventRelatedCache:
"""
This object behaves exactly like the cache implementations by Django
but with one important difference: It stores all keys related to a
certain event, so you pass an event when creating this object and if
you store data in this cache, it is only stored for this event. The
main purpose of this is to be able to flush all cached data related
to this event at once.
The EventRelatedCache instance itself is stateless, all state is
stored in the cache backend, so you can instantiate this class as many
times as you want.
"""
def __init__(self, event: Event, cache: str='default'):
assert isinstance(event, Event)
self.cache = caches[cache]
self.event = event
self.prefixkey = 'event:%s' % self.event.pk
def _prefix_key(self, original_key: str) -> str:
# Race conditions can happen here, but should be very very rare.
# We could only handle this by going _really_ lowlevel using
# memcached's `add` keyword instead of `set`.
# See also:
# https://code.google.com/p/memcached/wiki/NewProgrammingTricks#Namespacing
prefix = self.cache.get(self.prefixkey)
if prefix is None:
prefix = int(time.time())
self.cache.set(self.prefixkey, prefix)
key = 'event:%s:%d:%s' % (self.event.pk, prefix, original_key)
if len(key) > 200: # Hash long keys, as memcached has a length limit
# TODO: Use a more efficient, non-cryptographic hash algorithm
key = hashlib.sha256(key.encode("UTF-8")).hexdigest()
return key
@staticmethod
def _strip_prefix(key: str) -> str:
return key.split(":", 3)[-1] if 'event:' in key else key
def clear(self):
try:
prefix = self.cache.incr(self.prefixkey, 1)
except ValueError:
prefix = int(time.time())
self.cache.set(self.prefixkey, prefix)
def set(self, key: str, value: str, timeout: int=3600):
return self.cache.set(self._prefix_key(key), value, timeout)
def get(self, key: str) -> str:
return self.cache.get(self._prefix_key(key))
def get_many(self, keys: "list[str]") -> "dict[str, str]":
values = self.cache.get_many([self._prefix_key(key) for key in keys])
newvalues = {}
for k, v in values.items():
newvalues[self._strip_prefix(k)] = v
return newvalues
def set_many(self, values: "dict[str, str]", timeout=3600):
newvalues = {}
for k, v in values.items():
newvalues[self._prefix_key(k)] = v
return self.cache.set_many(newvalues, timeout)
def delete(self, key: str): # NOQA
return self.cache.delete(self._prefix_key(key))
def delete_many(self, keys: "list[str]"): # NOQA
return self.cache.delete_many([self._prefix_key(key) for key in keys])
def incr(self, key: str, by: int=1): # NOQA
return self.cache.incr(self._prefix_key(key), by)
def decr(self, key: str, by: int=1): # NOQA
return self.cache.decr(self._prefix_key(key), by)
def close(self): # NOQA
pass

25
src/pretix/base/forms.py Normal file
View File

@@ -0,0 +1,25 @@
from django.forms.models import ModelFormMetaclass, BaseModelForm
from django.utils import six
from versions.models import Versionable
class VersionedBaseModelForm(BaseModelForm):
"""
This is a helperclass to construct VersionedModelForm
"""
def save(self, commit=True):
if self.instance.pk is not None and isinstance(self.instance, Versionable):
if self.has_changed():
self.instance = self.instance.clone()
return super().save(commit)
class VersionedModelForm(six.with_metaclass(ModelFormMetaclass, VersionedBaseModelForm)):
"""
This is a modified version of Django's ModelForm which differs from ModelForm in
only one way: It executes the .clone() method of an object before saving it back to
the database, if the model is a sub-class of versions.models.Versionable. You can
safely use this as a base class for all your model forms, it will work out correctly
with both versioned and non-versioned models.
"""
pass

View File

@@ -0,0 +1,107 @@
import pytz
from django.conf import settings
from django.middleware.locale import LocaleMiddleware as BaseLocaleMiddleware
from django.utils.translation.trans_real import (
get_supported_language_variant,
parse_accept_lang_header,
language_code_re,
check_for_language
)
from django.utils.translation import LANGUAGE_SESSION_KEY
from django.utils import translation, timezone
from collections import OrderedDict
from django.utils.cache import patch_vary_headers
_supported = None
class LocaleMiddleware(BaseLocaleMiddleware):
"""
This middleware sets the correct locale and timezone
for a request.
"""
def process_request(self, request):
language = get_language_from_request(request)
translation.activate(language)
request.LANGUAGE_CODE = translation.get_language()
tzname = None
if request.user.is_authenticated():
tzname = request.user.timezone
if hasattr(request, 'event'):
tzname = request.event.timezone
if tzname:
try:
timezone.activate(pytz.timezone(tzname))
except pytz.UnknownTimeZoneError:
pass
else:
timezone.deactivate()
def process_response(self, request, response):
language = translation.get_language()
patch_vary_headers(response, ('Accept-Language',))
if 'Content-Language' not in response:
response['Content-Language'] = language
return response
def get_language_from_request(request) -> str:
"""
Analyzes the request to find what language the user wants the system to
show. Only languages listed in settings.LANGUAGES are taken into account.
If the user requests a sublanguage where we have a main language, we send
out the main language.
"""
global _supported
if _supported is None:
_supported = OrderedDict(settings.LANGUAGES)
# Priority 1: User settings
if request.user.is_authenticated():
lang_code = request.user.locale
if lang_code in _supported and lang_code is not None and check_for_language(lang_code):
return lang_code
# Priority 2: Anonymous user settings (session, cookie)
if hasattr(request, 'session'):
lang_code = request.session.get(LANGUAGE_SESSION_KEY)
if lang_code in _supported and lang_code is not None and check_for_language(lang_code):
return lang_code
lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME)
try:
return get_supported_language_variant(lang_code)
except LookupError:
pass
# Priority 3: Event default
if hasattr(request, 'event'):
lang_code = request.event.locale
try:
return get_supported_language_variant(lang_code)
except LookupError:
pass
# Priority 4: Browser default
accept = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
for accept_lang, unused in parse_accept_lang_header(accept):
if accept_lang == '*':
break
if not language_code_re.search(accept_lang):
continue
try:
return get_supported_language_variant(accept_lang)
except LookupError:
continue
try:
return get_supported_language_variant(settings.LANGUAGE_CODE)
except LookupError:
return settings.LANGUAGE_CODE

View File

@@ -0,0 +1,456 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import versions.models
import django.core.validators
from django.conf import settings
import django.db.models.deletion
import django.utils.timezone
import pretix.base.models
class Migration(migrations.Migration):
dependencies = [
('auth', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.AutoField(serialize=False, auto_created=True, verbose_name='ID', primary_key=True)),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('identifier', models.CharField(unique=True, max_length=255)),
('username', models.CharField(max_length=120, blank=True, null=True, help_text='Letters, digits and @/./+/-/_ only.')),
('email', models.EmailField(null=True, max_length=75, blank=True, db_index=True, verbose_name='E-mail')),
('givenname', models.CharField(max_length=255, blank=True, null=True, verbose_name='Given name')),
('familyname', models.CharField(max_length=255, blank=True, null=True, verbose_name='Family name')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('is_staff', models.BooleanField(default=False, verbose_name='Is site admin')),
('date_joined', models.DateTimeField(verbose_name='Date joined', auto_now_add=True)),
('locale', models.CharField(max_length=50, choices=[('de', 'German'), ('en', 'English')], default='en', verbose_name='Language')),
('timezone', models.CharField(max_length=100, default='UTC', verbose_name='Timezone')),
],
options={
'verbose_name_plural': 'Users',
'verbose_name': 'User',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='CartPosition',
fields=[
('id', models.AutoField(serialize=False, auto_created=True, verbose_name='ID', primary_key=True)),
('session', models.CharField(max_length=255, blank=True, null=True, verbose_name='Session key')),
('total', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Price')),
('datetime', models.DateTimeField(verbose_name='Date')),
('expires', models.DateTimeField(verbose_name='Expiration date')),
],
options={
'verbose_name_plural': 'Cart positions',
'verbose_name': 'Cart position',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Event',
fields=[
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
('identity', models.CharField(max_length=36)),
('version_start_date', models.DateTimeField()),
('version_end_date', models.DateTimeField(blank=True, null=True, default=None)),
('version_birth_date', models.DateTimeField()),
('name', models.CharField(max_length=200, verbose_name='Name')),
('slug', models.CharField(max_length=50, db_index=True, validators=[django.core.validators.RegexValidator(regex='^[a-zA-Z0-9.-]+$', message='The slug may only contain letters, numbers, dots and dashes.')], verbose_name='Slug', help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.')),
('locale', models.CharField(max_length=10, choices=[('de', 'German'), ('en', 'English')], verbose_name='Default locale')),
('timezone', models.CharField(max_length=100, default='UTC', verbose_name='Default timezone')),
('currency', models.CharField(max_length=10, verbose_name='Default currency')),
('date_from', models.DateTimeField(verbose_name='Event start time')),
('date_to', models.DateTimeField(blank=True, null=True, verbose_name='Event end time')),
('show_date_to', models.BooleanField(default=True, help_text="If disabled, only event's start date will be displayed to the public.", verbose_name='Show event end date')),
('show_times', models.BooleanField(default=True, help_text="If disabled, the event's start and end date will be displayed without the time of day.", verbose_name='Show dates with time')),
('presale_end', models.DateTimeField(blank=True, null=True, help_text='No items will be sold after this date.', verbose_name='End of presale')),
('presale_start', models.DateTimeField(blank=True, null=True, help_text='No items will be sold before this date.', verbose_name='Start of presale')),
('payment_term_days', models.PositiveIntegerField(default=14, help_text='The number of days after placing an order the user has to pay to preserve his reservation.', verbose_name='Payment term in days')),
('payment_term_last', models.DateTimeField(blank=True, null=True, help_text='The last date any payments are accepted. This has precedence over the number of days configured above.', verbose_name='Last date of payments')),
('plugins', models.TextField(blank=True, null=True, verbose_name='Plugins')),
],
options={
'verbose_name_plural': 'Events',
'ordering': ('date_from', 'name'),
'verbose_name': 'Event',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='EventPermission',
fields=[
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
('identity', models.CharField(max_length=36)),
('version_start_date', models.DateTimeField()),
('version_end_date', models.DateTimeField(blank=True, null=True, default=None)),
('version_birth_date', models.DateTimeField()),
('can_change_settings', models.BooleanField(default=True, verbose_name='Can change event settings')),
('can_change_items', models.BooleanField(default=True, verbose_name='Can change item settings')),
('event', versions.models.VersionedForeignKey(to='pretixbase.Event')),
('user', models.ForeignKey(related_name='event_perms', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name_plural': 'Event permissions',
'verbose_name': 'Event permission',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Item',
fields=[
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
('identity', models.CharField(max_length=36)),
('version_start_date', models.DateTimeField()),
('version_end_date', models.DateTimeField(blank=True, null=True, default=None)),
('version_birth_date', models.DateTimeField()),
('name', models.CharField(max_length=255, verbose_name='Item name')),
('active', models.BooleanField(default=True, verbose_name='Active')),
('deleted', models.BooleanField(default=False)),
('short_description', models.TextField(blank=True, null=True, help_text='This is shown below the item name in lists.', verbose_name='Short description')),
('long_description', models.TextField(blank=True, null=True, verbose_name='Long description')),
('default_price', models.DecimalField(decimal_places=2, max_digits=7, blank=True, null=True, verbose_name='Default price')),
('tax_rate', models.DecimalField(decimal_places=2, max_digits=7, blank=True, null=True, verbose_name='Taxes included in percent')),
],
options={
'verbose_name_plural': 'Items',
'verbose_name': 'Item',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='ItemCategory',
fields=[
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
('identity', models.CharField(max_length=36)),
('version_start_date', models.DateTimeField()),
('version_end_date', models.DateTimeField(blank=True, null=True, default=None)),
('version_birth_date', models.DateTimeField()),
('name', models.CharField(max_length=255, verbose_name='Category name')),
('position', models.IntegerField(default=0)),
('event', versions.models.VersionedForeignKey(related_name='categories', to='pretixbase.Event')),
],
options={
'verbose_name_plural': 'Item categories',
'ordering': ('position',),
'verbose_name': 'Item category',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='ItemVariation',
fields=[
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
('identity', models.CharField(max_length=36)),
('version_start_date', models.DateTimeField()),
('version_end_date', models.DateTimeField(blank=True, null=True, default=None)),
('version_birth_date', models.DateTimeField()),
('active', models.BooleanField(default=True, verbose_name='Active')),
('default_price', models.DecimalField(decimal_places=2, max_digits=7, blank=True, null=True, verbose_name='Default price')),
('item', versions.models.VersionedForeignKey(related_name='variations', to='pretixbase.Item')),
],
options={
'verbose_name_plural': 'Item variations',
'verbose_name': 'Item variation',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Order',
fields=[
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
('identity', models.CharField(max_length=36)),
('version_start_date', models.DateTimeField()),
('version_end_date', models.DateTimeField(blank=True, null=True, default=None)),
('version_birth_date', models.DateTimeField()),
('status', models.CharField(max_length=3, choices=[('p', 'pending'), ('n', 'paid'), ('e', 'expired'), ('c', 'cancelled')], verbose_name='Status')),
('datetime', models.DateTimeField(verbose_name='Date', auto_now_add=True)),
('expires', models.DateTimeField(verbose_name='Expiration date')),
('payment_date', models.DateTimeField(verbose_name='Payment date')),
('payment_info', models.TextField(verbose_name='Payment information')),
('total', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Total amount')),
('event', versions.models.VersionedForeignKey(verbose_name='Event', to='pretixbase.Event')),
('user', models.ForeignKey(null=True, verbose_name='User', blank=True, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name_plural': 'Orders',
'verbose_name': 'Order',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='OrderPosition',
fields=[
('id', models.AutoField(serialize=False, auto_created=True, verbose_name='ID', primary_key=True)),
('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Price')),
],
options={
'verbose_name_plural': 'Order positions',
'verbose_name': 'Order position',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Organizer',
fields=[
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
('identity', models.CharField(max_length=36)),
('version_start_date', models.DateTimeField()),
('version_end_date', models.DateTimeField(blank=True, null=True, default=None)),
('version_birth_date', models.DateTimeField()),
('name', models.CharField(max_length=200, verbose_name='Name')),
('slug', models.CharField(unique=True, max_length=50, db_index=True, verbose_name='Slug')),
],
options={
'verbose_name_plural': 'Organizers',
'ordering': ('name',),
'verbose_name': 'Organizer',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='OrganizerPermission',
fields=[
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
('identity', models.CharField(max_length=36)),
('version_start_date', models.DateTimeField()),
('version_end_date', models.DateTimeField(blank=True, null=True, default=None)),
('version_birth_date', models.DateTimeField()),
('can_create_events', models.BooleanField(default=True, verbose_name='Can create events')),
('organizer', versions.models.VersionedForeignKey(to='pretixbase.Organizer')),
('user', models.ForeignKey(related_name='organizer_perms', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name_plural': 'Organizer permissions',
'verbose_name': 'Organizer permission',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Property',
fields=[
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
('identity', models.CharField(max_length=36)),
('version_start_date', models.DateTimeField()),
('version_end_date', models.DateTimeField(blank=True, null=True, default=None)),
('version_birth_date', models.DateTimeField()),
('name', models.CharField(max_length=250, verbose_name='Property name')),
('event', versions.models.VersionedForeignKey(related_name='properties', to='pretixbase.Event')),
],
options={
'verbose_name_plural': 'Item properties',
'verbose_name': 'Item property',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='PropertyValue',
fields=[
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
('identity', models.CharField(max_length=36)),
('version_start_date', models.DateTimeField()),
('version_end_date', models.DateTimeField(blank=True, null=True, default=None)),
('version_birth_date', models.DateTimeField()),
('value', models.CharField(max_length=250, verbose_name='Value')),
('position', models.IntegerField(default=0)),
('prop', versions.models.VersionedForeignKey(related_name='values', to='pretixbase.Property')),
],
options={
'verbose_name_plural': 'Property values',
'ordering': ('position',),
'verbose_name': 'Property value',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Question',
fields=[
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
('identity', models.CharField(max_length=36)),
('version_start_date', models.DateTimeField()),
('version_end_date', models.DateTimeField(blank=True, null=True, default=None)),
('version_birth_date', models.DateTimeField()),
('question', models.TextField(verbose_name='Question')),
('type', models.CharField(max_length=5, choices=[('N', 'Number'), ('S', 'Text (one line)'), ('T', 'Multiline text'), ('B', 'Yes/No')], verbose_name='Question type')),
('required', models.BooleanField(default=False, verbose_name='Required question')),
('event', versions.models.VersionedForeignKey(related_name='questions', to='pretixbase.Event')),
],
options={
'verbose_name_plural': 'Questions',
'verbose_name': 'Question',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='QuestionAnswer',
fields=[
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
('identity', models.CharField(max_length=36)),
('version_start_date', models.DateTimeField()),
('version_end_date', models.DateTimeField(blank=True, null=True, default=None)),
('version_birth_date', models.DateTimeField()),
('answer', models.TextField()),
('cartposition', models.ForeignKey(null=True, to='pretixbase.CartPosition', blank=True)),
('orderposition', models.ForeignKey(null=True, to='pretixbase.OrderPosition', blank=True)),
('question', versions.models.VersionedForeignKey(to='pretixbase.Question')),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Quota',
fields=[
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
('identity', models.CharField(max_length=36)),
('version_start_date', models.DateTimeField()),
('version_end_date', models.DateTimeField(blank=True, null=True, default=None)),
('version_birth_date', models.DateTimeField()),
('name', models.CharField(max_length=200, verbose_name='Name')),
('size', models.PositiveIntegerField(verbose_name='Total capacity')),
('event', versions.models.VersionedForeignKey(related_name='quotas', to='pretixbase.Event', verbose_name='Event')),
('items', versions.models.VersionedManyToManyField(blank=True, related_name='quotas', to='pretixbase.Item', verbose_name='Item')),
('lock_cache', models.ManyToManyField(blank=True, to='pretixbase.CartPosition')),
('order_cache', models.ManyToManyField(blank=True, to='pretixbase.OrderPosition')),
('variations', pretix.base.models.VariationsField(blank=True, related_name='quotas',
to='pretixbase.ItemVariation', verbose_name='Variations')),
],
options={
'verbose_name_plural': 'Quotas',
'verbose_name': 'Quota',
},
bases=(models.Model,),
),
migrations.AlterUniqueTogether(
name='questionanswer',
unique_together=set([('id', 'identity')]),
),
migrations.AddField(
model_name='organizer',
name='permitted',
field=models.ManyToManyField(through='pretixbase.OrganizerPermission', related_name='organizers', to=settings.AUTH_USER_MODEL),
preserve_default=True,
),
migrations.AddField(
model_name='orderposition',
name='answers',
field=versions.models.VersionedManyToManyField(through='pretixbase.QuestionAnswer', to='pretixbase.Question', verbose_name='Answers'),
preserve_default=True,
),
migrations.AddField(
model_name='orderposition',
name='item',
field=versions.models.VersionedForeignKey(verbose_name='Item', to='pretixbase.Item'),
preserve_default=True,
),
migrations.AddField(
model_name='orderposition',
name='order',
field=versions.models.VersionedForeignKey(verbose_name='Order', to='pretixbase.Order'),
preserve_default=True,
),
migrations.AddField(
model_name='orderposition',
name='variation',
field=versions.models.VersionedForeignKey(null=True, verbose_name='Variation', blank=True, to='pretixbase.ItemVariation'),
preserve_default=True,
),
migrations.AddField(
model_name='itemvariation',
name='values',
field=versions.models.VersionedManyToManyField(related_name='variations', to='pretixbase.PropertyValue'),
preserve_default=True,
),
migrations.AddField(
model_name='item',
name='category',
field=versions.models.VersionedForeignKey(on_delete=django.db.models.deletion.PROTECT, null=True, related_name='items', verbose_name='Category', blank=True, to='pretixbase.ItemCategory'),
preserve_default=True,
),
migrations.AddField(
model_name='item',
name='event',
field=versions.models.VersionedForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='items', to='pretixbase.Event', verbose_name='Event'),
preserve_default=True,
),
migrations.AddField(
model_name='item',
name='properties',
field=versions.models.VersionedManyToManyField(blank=True, related_name='items', help_text="The selected properties will be available for the user to select. After saving this field, move to the 'Variations' tab to configure the details.", verbose_name='Properties', to='pretixbase.Property'),
preserve_default=True,
),
migrations.AddField(
model_name='item',
name='questions',
field=versions.models.VersionedManyToManyField(blank=True, related_name='items', help_text='The user will be asked to fill in answers for the selected questions', verbose_name='Questions', to='pretixbase.Question'),
preserve_default=True,
),
migrations.AddField(
model_name='event',
name='organizer',
field=versions.models.VersionedForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='events', to='pretixbase.Organizer'),
preserve_default=True,
),
migrations.AddField(
model_name='event',
name='permitted',
field=models.ManyToManyField(through='pretixbase.EventPermission', related_name='events', to=settings.AUTH_USER_MODEL),
preserve_default=True,
),
migrations.AddField(
model_name='cartposition',
name='event',
field=versions.models.VersionedForeignKey(verbose_name='Event', to='pretixbase.Event'),
preserve_default=True,
),
migrations.AddField(
model_name='cartposition',
name='item',
field=versions.models.VersionedForeignKey(verbose_name='Item', to='pretixbase.Item'),
preserve_default=True,
),
migrations.AddField(
model_name='cartposition',
name='user',
field=models.ForeignKey(null=True, verbose_name='User', blank=True, to=settings.AUTH_USER_MODEL),
preserve_default=True,
),
migrations.AddField(
model_name='cartposition',
name='variation',
field=versions.models.VersionedForeignKey(null=True, verbose_name='Variation', blank=True, to='pretixbase.ItemVariation'),
preserve_default=True,
),
migrations.AddField(
model_name='user',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, null=True, related_name='users', to='pretixbase.Event', blank=True),
preserve_default=True,
),
migrations.AddField(
model_name='user',
name='groups',
field=models.ManyToManyField(related_name='user_set', help_text='The groups this user belongs to. A user will get all permissions granted to each of his/her group.', verbose_name='groups', related_query_name='user', blank=True, to='auth.Group'),
preserve_default=True,
),
migrations.AddField(
model_name='user',
name='user_permissions',
field=models.ManyToManyField(related_name='user_set', help_text='Specific permissions for this user.', verbose_name='user permissions', related_query_name='user', blank=True, to='auth.Permission'),
preserve_default=True,
),
migrations.AlterUniqueTogether(
name='user',
unique_together=set([('event', 'username')]),
),
]

View File

@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import pretix.base.models
import versions.models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='itemcategory',
options={'verbose_name_plural': 'Item categories', 'verbose_name': 'Item category', 'ordering': ('position', 'id')},
),
migrations.RenameField(
model_name='cartposition',
old_name='total',
new_name='price',
),
migrations.AlterField(
model_name='event',
name='currency',
field=models.CharField(verbose_name='Default currency', default='EUR', max_length=10),
preserve_default=True,
),
migrations.AlterField(
model_name='event',
name='locale',
field=models.CharField(verbose_name='Default locale', choices=[('de', 'German'), ('en', 'English')], default='en', max_length=10),
preserve_default=True,
),
migrations.AlterUniqueTogether(
name='questionanswer',
unique_together=set([]),
),
]

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import versions.models
import pretix.base.models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0002_auto_20150211_2031'),
]
operations = [
migrations.RemoveField(
model_name='quota',
name='lock_cache',
),
migrations.RemoveField(
model_name='quota',
name='order_cache',
),
]

View File

@@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.utils.timezone import utc
import versions.models
import datetime
import pretix.base.models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0003_auto_20150211_2042'),
]
operations = [
migrations.AddField(
model_name='cartposition',
name='identity',
field=models.CharField(default='LEGACY', max_length=36),
preserve_default=False,
),
migrations.AddField(
model_name='cartposition',
name='version_birth_date',
field=models.DateTimeField(default=datetime.datetime(2015, 2, 11, 23, 30, 3, 234665, tzinfo=utc)),
preserve_default=False,
),
migrations.AddField(
model_name='cartposition',
name='version_end_date',
field=models.DateTimeField(blank=True, null=True, default=None),
preserve_default=True,
),
migrations.AddField(
model_name='cartposition',
name='version_start_date',
field=models.DateTimeField(default=datetime.datetime(2015, 2, 11, 23, 30, 3, 234665, tzinfo=utc)),
preserve_default=False,
),
migrations.AddField(
model_name='orderposition',
name='identity',
field=models.CharField(default='LEGACY', max_length=36),
preserve_default=False,
),
migrations.AddField(
model_name='orderposition',
name='version_birth_date',
field=models.DateTimeField(default=datetime.datetime(2015, 2, 11, 23, 30, 15, 115790, tzinfo=utc)),
preserve_default=False,
),
migrations.AddField(
model_name='orderposition',
name='version_end_date',
field=models.DateTimeField(blank=True, null=True, default=None),
preserve_default=True,
),
migrations.AddField(
model_name='orderposition',
name='version_start_date',
field=models.DateTimeField(default=datetime.datetime(2015, 2, 11, 23, 30, 21, 726769, tzinfo=utc)),
preserve_default=False,
),
migrations.AlterField(
model_name='cartposition',
name='id',
field=models.CharField(primary_key=True, serialize=False, max_length=36),
preserve_default=True,
),
migrations.AlterField(
model_name='orderposition',
name='id',
field=models.CharField(primary_key=True, serialize=False, max_length=36),
preserve_default=True,
),
]

View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import pretix.base.models
import versions.models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0004_auto_20150211_2330'),
]
operations = [
migrations.AlterField(
model_name='order',
name='payment_date',
field=models.DateTimeField(null=True, blank=True, verbose_name='Payment date'),
preserve_default=True,
),
migrations.AlterField(
model_name='order',
name='payment_info',
field=models.TextField(null=True, blank=True, verbose_name='Payment information'),
preserve_default=True,
),
]

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import pretix.base.models
import versions.models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0005_auto_20150212_0901'),
]
operations = [
migrations.AlterField(
model_name='cartposition',
name='datetime',
field=models.DateTimeField(auto_now_add=True, verbose_name='Date'),
preserve_default=True,
),
]

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import versions.models
import pretix.base.models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0006_auto_20150212_0908'),
]
operations = [
migrations.AddField(
model_name='event',
name='max_items_per_order',
field=models.IntegerField(verbose_name='Maximum number of items per order', default=10),
preserve_default=True,
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0007_auto_20150212_0939'),
]
operations = [
migrations.AddField(
model_name='quota',
name='locked',
field=models.DateTimeField(null=True, blank=True),
preserve_default=True,
),
]

View File

@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import versions.models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0008_quota_locked'),
]
operations = [
migrations.CreateModel(
name='EventSetting',
fields=[
('id', models.CharField(primary_key=True, serialize=False, max_length=36)),
('identity', models.CharField(max_length=36)),
('version_start_date', models.DateTimeField()),
('version_end_date', models.DateTimeField(blank=True, default=None, null=True)),
('version_birth_date', models.DateTimeField()),
('key', models.CharField(max_length=255)),
('value', models.TextField()),
('event', versions.models.VersionedForeignKey(related_name='setting_objects', to='pretixbase.Event')),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='OrganizerSetting',
fields=[
('id', models.CharField(primary_key=True, serialize=False, max_length=36)),
('identity', models.CharField(max_length=36)),
('version_start_date', models.DateTimeField()),
('version_end_date', models.DateTimeField(blank=True, default=None, null=True)),
('version_birth_date', models.DateTimeField()),
('key', models.CharField(max_length=255)),
('value', models.TextField()),
('organizer', versions.models.VersionedForeignKey(related_name='setting_objects', to='pretixbase.Organizer')),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
]

View File

1451
src/pretix/base/models.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
try: # NOQA
from enum import Enum
except ImportError: # NOQA
from flufl.enum import Enum # remove this dependency when support for python <=3.3 is dropped
from django.apps import apps
class PluginType(Enum):
RESTRICTION = 1
def get_all_plugins() -> "List[class]":
"""
Returns the PretixPluginMeta classes of all plugins found in the installed Django apps.
"""
plugins = []
for app in apps.get_app_configs():
if hasattr(app, 'PretixPluginMeta'):
meta = app.PretixPluginMeta
meta.module = app.name
plugins.append(meta)
return plugins

View File

@@ -0,0 +1,53 @@
import django.dispatch
from django.apps import apps
from django.dispatch.dispatcher import NO_RECEIVERS
from .models import Event
class EventPluginSignal(django.dispatch.Signal):
"""
This is an extension to Django's built-in signals which differs in a way that it sends
out it's events only to receivers which belong to plugins that are enabled for the given
Event.
"""
def send(self, sender, **named):
"""
Send signal from sender to all connected receivers that belong to
plugins enabled for the given Event.
sender is required to be an instance of ``pretix.base.models.Event``.
"""
assert isinstance(sender, Event)
responses = []
if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
return responses
for receiver in self._live_receivers(sender):
# Find the Django application this belongs to
searchpath = receiver.__module__
app = None
while "." in searchpath:
try:
if apps.is_installed(searchpath):
app = apps.get_app_config(searchpath.split(".")[-1])
except LookupError:
pass
searchpath, mod = searchpath.rsplit(".", 1)
# Only fire receivers from active plugins
if app.name in sender.get_plugins():
response = receiver(signal=self, sender=sender, **named)
responses.append((receiver, response))
return responses
"""
This signal is sent out every time some component of pretix wants to know whether a specific
item or variation is available for sell. The item will only be sold, if all (active) receivers
return a positive result (see plugin API documentation for details).
"""
determine_availability = EventPluginSignal(
providing_args=["item", "variations", "context", "cache"]
)

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,110 @@
import os
import sys
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.conf import settings
from selenium import webdriver
RUN_LOCAL = ('SAUCE_USERNAME' not in os.environ)
"""
For a long time, we used SauceLabs for CI testing, because they provide free
browser VMs for Open Source projects. However, more tests failed because of
connection timeouts to SauceLabs than for real reasons, so we're using
PhantomJS now. However, we'll keep the SauceClient code here as it might prove
useful some day.
"""
if RUN_LOCAL:
# could add Chrome, Firefox, etc... here
BROWSERS = [os.environ.get('TEST_BROWSER', 'PhantomJS')]
else:
from sauceclient import SauceClient
USERNAME = os.environ.get('SAUCE_USERNAME')
ACCESS_KEY = os.environ.get('SAUCE_ACCESS_KEY')
sauce = SauceClient(USERNAME, ACCESS_KEY)
BROWSERS = [
{"platform": "Mac OS X 10.9",
"browserName": "chrome",
"version": "35"},
{"platform": "Windows 8.1",
"browserName": "internet explorer",
"version": "11"},
{"platform": "Linux",
"browserName": "firefox",
"version": "29"}]
def on_platforms():
if RUN_LOCAL:
def decorator(base_class):
module = sys.modules[base_class.__module__].__dict__
for i, platform in enumerate(BROWSERS):
d = dict(base_class.__dict__)
d['browser'] = platform
name = "%s_%s" % (base_class.__name__, i + 1)
module[name] = type(name, (base_class,), d)
pass
return decorator
def decorator(base_class):
module = sys.modules[base_class.__module__].__dict__
for i, platform in enumerate(BROWSERS):
d = dict(base_class.__dict__)
d['desired_capabilities'] = platform
name = "%s_%s" % (base_class.__name__, i + 1)
module[name] = type(name, (base_class,), d)
return decorator
class BrowserTest(StaticLiveServerTestCase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
settings.DEBUG = ('--debug' in sys.argv)
def setUp(self):
if RUN_LOCAL:
self.setUpLocal()
else:
self.setUpSauce()
def tearDown(self):
if RUN_LOCAL:
self.tearDownLocal()
else:
self.tearDownSauce()
def setUpSauce(self):
if 'TRAVIS_JOB_NUMBER' in os.environ:
self.desired_capabilities['tunnel-identifier'] = \
os.environ['TRAVIS_JOB_NUMBER']
self.desired_capabilities['build'] = os.environ['TRAVIS_BUILD_NUMBER']
self.desired_capabilities['tags'] = \
[os.environ['TRAVIS_PYTHON_VERSION'], 'CI']
self.desired_capabilities['name'] = self.id()
sauce_url = "http://%s:%s@ondemand.saucelabs.com:80/wd/hub"
self.driver = webdriver.Remote(
desired_capabilities=self.desired_capabilities,
command_executor=sauce_url % (USERNAME, ACCESS_KEY)
)
self.driver.implicitly_wait(5)
def setUpLocal(self):
self.driver = getattr(webdriver, self.browser)()
self.driver.implicitly_wait(3)
def tearDownLocal(self):
self.driver.quit()
def tearDownSauce(self):
print("\nLink to your job: \n "
"https://saucelabs.com/jobs/%s \n" % self.driver.session_id)
try:
if sys.exc_info() == (None, None, None):
sauce.jobs.update_job(self.driver.session_id, passed=True)
else:
sauce.jobs.update_job(self.driver.session_id, passed=False)
finally:
self.driver.quit()

View File

@@ -0,0 +1,47 @@
import random
from django.test import TestCase
from django.core.cache import cache as django_cache
from django.utils.timezone import now
from pretix.base.models import Event, Organizer
class CacheTest(TestCase):
"""
This test case tests the invalidation of the event related
cache.
"""
def setUp(self):
o = Organizer.objects.create(name='Dummy', slug='dummy')
self.event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
date_from=now(),
)
self.cache = self.event.get_cache()
randint = random.random()
self.testkey = "test" + str(randint)
def test_interference(self):
django_cache.clear()
self.cache.set(self.testkey, "foo")
self.assertIsNone(django_cache.get(self.testkey))
self.assertIn(self.cache.get(self.testkey), (None, "foo"))
def test_longkey(self):
self.cache.set(self.testkey * 100, "foo")
self.assertEquals(self.cache.get(self.testkey * 100), "foo")
def test_invalidation(self):
self.cache.set(self.testkey, "foo")
self.cache.clear()
self.assertIsNone(self.cache.get(self.testkey))
def test_many(self):
inp = {
'a': 'foo',
'b': 'bar',
}
self.cache.set_many(inp)
self.assertEquals(inp, self.cache.get_many(inp.keys()))

View File

@@ -0,0 +1,72 @@
from django.test import TestCase, Client
from django.utils.timezone import now
from django.conf import settings
from pretix.base.models import Event, Organizer, User
class LocaleDeterminationTest(TestCase):
"""
This test case tests various methods around the properties /
variations concept.
"""
def setUp(self):
o = Organizer.objects.create(name='Dummy', slug='dummy')
self.event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
date_from=now(),
)
self.TEST_LOCALE = 'de' if settings.LANGUAGE_CODE == 'en' else 'en'
self.TEST_LOCALE_LONG = 'de-AT' if settings.LANGUAGE_CODE == 'en' else 'en-NZ'
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy@dummy.dummy', 'dummy')
def test_global_default(self):
c = Client()
response = c.get('/control/login')
language = response['Content-Language']
self.assertEqual(language, settings.LANGUAGE_CODE)
def test_browser_default(self):
c = Client(HTTP_ACCEPT_LANGUAGE=self.TEST_LOCALE)
response = c.get('/control/login')
language = response['Content-Language']
self.assertEqual(language, self.TEST_LOCALE)
c = Client(HTTP_ACCEPT_LANGUAGE=self.TEST_LOCALE_LONG)
response = c.get('/control/login')
language = response['Content-Language']
self.assertEqual(language, self.TEST_LOCALE)
def test_unknown_browser_default(self):
c = Client(HTTP_ACCEPT_LANGUAGE='sjn')
response = c.get('/control/login')
language = response['Content-Language']
self.assertEqual(language, settings.LANGUAGE_CODE)
def test_cookie_settings(self):
c = Client()
cookies = c.cookies
cookies[settings.LANGUAGE_COOKIE_NAME] = self.TEST_LOCALE
response = c.get('/control/login')
language = response['Content-Language']
self.assertEqual(language, self.TEST_LOCALE)
cookies[settings.LANGUAGE_COOKIE_NAME] = self.TEST_LOCALE_LONG
response = c.get('/control/login')
language = response['Content-Language']
self.assertEqual(language, self.TEST_LOCALE)
def test_user_settings(self):
c = Client()
self.user.locale = self.TEST_LOCALE
self.user.save()
response = c.post('/control/login', {
'email': 'dummy@dummy.dummy',
'password': 'dummy',
})
self.assertEqual(response.status_code, 302)
response = c.get('/control/login')
language = response['Content-Language']
self.assertEqual(language, self.TEST_LOCALE)

View File

@@ -0,0 +1,358 @@
from datetime import timedelta
from django.test import TestCase
from django.utils.timezone import now
from pretix.base.models import (
Event, Organizer, Item, ItemVariation,
Property, PropertyValue, User, Quota,
Order, OrderPosition, CartPosition,
OrganizerSetting)
from pretix.base.types import VariationDict
class ItemVariationsTest(TestCase):
"""
This test case tests various methods around the properties /
variations concept.
"""
def setUp(self):
o = Organizer.objects.create(name='Dummy', slug='dummy')
self.event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
date_from=now(),
)
p = Property.objects.create(event=self.event, name='Size')
PropertyValue.objects.create(prop=p, value='S')
PropertyValue.objects.create(prop=p, value='M')
PropertyValue.objects.create(prop=p, value='L')
p = Property.objects.create(event=self.event, name='Color')
PropertyValue.objects.create(prop=p, value='black')
PropertyValue.objects.create(prop=p, value='blue')
def test_variationdict(self):
i = Item.objects.create(event=self.event, name='Dummy')
p = Property.objects.get(event=self.event, name='Size')
i.properties.add(p)
iv = ItemVariation.objects.create(item=i)
pv = PropertyValue.objects.get(prop=p, value='S')
iv.values.add(pv)
variations = i.get_all_variations()
for vd in variations:
for i, v in vd.relevant_items():
self.assertIs(type(v), PropertyValue)
for v in vd.relevant_values():
self.assertIs(type(v), PropertyValue)
if vd[p.pk] == pv:
vd1 = vd
vd2 = VariationDict()
vd2[p.pk] = pv
self.assertEqual(vd2.identify(), vd1.identify())
self.assertEqual(vd2, vd1)
vd2[p.pk] = PropertyValue.objects.get(prop=p, value='M')
self.assertNotEqual(vd2.identify(), vd.identify())
self.assertNotEqual(vd2, vd1)
vd3 = vd2.copy()
self.assertEqual(vd3, vd2)
vd2[p.pk] = pv
self.assertNotEqual(vd3, vd2)
vd4 = VariationDict()
vd4[4] = 'b'
vd4[2] = 'a'
self.assertEqual(vd4.ordered_values(), ['a', 'b'])
def test_get_all_variations(self):
i = Item.objects.create(event=self.event, name='Dummy')
# No properties available
v = i.get_all_variations()
self.assertEqual(len(v), 1)
self.assertEqual(v[0], {})
# One property, no variations
p = Property.objects.get(event=self.event, name='Size')
i.properties.add(p)
v = i.get_all_variations()
self.assertIs(type(v), list)
self.assertEqual(len(v), 3)
values = []
for var in v:
self.assertIs(type(var), VariationDict)
self.assertIn(p.pk, var)
self.assertIs(type(var[p.pk]), PropertyValue)
values.append(var[p.pk].value)
self.assertEqual(sorted(values), sorted(['S', 'M', 'L']))
# One property, one variation
iv = ItemVariation.objects.create(item=i)
iv.values.add(PropertyValue.objects.get(prop=p, value='S'))
v = i.get_all_variations()
self.assertIs(type(v), list)
self.assertEqual(len(v), 3)
values = []
num_variations = 0
for var in v:
self.assertIs(type(var), VariationDict)
if 'variation' in var and type(var['variation']) is ItemVariation:
self.assertEqual(iv.pk, var['variation'].pk)
values.append(var['variation'].values.all()[0].value)
num_variations += 1
elif p.pk in var:
self.assertIs(type(var[p.pk]), PropertyValue)
values.append(var[p.pk].value)
self.assertEqual(sorted(values), sorted(['S', 'M', 'L']))
self.assertEqual(num_variations, 1)
# Two properties, one variation
p2 = Property.objects.get(event=self.event, name='Color')
i.properties.add(p2)
iv.values.add(PropertyValue.objects.get(prop=p2, value='black'))
v = i.get_all_variations()
self.assertIs(type(v), list)
self.assertEqual(len(v), 6)
values = []
num_variations = 0
for var in v:
self.assertIs(type(var), VariationDict)
if 'variation' in var:
self.assertEqual(iv.pk, var['variation'].pk)
values.append(sorted([ivv.value for ivv in iv.values.all()]))
self.assertEqual(sorted([ivv.value for ivv in iv.values.all()]), sorted(['S', 'black']))
num_variations += 1
else:
values.append(sorted([pv.value for pv in var.values()]))
self.assertEqual(sorted(values), sorted([
['S', 'black'],
['S', 'blue'],
['M', 'black'],
['M', 'blue'],
['L', 'black'],
['L', 'blue'],
]))
self.assertEqual(num_variations, 1)
class VersionableTestCase(TestCase):
def test_shallow_cone(self):
o = Organizer.objects.create(name='Dummy', slug='dummy')
event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
date_from=now(),
)
old = Item.objects.create(event=event, name='Dummy', default_price=14)
prop = Property.objects.create(event=event, name='Size')
old.properties.add(prop)
new = old.clone_shallow()
self.assertIsNone(new.version_end_date)
self.assertIsNotNone(old.version_end_date)
self.assertEqual(new.properties.count(), 0)
self.assertEqual(old.properties.count(), 1)
class UserTestCase(TestCase):
def test_identifier_local(self):
o = Organizer.objects.create(name='Dummy', slug='dummy')
event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
date_from=now(),
)
u = User(event=event, username='tester')
u.set_password("test")
u.save()
self.assertEqual(u.identifier, "%s@%s.event.pretix" % (u.username.lower(), event.id))
def test_identifier_global(self):
u = User(email='test@example.com')
u.set_password("test")
u.save()
self.assertEqual(u.identifier, "test@example.com")
class QuotaTestCase(TestCase):
def setUp(self):
o = Organizer.objects.create(name='Dummy', slug='dummy')
self.event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
date_from=now(),
)
self.quota = Quota.objects.create(name="Test", size=2, event=self.event)
self.item1 = Item.objects.create(event=self.event, name="Ticket")
self.item2 = Item.objects.create(event=self.event, name="T-Shirt")
p = Property.objects.create(event=self.event, name='Size')
pv1 = PropertyValue.objects.create(prop=p, value='S')
PropertyValue.objects.create(prop=p, value='M')
PropertyValue.objects.create(prop=p, value='L')
self.var1 = ItemVariation.objects.create(item=self.item2)
self.var1.values.add(pv1)
self.item2.properties.add(p)
def test_available(self):
self.quota.items.add(self.item1)
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 2))
self.quota.items.add(self.item2)
self.quota.variations.add(self.var1)
try:
self.item2.check_quotas()
self.assertTrue(False)
except:
pass
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 2))
def test_sold_out(self):
self.quota.items.add(self.item1)
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
expires=now() + timedelta(days=3),
total=4)
OrderPosition.objects.create(order=order, item=self.item1, price=2)
OrderPosition.objects.create(order=order, item=self.item1, price=2)
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_GONE, 0))
self.quota.items.add(self.item2)
self.quota.variations.add(self.var1)
self.quota.size = 3
self.quota.save()
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
expires=now() + timedelta(days=3),
total=4)
OrderPosition.objects.create(order=order, item=self.item2, variation=self.var1, price=2)
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_GONE, 0))
def test_ordered(self):
self.quota.items.add(self.item1)
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
expires=now() + timedelta(days=3),
total=4)
OrderPosition.objects.create(order=order, item=self.item1, price=2)
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
order = Order.objects.create(event=self.event, status=Order.STATUS_PENDING,
expires=now() + timedelta(days=3),
total=4)
OrderPosition.objects.create(order=order, item=self.item1, price=2)
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0))
order.expires = now() - timedelta(days=3)
order.save()
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
def test_reserved(self):
self.quota.items.add(self.item1)
self.quota.size = 3
self.quota.save()
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
expires=now() + timedelta(days=3),
total=4)
OrderPosition.objects.create(order=order, item=self.item1, price=2)
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 2))
order = Order.objects.create(event=self.event, status=Order.STATUS_PENDING,
expires=now() + timedelta(days=3),
total=4)
OrderPosition.objects.create(order=order, item=self.item1, price=2)
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
cp = CartPosition.objects.create(event=self.event, item=self.item1, price=2,
expires=now() + timedelta(days=3))
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_RESERVED, 0))
cp.expires = now() - timedelta(days=3)
cp.save()
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
self.quota.items.add(self.item2)
self.quota.variations.add(self.var1)
cp = CartPosition.objects.create(event=self.event, item=self.item2, variation=self.var1,
price=2, expires=now() + timedelta(days=3))
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_RESERVED, 0))
def test_multiple(self):
self.quota.items.add(self.item1)
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 2))
quota2 = Quota.objects.create(event=self.event, name="Test 2", size=1)
quota2.items.add(self.item1)
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
quota2.size = 0
quota2.save()
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_GONE, 0))
class SettingsTestCase(TestCase):
def setUp(self):
OrganizerSetting.DEFAULTS['test_default'] = 'def'
self.organizer = Organizer.objects.create(name='Dummy', slug='dummy')
self.event = Event.objects.create(
organizer=self.organizer, name='Dummy', slug='dummy',
date_from=now(),
)
def test_event_set_explicit(self):
self.event.settings.test = 'foo'
self.assertEqual(self.event.settings.test, 'foo')
# Reload object
self.event = Event.objects.get(identity=self.event.identity)
self.assertEqual(self.event.settings.test, 'foo')
def test_event_set_on_organizer(self):
self.organizer.settings.test = 'foo'
self.assertEqual(self.organizer.settings.test, 'foo')
self.assertEqual(self.event.settings.test, 'foo')
# Reload object
self.organizer = Organizer.objects.get(identity=self.organizer.identity)
self.event = Event.objects.get(identity=self.event.identity)
self.assertEqual(self.organizer.settings.test, 'foo')
self.assertEqual(self.event.settings.test, 'foo')
def test_override_organizer(self):
self.organizer.settings.test = 'foo'
self.event.settings.test = 'bar'
self.assertEqual(self.organizer.settings.test, 'foo')
self.assertEqual(self.event.settings.test, 'bar')
# Reload object
self.organizer = Organizer.objects.get(identity=self.organizer.identity)
self.event = Event.objects.get(identity=self.event.identity)
self.assertEqual(self.organizer.settings.test, 'foo')
self.assertEqual(self.event.settings.test, 'bar')
def test_default(self):
self.assertEqual(self.organizer.settings.test_default, 'def')
self.assertEqual(self.event.settings.test_default, 'def')
def test_delete(self):
self.organizer.settings.test = 'foo'
self.event.settings.test = 'bar'
self.assertEqual(self.organizer.settings.test, 'foo')
self.assertEqual(self.event.settings.test, 'bar')
del self.event.settings.test
self.assertEqual(self.event.settings.test, 'foo')
self.event = Event.objects.get(identity=self.event.identity)
self.assertEqual(self.event.settings.test, 'foo')
del self.organizer.settings.test
self.assertIsNone(self.organizer.settings.test)
self.organizer = Organizer.objects.get(identity=self.organizer.identity)
self.assertIsNone(self.organizer.settings.test)

View File

@@ -0,0 +1,57 @@
from django.test import TestCase
from django.utils.timezone import now
from django.conf import settings
from pretix.base.models import Event, Organizer
from pretix.base.plugins import get_all_plugins
from pretix.base.signals import determine_availability
class PluginRegistryTest(TestCase):
"""
This test case performs tests for the plugin registry.
"""
def setUp(self):
o = Organizer.objects.create(name='Dummy', slug='dummy')
self.event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
date_from=now(),
)
def test_plugin_names(self):
for mod in get_all_plugins():
self.assertIn(mod.module, settings.INSTALLED_APPS)
def test_metadata(self):
for mod in get_all_plugins():
self.assertTrue(hasattr(mod, 'name'))
self.assertTrue(hasattr(mod, 'version'))
self.assertTrue(hasattr(mod, 'type'))
class PluginSignalTest(TestCase):
"""
This test case tests the EventPluginSignal handler
"""
def setUp(self):
o = Organizer.objects.create(name='Dummy', slug='dummy')
self.event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
date_from=now(),
)
def test_no_plugins_active(self):
self.event.plugins = ''
self.event.save()
responses = determine_availability.send(self.event)
self.assertEqual(len(responses), 0)
def test_one_plugin_active(self):
self.event.plugins = 'pretix.plugins.testdummy'
self.event.save()
payload = {'foo': 'bar'}
responses = determine_availability.send(self.event, **payload)
self.assertEqual(len(responses), 1)
self.assertIn('pretix.plugins.testdummy.signals', [r[0].__module__ for r in responses])

93
src/pretix/base/types.py Normal file
View File

@@ -0,0 +1,93 @@
class VariationDict(dict):
"""
A VariationDict object behaves exactle the same as the Python built-in
``dict`` does, but adds some special methods. It is used for the dicts
returned by ``Item.get_all_variations()`` to avoid duplicate code in the
code calling this method.
"""
IGNORE_KEYS = ('variation', 'key', 'available', 'price')
def relevant_items(self) -> "list[(str, PropertyValue)]":
"""
Iterate over all items with numeric keys.
This is in use because the variation dictionaries use property ids
as key and have some special keys like 'variation'.
"""
return (i for i in self.items() if i[0] not in self.IGNORE_KEYS)
def relevant_values(self) -> "list[PropertyValue]":
"""
Iterate over all values with numeric keys.
This is in use because the variation dictionaries use property ids
as key and have some special keys like 'variation'.
"""
return (i[1] for i in self.items() if i[0] not in self.IGNORE_KEYS)
def identify(self) -> str:
"""
Build a simple and unique identifier for this dict. This can be any string
used to compare one VariationDict to others.
In the current implementation, it is a string containing a list of
the PropertyValue id's, sorted by the Property id's and is therefore
unique among one item.
"""
def order_key(i):
return i[0]
return ",".join((
str(v[1].pk) for v in sorted(self.relevant_items(), key=order_key)
))
def key(self) -> str:
"""
Build an identifier for this dict which exactly specifies the combination
for this variation without any doubt. This can be used to "talk" about a
variation in network communication.
In the current implementation, it is a string containing a list of
the propertyid:valueid tuples without any specific order and is therefore
not useful to compare two VariationDicts for equality.
"""
return ",".join((
str(v[0]) + ":" + str(v[1].pk) for v in self.relevant_items()
))
def __eq__(self, other):
if type(other) is type(self):
return self.identify() == other.identify()
else:
return super().__eq__(other)
def empty(self):
"""
Returns true, if this VariationDict does not contain any "real" data like
references to PropertyValues, but only "metadata".
"""
return not next(self.relevant_items(), False)
def ordered_values(self) -> "list[ItemVariation]":
"""
Returns a list of values ordered by their keys
"""
return [
i[1] for i
in sorted(
[it for it in self.relevant_items()],
key=lambda i: i[0]
)
]
def __str__(self):
return " ".join([v.value for v in self.ordered_values()])
def copy(self) -> "VariationDict":
"""
Return a one-level deep copy of this object (create a new
VariationDict but make a shallow copy of the dict inside it).
"""
new = VariationDict()
for k, v in self.items():
new[k] = v
return new

View File

@@ -0,0 +1,8 @@
from django.apps import AppConfig
class PretixControlConfig(AppConfig):
name = 'pretix.control'
label = 'pretixcontrol'
default_app_config = 'pretix.control.PretixControlConfig'

View File

@@ -0,0 +1,14 @@
from django.conf import settings
from django.core.urlresolvers import resolve
def contextprocessor(request):
"""
Adds data to all template contexts
"""
ctx = {
'url_name': resolve(request.path_info).url_name,
'settings': settings,
}
return ctx

View File

@@ -0,0 +1,59 @@
from django.conf import settings
from django.core.urlresolvers import resolve
from django.utils.encoding import force_str
from django.utils.six.moves.urllib.parse import urlparse
from django.shortcuts import resolve_url
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.http import HttpResponseNotFound
from django.utils.translation import ugettext as _
from pretix.base.models import Event
class PermissionMiddleware:
"""
This middleware enforces all requests to the control app to require login.
Additionally, it enforces all requests to "control:event." URLs
to be for an event the user has basic access to.
"""
EXCEPTIONS = (
"auth.login"
)
def process_request(self, request):
url = resolve(request.path_info)
url_namespace = url.namespace
url_name = url.url_name
if url_namespace != 'control' or url_name in self.EXCEPTIONS:
return
if not request.user.is_authenticated():
# Taken from django/contrib/auth/decorators.py
path = request.build_absolute_uri()
# urlparse chokes on lazy objects in Python 3, force to str
resolved_login_url = force_str(
resolve_url(settings.LOGIN_URL_CONTROL))
# If the login url is the same scheme and net location then just
# use the path as the "next" url.
login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
current_scheme, current_netloc = urlparse(path)[:2]
if ((not login_scheme or login_scheme == current_scheme) and
(not login_netloc or login_netloc == current_netloc)):
path = request.get_full_path()
from django.contrib.auth.views import redirect_to_login
return redirect_to_login(
path, resolved_login_url, REDIRECT_FIELD_NAME)
request.user.events_cache = request.user.events.current.order_by(
"organizer", "date_from").prefetch_related("organizer")
if 'event.' in url_name and 'event' in url.kwargs:
try:
request.event = Event.objects.current.filter(
slug=url.kwargs['event'],
permitted__id__exact=request.user.id,
organizer__slug=url.kwargs['organizer'],
).select_related('organizer')[0]
except IndexError:
return HttpResponseNotFound(_("The selected event was not found or you "
"have no permission to administrate it."))

View File

@@ -0,0 +1,43 @@
from django.http import HttpResponseForbidden
from django.utils.translation import ugettext as _
from pretix.base.models import EventPermission
def event_permission_required(permission):
"""
This view decorator rejects all requests with a 403 response which are not from
users having the given permission for the event the request is associated with.
"""
def decorator(function):
def wrapper(request, *args, **kw):
if not request.user.is_authenticated(): # NOQA
# just a double check, should not ever happen
return HttpResponseForbidden()
perm = EventPermission.objects.get(
event=request.event,
user=request.user
)
allowed = False
try:
allowed = getattr(perm, permission)
except AttributeError:
pass
if allowed:
return function(request, *args, **kw)
return HttpResponseForbidden(_('You do not have permission to view this content.'))
return wrapper
return decorator
class EventPermissionRequiredMixin:
"""
This mixin is equivalent to the event_permission_required view decorator but
is in a form suitable for class-based views.
"""
permission = ''
@classmethod
def as_view(cls, **initkwargs):
view = super(EventPermissionRequiredMixin, cls).as_view(**initkwargs)
return event_permission_required(cls.permission)(view)

View File

@@ -0,0 +1,10 @@
from pretix.base.signals import EventPluginSignal
"""
This signal is sent out to build configuration forms for all restriction formsets
(see plugin API documentation for details).
"""
restriction_formset = EventPluginSignal(
providing_args=["item"]
)

View File

@@ -0,0 +1,27 @@
"use strict";
$(function () {
$("[data-formset]").formset({
animateForms: true,
reorderMode: 'animate'
});
$(document).on("click", ".variations .variations-select-all", function (e) {
$(this).parent().parent().find("input[type=checkbox]").prop("checked", true).change();
e.stopPropagation();
return false;
});
$(document).on("click", ".variations .variations-select-none", function (e) {
$(this).parent().parent().find("input[type=checkbox]").prop("checked", false).change();
e.stopPropagation();
return false;
});
if ($(".items-on-quota").length) {
$(".items-on-quota .panel").each(function () {
var $panel = $(this);
$panel.toggleClass("panel-success", $panel.find("input:checked").length > 0);
$(this).find("input").change(function () {
$panel.toggleClass("panel-success", $panel.find("input:checked").length > 0);
});
});
}
$('.collapse').collapse();
});

View File

@@ -0,0 +1,28 @@
@import "../../../../base/static/bootstrap/less/bootstrap.less";
body {
background: #eee;
}
footer {
text-align: center;
padding: 10px 0;
font-size: 11px;
}
.form-signin {
.well;
max-width: 330px;
margin: auto;
margin-top: 10%;
padding-bottom: 0;
.control-label {
.sr-only;
}
.buttons {
text-align: right;
}
}

View File

@@ -0,0 +1,78 @@
td > .form-group {
margin-bottom: 0;
}
td > .errorlist {
color: #a94442;
list-style-type: none;
padding: 0;
margin: 0;
}
td > .form-group > .checkbox {
margin-top: 0;
margin-bottom: 0;
}
.has-success .form-control {
border-color: #cccccc;
}
.form-horizontal [data-formset] .form-group {
width: 100%;
}
[data-formset] .form-group:not([data-formset-form-deleted]):last-of-type [data-formset-move-down-button],
[data-formset] .form-group:not([data-formset-form-deleted]):first-of-type [data-formset-move-up-button] {
cursor: not-allowed;
pointer-events: none; // Future-proof disabling of clicks
.opacity(.65);
.box-shadow(none);
}
.form-plugins .panel-title {
line-height: 34px;
}
.restriction-formset .variations label {
margin: 0;
}
.submit-group {
margin: 15px 0 0 0 !important;
padding: 15px;
background: #eeeeee;
text-align: right;
.btn-save {
.btn-lg;
}
.btn-cancel {
.pull-left;
.btn-lg;
}
}
.panel .form-group:last-child {
margin-bottom: 0;
}
.container ul.nav-pills {
margin: 20px 0;
}
.variation-matrix {
td .form-group, .checkbox {
margin: 0;
}
}
.table-quotas td ul {
list-style: none;
margin-left: 0;
padding-left: 0;
}
@media (min-width: @screen-sm-min) {
.variation-matrix > tbody > tr > td {
line-height: 34px;
input[type=checkbox] {
margin-top: 10px;
}
}
}

View File

@@ -0,0 +1,4 @@
@import "../../../../base/static/bootstrap/less/bootstrap.less";
@import "../../../../base/static/fontawesome/less/font-awesome.less";
@fa-font-path: "../../fontawesome/fonts";
@import "forms.less";

View File

@@ -0,0 +1,25 @@
{% load compress %}
{% load i18n %}
{% load staticfiles %}
<!DOCTYPE html>
<html>
<head>
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
{% compress css %}
<link rel="stylesheet" type="text/less" href="{% static "pretixcontrol/less/auth.less" %}" />
{% endcompress %}
</head>
<body>
<div class="container">
{% block content %}
{% endblock %}
<footer>
{% with "href='http://pretix.de'" as a_attr %}
{% blocktrans trimmed %}
powered by <a {{ a_attr }}>pretix</a>
{% endblocktrans %}
{% endwith %}
</footer>
</div>
</body>
</html>

View File

@@ -0,0 +1,16 @@
{% extends "pretixcontrol/auth/base.html" %}
{% load bootstrap3 %}
{% load i18n %}
{% block content %}
<form class="form-signin" action="" method="post">
{% bootstrap_form_errors form type='all' layout='inline' %}
{% csrf_token %}
{% bootstrap_field form.email %}
{% bootstrap_field form.password %}
<div class="form-group buttons">
<button type="submit" class="btn btn-primary">
{% trans "Log in" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,50 @@
{% load compress %}
{% load staticfiles %}
{% load i18n %}
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}{% endblock %}{% if url_name != "index" %} :: {% endif %}
{{ settings.PRETIX_INSTANCE_NAME }}</title>
{% compress css %}
<link rel="stylesheet" type="text/less" href="{% static "pretixcontrol/less/main.less" %}" />
{% endcompress %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
<script type="text/javascript" src="{% static "js/jquery.formset.js" %}"></script>
<script type="text/javascript" src="{% static "bootstrap/dist/js/bootstrap.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/main.js" %}"></script>
{% endcompress %}
</head>
<body>
<div class="navbar navbar-default navbar-static-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">{% trans "Toggle navigation" %}</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">{{ settings.PRETIX_INSTANCE_NAME }}</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
{% block nav %}
<li {% if url_name == "index" %}class="active"{% endif %}><a href="{% url 'control:index' %}">{% trans "Dashboard" %}</a></li>
<li {% if "events" in url_name %}class="active"{% endif %}><a href="{% url 'control:events' %}">{% trans "Events" %}</a></li>
{% endblock %}
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="#"><i class="fa fa-user"></i> {{ request.user.get_full_name }}</a></li>
<li><a href="{% url 'control:auth.logout' %}" title="{% trans "Log out" %}"><i class="fa fa-sign-out"></i><span class="visible-xs-inline">{% trans "Log out" %}</span></a></li>
</ul>
</div><!--/.nav-collapse -->
</div>
</div>
<div class="container">
{% block content %}
{% endblock %}
</div>
</body>
</html>

View File

@@ -0,0 +1,21 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% block nav %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-calendar"></i> {{ request.event.slug }} <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li><a href="{% url "control:events" %}">{% trans "Event overview" %}</a></li>
{% regroup request.user.events_cache by organizer as event_list %}
{% for g in event_list %}
<li class="dropdown-header">{{ g.grouper }}</li>
{% for e in g.list %}
<li><a href="{% url "control:event.index" organizer=g.grouper.slug event=e.slug %}">{{ e.name }}</a></li>
{% endfor %}
{% endfor %}
</ul>
</li>
<li {% if url_name == "event.index" %}class="active"{% endif %}><a href="{% url 'control:event.index' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Dashboard" %}</a></li>
<li {% if "event.settings" in url_name %}class="active"{% endif %}><a href="{% url 'control:event.settings' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Settings" %}</a></li>
<li {% if "event.item" in url_name %}class="active"{% endif %}><a href="{% url 'control:event.items' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Items" %}</a></li>
{% endblock %}

View File

@@ -0,0 +1,6 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% block title %}{{ request.event.name }}{% endblock %}
{% block content %}
<h1>{{ request.event.name }}</h1>
{% endblock %}

View File

@@ -0,0 +1,39 @@
{% extends "pretixcontrol/event/settings_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inside %}
<form action="" method="post" class="form-horizontal form-plugins">
{% csrf_token %}
<fieldset>
<legend>{% trans "Installed plugins" %}</legend>
{% if "success" in request.GET %}
<div class="alert alert-success">
{% trans "Your changes have been saved." %}
</div>
{% endif %}
{% for plugin in plugins %}
<div class="panel panel-{% if plugin.module in plugins_active %}success{% else %}default{% endif %}">
<div class="panel-heading">
<div class="row">
<div class="col-sm-10">
<h3 class="panel-title">{{ plugin.name }}</h3>
</div>
<div class="col-sm-2">
{% if plugin.module in plugins_active %}
<button class="btn btn-default btn-block" name="plugin:{{ plugin.module }}" value="disable">{% trans "Disable" %}</button>
{% else %}
<button class="btn btn-primary btn-block" name="plugin:{{ plugin.module }}" value="enable">{% trans "Enable" %}</button>
{% endif %}
</div>
</div>
</div>
<div class="panel-body">
<p class="meta">{% blocktrans %}Version {{ plugin.version }} by <em>{{ plugin.author }}</em>{% endblocktrans %}</p>
<p>{{ plugin.description }}</p>
</div>
</div>
{% endfor %}
</fieldset>
</form>
{% endblock %}

View File

@@ -0,0 +1,44 @@
{% extends "pretixcontrol/event/settings_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inside %}
<form action="" method="post" class="form-horizontal">
{% if "success" in request.GET %}
<div class="alert alert-success">
{% trans "Your changes have been saved." %}
</div>
{% endif %}
{% csrf_token %}
<fieldset>
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.slug layout="horizontal" %}
{% bootstrap_field form.date_from layout="horizontal" %}
{% bootstrap_field form.date_to layout="horizontal" %}
{% bootstrap_field form.currency layout="horizontal" %}
</fieldset>
<fieldset>
<legend>{% trans "Display settings" %}</legend>
{% bootstrap_field form.locale layout="horizontal" %}
{% bootstrap_field form.timezone layout="horizontal" %}
{% bootstrap_field form.show_date_to layout="horizontal" %}
{% bootstrap_field form.show_times layout="horizontal" %}
</fieldset>
<fieldset>
<legend>{% trans "Presale settings" %}</legend>
{% bootstrap_field form.presale_start layout="horizontal" %}
{% bootstrap_field form.presale_end layout="horizontal" %}
{% bootstrap_field form.max_items_per_order layout="horizontal" %}
</fieldset>
<fieldset>
<legend>{% trans "Payment settings" %}</legend>
{% bootstrap_field form.payment_term_days layout="horizontal" %}
{% bootstrap_field form.payment_term_last layout="horizontal" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,13 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{{ request.event.name }}{% endblock %}
{% block content %}
<h1>{% trans "Settings" %}</h1>
<ul class="nav nav-pills">
<li {% if "event.settings" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.settings' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "General settings" %}</a></li>
<li {% if "event.settings.plugins" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.settings.plugins' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Plugins" %}</a></li>
</ul>
{% block inside %}
{% endblock %}
{% endblock %}

View File

@@ -0,0 +1,27 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% block title %}{% trans "Events" %}{% endblock %}
{% block content %}
<h1>{% trans "Events" %}</h1>
<p>{% trans "The list below shows all events you have administrative access to. Click on the event name to access event details." %}</p>
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "Event name" %}</th>
<th>{% trans "Organizer" %}</th>
<th>{% trans "Start date" %}</th>
<th>{% trans "End date" %}</th>
</tr>
</thead>
<tbody>
{% for e in events %}
<tr>
<td><strong><a href="{% url "control:event.index" organizer=e.organizer.slug event=e.slug %}">{{ e.name }}</a></strong></td>
<td>{{ e.organizer }}</td>
<td>{{ e.get_date_from_display }}</td>
<td>{{ e.get_date_to_display }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@@ -0,0 +1,28 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% block title %}{{ item.name }} :: {% trans "Item" %}{% endblock %}
{% block content %}
{% if item.identity %}
<h1>{% trans "Modify item:" %} {{ item.name }}</h1>
<ul class="nav nav-pills">
<li {% if "event.item" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.item' organizer=request.event.organizer.slug event=request.event.slug item=item.identity %}">{% trans "General information" %}</a></li>
<li {% if "event.item.variations" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.item.variations' organizer=request.event.organizer.slug event=request.event.slug item=item.identity %}">{% trans "Variations" %}</a></li>
<li {% if "event.item.restrictions" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.item.restrictions' organizer=request.event.organizer.slug event=request.event.slug item=item.identity %}">{% trans "Restrictions" %}</a></li>
</ul>
{% else %}
<h1>{% trans "Create item" %}</h1>
<p>{% blocktrans trimmed %}
You will be able to adjust further settings in the next step.
{% endblocktrans %}</p>
{% endif %}
{% if item.identity and not item.quotas.exists %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
Please note, that your item will <strong>not</strong> be available for sale until you added your item
to an existing or newly created quota.
{% endblocktrans %}
</div>
{% endif %}
{% block inside %}
{% endblock %}
{% endblock %}

View File

@@ -0,0 +1,36 @@
{% extends "pretixcontrol/item/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inside %}
{% if "success" in request.GET %}
<div class="alert alert-success">
{% trans "Your changes have been saved." %}
</div>
{% endif %}
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<fieldset>
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.active layout="horizontal" %}
{% bootstrap_field form.category layout="horizontal" %}
{% bootstrap_field form.short_description layout="horizontal" %}
{% bootstrap_field form.long_description layout="horizontal" %}
</fieldset>
<fieldset>
<legend>{% trans "Price settings" %}</legend>
{% bootstrap_field form.default_price layout="horizontal" %}
{% bootstrap_field form.tax_rate layout="horizontal" %}
</fieldset>
<fieldset>
<legend>{% trans "Advanced settings" %}</legend>
{% bootstrap_field form.properties layout="horizontal" %}
{% bootstrap_field form.questions layout="horizontal" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,71 @@
{% extends "pretixcontrol/item/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load formset_tags %}
{% block inside %}
<p>{% blocktrans trimmed %}
In this area, you can choose of a set of "restriction types" to restrict the availability of your item with
certain conditions.
{% endblocktrans %}</p>
<form action="" method="post">
{% csrf_token %}
{% for set in formsets %}
<fieldset>
<legend>{{ set.title }}</legend>
<p>{{ set.description }}</p>
<div data-formset class="restriction-formset" data-formset-prefix="{{ set.formset.prefix }}">
<div data-formset-body class="panel-group collapse" id="accordion_{{ set.formset.prefix }}">
{{ set.formset.management_form }}
{% for f in set.formset %}
<div class="panel panel-default" data-formset-form>
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" data-parent="#accordion"
href="#collapse{{ f.prefix }}">
{{ set.title }}
</a>
</h4>
</div>
<div id="collapse{{ f.prefix }}" class="panel-collapse collapse in">
<div class="panel-body">
<div class="form-horizontal">
{% bootstrap_form f layout="horizontal" field_class="col-md-10" %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="panel panel-default" data-formset-form>
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapse__prefix__">
{% trans "New restriction" %}
</a>
</h4>
</div>
<div id="collapse__prefix__" class="panel-collapse collapse in">
<div class="panel-body">
<div class="form-horizontal">
{% bootstrap_form set.formset.initialized_empty_form layout="horizontal" field_class="col-md-10" %}
</div>
</div>
</div>
</div>
{% endescapescript %}
</script>
<button type="button" class="btn btn-default" data-formset-add><i
class="fa fa-plus"></i> {% trans "Add a new restriction" %}</button>
</div>
</fieldset>
{% endfor %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,6 @@
{% extends "pretixcontrol/item/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inside %}
<em>{% trans "You have to define and select propreties to be able to configure variations." %}
{% endblock %}

View File

@@ -0,0 +1,33 @@
{% extends "pretixcontrol/item/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inside %}
<form action="" method="post">
{% csrf_token %}
<table class="table">
<thead>
<tr>
<th>{{ properties.0 }}</th>
<th>{% trans "Active" %}</th>
<th>{% trans "Price" %}</th>
</tr>
</thead>
<tbody>
{% for form in forms %}
{% bootstrap_form_errors form type='all' layout='inline' %}
<tr>
<td>{{ form.values.0 }}</td>
<td>{% bootstrap_field form.active layout='inline' %}</td>
<td>{% bootstrap_field form.default_price layout='inline' %} {{ form.default_price.errors }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,49 @@
{% extends "pretixcontrol/item/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inside %}
<form action="" method="post">
{% csrf_token %}
{% for major in forms %}
{% if major.row %}
<h3>{{ major.row }}</h3>
{% endif %}
<table class="table variation-matrix">
<thead>
<tr>
<th></th>
{% for val in properties.1.values.all %}
<th>{{ val.value }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for sub in major.forms %}
<tr>
<td>{{ sub.row.value }}</td>
{% for form in sub.forms %}
<td>
<div class="row">
<div class="col-sm-5">
{% bootstrap_field form.active layout='inline' %}
</div>
<div class="col-sm-7">
{% bootstrap_field form.default_price layout='inline' %}
</div>
</div>
{{ form.default_price.errors }}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% endfor %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% block title %}{% trans "Items" %}{% endblock %}
{% block content %}
<ul class="nav nav-pills">
<li {% if "event.items" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.items' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Items" %}</a></li>
<li {% if "event.items.quotas" in url_name %}class="active"{% endif %}><a href="{% url 'control:event.items.quotas' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Quotas" %}</a></li>
<li {% if "event.items.categories" in url_name %}class="active"{% endif %}><a href="{% url 'control:event.items.categories' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Categories" %}</a></li>
<li {% if "event.items.properties" in url_name %}class="active"{% endif %}><a href="{% url 'control:event.items.properties' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Properties" %}</a></li>
<li {% if "event.items.questions" in url_name %}class="active"{% endif %}><a href="{% url 'control:event.items.questions' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Questions" %}</a></li>
</ul>
{% block inside %}
{% endblock %}
{% endblock %}

View File

@@ -0,0 +1,43 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% block title %}{% trans "Item categories" %}{% endblock %}
{% block inside %}
<h1>{% trans "Item categories" %}</h1>
{% if "updated" in request.GET %}
<div class="alert alert-success">
{% trans "Your changes have been saved." %}
</div>
{% elif "created" in request.GET %}
<div class="alert alert-success">
{% trans "A new category has been created." %}
</div>
{% elif "deleted" in request.GET %}
<div class="alert alert-success">
{% trans "The category has been deleted." %}
</div>
{% endif %}
<p>
<a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create new category" %}</a>
</p>
<table class="table table-hover">
<thead>
<tr>
<th>{% trans "Item categories" %}</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{% for c in categories %}
<tr>
<td><strong><a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.identity %}">{{ c.name }}</a></strong></td>
<td>
<a href="{% url "control:event.items.categories.up" organizer=request.event.organizer.slug event=request.event.slug category=c.identity %}" class="btn btn-default btn-sm {% if forloop.counter0 == 0 %}disabled{% endif %}"><i class="fa fa-arrow-up"></i></a>
<a href="{% url "control:event.items.categories.down" organizer=request.event.organizer.slug event=request.event.slug category=c.identity %}" class="btn btn-default btn-sm {% if forloop.revcounter0 == 0 %}disabled{% endif %}"><i class="fa fa-arrow-down"></i></a>
</td>
<td class="text-right"><a href="{% url "control:event.items.categories.delete" organizer=request.event.organizer.slug event=request.event.slug category=c.identity %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Item category" %}{% endblock %}
{% block inside %}
<h1>{% trans "Item category" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% if "success" in request.GET %}
<div class="alert alert-success">
{% trans "Your changes have been saved." %}
</div>
{% endif %}
<fieldset>
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="horizontal" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,19 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Delete item category" %}{% endblock %}
{% block inside %}
<h1>{% trans "Delete item category" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>{% blocktrans %}Are you sure you want to delete the category <strong>{{ category.name }}</strong>?{% endblocktrans %}</p>
<div class="form-group submit-group">
<a href="{% url "control:event.items.categories" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% trans "Delete" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,26 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% block title %}{% trans "Items" %}{% endblock %}
{% block inside %}
<h1>{% trans "Items" %}</h1>
<p>
<a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create new item" %}</a>
</p>
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "Item name" %}</th>
<th>{% trans "Category" %}</th>
</tr>
</thead>
<tbody>
{% for i in items %}
<tr>
<td><strong><a href="
{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.identity %}">{{ i.name }}</a></strong></td>
<td>{% if i.category %}{{ i.category }}{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@@ -0,0 +1,40 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% block title %}{% trans "Item properties" %}{% endblock %}
{% block inside %}
<h1>{% trans "Item properties" %}</h1>
{% if "updated" in request.GET %}
<div class="alert alert-success">
{% trans "Your changes have been saved." %}
</div>
{% elif "created" in request.GET %}
<div class="alert alert-success">
{% trans "A new property has been created." %}
</div>
{% elif "deleted" in request.GET %}
<div class="alert alert-success">
{% trans "The property has been deleted." %}
</div>
{% endif %}
<p>
<a href="{% url "control:event.items.properties.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create new property" %}</a>
</p>
<table class="table table-hover">
<thead>
<tr>
<th>{% trans "Item properties" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for p in properties %}
<tr>
<td><strong><a href="
{% url "control:event.items.properties.edit" organizer=request.event.organizer.slug event=request.event.slug property=p.identity %}">{{ p.name }}</a></strong></td>
<td class="text-right"><a href="
{% url "control:event.items.properties.delete" organizer=request.event.organizer.slug event=request.event.slug property=p.identity %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@@ -0,0 +1,70 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load formset_tags %}
{% block title %}{% trans "Item property" %}{% endblock %}
{% block inside %}
<h1>{% trans "Item property" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% if "success" in request.GET %}
<div class="alert alert-success">
{% trans "Your changes have been saved." %}
</div>
{% endif %}
<fieldset>
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="horizontal" %}
</fieldset>
<fieldset>
<legend>{% trans "Values" %}</legend>
<div data-formset data-formset-prefix="{{ formset.prefix }}">
<div data-formset-body>
{{ formset.management_form }}
{% for f in formset %}
<div class="form-group" data-formset-form>
{{ f.id }}
<div class="col-sm-9">
{% bootstrap_field f.value form_group_class="" layout="inline" %}
</div>
<div class="sr-only">
{% bootstrap_field f.ORDER form_group_class="" layout="inline" %}
{% bootstrap_field f.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-sm-3 text-right">
<button type="button" class="btn btn-default" data-formset-move-up-button><i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-default" data-formset-move-down-button><i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-danger" data-formset-delete-button><i class="fa fa-trash"></i></button>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="form-group" data-formset-form>
{{ formset.empty_form.id }}
<div class="col-sm-9">
{% bootstrap_field formset.empty_form.value form_group_class="" layout="inline" %}
</div>
<div class="sr-only">
{% bootstrap_field formset.empty_form.ORDER form_group_class="" layout="inline" %}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-sm-3 text-right">
<button type="button" class="btn btn-default" data-formset-move-up-button><i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-default" data-formset-move-down-button><i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-danger" data-formset-delete-button><i class="fa fa-trash"></i></button>
</div>
</div>
{% endescapescript %}
</script>
<button type="button" class="btn btn-default" data-formset-add><i class="fa fa-plus"></i> {% trans "Add a new value" %}</button>
</div>
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,28 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Delete item property" %}{% endblock %}
{% block inside %}
<h1>{% trans "Delete item property" %}</h1>
{% if not possible %}
<p>{% blocktrans %}You can not delete the property <strong>{{ property }}</strong> as long as the following items use it:{% endblocktrans %}</p>
<ul>
{% for item in dependent %}
<li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.pk %}">{{ item.name }}</a></li>
{% endfor %}
</ul>
{% else %}
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>{% blocktrans %}Are you sure you want to the property <strong>{{ property }}</strong>?{% endblocktrans %}</p>
<div class="form-group submit-group">
<a href="{% url "control:event.items.properties" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% trans "Delete" %}
</button>
</div>
</form>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,26 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Question" %}{% endblock %}
{% block inside %}
<h1>{% trans "Question" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% if "success" in request.GET %}
<div class="alert alert-success">
{% trans "Your changes have been saved." %}
</div>
{% endif %}
<fieldset>
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.question layout="horizontal" %}
{% bootstrap_field form.type layout="horizontal" %}
{% bootstrap_field form.required layout="horizontal" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,25 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Delete question" %}{% endblock %}
{% block inside %}
<h1>{% trans "Delete question" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>{% blocktrans %}Are you sure you want to the question <strong>{{ question }}</strong>?{% endblocktrans %}</p>
{% if dependent|length > 0 %}
<p>{% blocktrans %}All answers to the question given by the buyers of the following tickets will be <strong>lost</strong>.{% endblocktrans %}</p>
{% for item in dependent %}
<li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.pk %}">{{ item.name }}</a></li>
{% endfor %}
{% endif %}
<div class="form-group submit-group">
<a href="{% url "control:event.items.questions" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% trans "Delete" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,42 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% block title %}{% trans "Questions" %}{% endblock %}
{% block inside %}
<h1>{% trans "Questions" %}</h1>
{% if "updated" in request.GET %}
<div class="alert alert-success">
{% trans "Your changes have been saved." %}
</div>
{% elif "created" in request.GET %}
<div class="alert alert-success">
{% trans "A new question has been created." %}
</div>
{% elif "deleted" in request.GET %}
<div class="alert alert-success">
{% trans "The question has been deleted." %}
</div>
{% endif %}
<p>
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create new question" %}</a>
</p>
<table class="table table-hover">
<thead>
<tr>
<th>{% trans "Question" %}</th>
<th>{% trans "Type" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for q in questions %}
<tr>
<td><strong><a href="
{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=q.identity %}">{{ q.question }}</a></strong></td>
<td>{{ q.get_type_display }}</td>
<td class="text-right"><a href="
{% url "control:event.items.questions.delete" organizer=request.event.organizer.slug event=request.event.slug question=q.identity %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@@ -0,0 +1,54 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Quota" %}{% endblock %}
{% block inside %}
<h1>{% trans "Quota" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% if "success" in request.GET %}
<div class="alert alert-success">
{% trans "Your changes have been saved." %}
</div>
{% endif %}
<fieldset>
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.size layout="horizontal" %}
<legend>{% trans "Items" %}</legend>
<p>
{% blocktrans trimmed %}
Please select the items or item variations this quota should be applied to. If you apply two
quotas to the same items, it will only be available if <strong>both</strong> quotas have capacity
left.
{% endblocktrans %}
</p>
<div class="panel-group items-on-quota">
{% for item in items %}
<div class="panel panel-default" data-formset-form>
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" data-parent="#accordion"
href="#collapse{{ item.identity }}">
{{ item.name }}
</a>
</h4>
</div>
<div id="collapse{{ item.identity }}" class="panel-collapse collapse in">
<div class="panel-body">
<div class="form-horizontal">
{% bootstrap_field item.field layout="horizontal" %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,25 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Delete quota" %}{% endblock %}
{% block inside %}
<h1>{% trans "Delete quota" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>{% blocktrans %}Are you sure you want to delete the quota <strong>{{ quota }}</strong>?{% endblocktrans %}</p>
{% if dependent|length > 0 %}
<p>{% blocktrans %}The following items might be no longer available for sale:{% endblocktrans %}</p>
{% for item in dependent %}
<li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.pk %}">{{ item.name }}</a></li>
{% endfor %}
{% endif %}
<div class="form-group submit-group">
<a href="{% url "control:event.items.quotas" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% trans "Delete" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,52 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% block title %}{% trans "Quotas" %}{% endblock %}
{% block inside %}
<h1>{% trans "Quotas" %}</h1>
{% if "updated" in request.GET %}
<div class="alert alert-success">
{% trans "Your changes have been saved." %}
</div>
{% elif "created" in request.GET %}
<div class="alert alert-success">
{% trans "A new quota has been created." %}
</div>
{% elif "deleted" in request.GET %}
<div class="alert alert-success">
{% trans "The quota has been deleted." %}
</div>
{% endif %}
<p>
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new quota" %}</a>
</p>
<table class="table table-hover table-quotas">
<thead>
<tr>
<th>{% trans "Quota name" %}</th>
<th>{% trans "Items" %}</th>
<th>{% trans "Total capacity" %}</th>
<th>{% trans "Capacity left" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for q in quotas %}
<tr>
<td><strong><a href="{% url "control:event.items.quotas.edit" organizer=request.event.organizer.slug event=request.event.slug quota=q.identity %}">{{ q.name }}</a></strong></td>
<td>
<ul>
{% for item in q.items.all %}
<li><a href="
{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.identity %}"
>{{ item.name }}</a></li>
{% endfor %}
</ul>
</td>
<td>{{ q.size }}</td>
<td></td>
<td class="text-right"><a href="{% url "control:event.items.quotas.delete" organizer=request.event.organizer.slug event=request.event.slug quota=q.identity %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

View File

@@ -0,0 +1,108 @@
from django.test import TestCase, Client
from pretix.base.models import User
from pretix.base.tests import BrowserTest, on_platforms
@on_platforms()
class LoginFormBrowserTest(BrowserTest):
def setUp(self):
super().setUp()
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy@dummy.dummy', 'dummy')
def test_login(self):
self.driver.implicitly_wait(10)
self.driver.get('%s%s' % (self.live_server_url, '/control/login'))
username_input = self.driver.find_element_by_name("email")
username_input.send_keys('dummy@dummy.dummy')
password_input = self.driver.find_element_by_name("password")
password_input.send_keys('dummy')
self.driver.find_element_by_css_selector('button[type="submit"]').click()
self.driver.find_element_by_class_name("navbar-right")
def test_login_fail(self):
self.driver.implicitly_wait(10)
self.driver.get('%s%s' % (self.live_server_url, '/control/login'))
username_input = self.driver.find_element_by_name("email")
username_input.send_keys('dummy@dummy.dummy')
password_input = self.driver.find_element_by_name("password")
password_input.send_keys('wrong')
self.driver.find_element_by_css_selector('button[type="submit"]').click()
self.driver.find_element_by_class_name("alert-danger")
class LoginFormTest(TestCase):
"""
This test case tests various methods around the properties /
variations concept.
"""
def setUp(self):
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy@dummy.dummy', 'dummy')
def test_wrong_credentials(self):
c = Client()
response = c.post('/control/login', {
'email': 'dummy@dummy.dummy',
'password': 'foo',
})
self.assertEqual(response.status_code, 200)
def test_correct_credentials(self):
c = Client()
response = c.post('/control/login', {
'email': 'dummy@dummy.dummy',
'password': 'dummy',
})
self.assertEqual(response.status_code, 302)
def test_inactive_account(self):
self.user.is_active = False
self.user.save()
c = Client()
response = c.post('/control/login', {
'email': 'dummy@dummy.dummy',
'password': 'dummy',
})
self.assertEqual(response.status_code, 200)
def test_redirect(self):
c = Client()
response = c.post('/control/login?next=/control/events/', {
'email': 'dummy@dummy.dummy',
'password': 'dummy',
})
self.assertEqual(response.status_code, 302)
self.assertIn('/control/events/', response['Location'])
def test_logged_in(self):
c = Client()
response = c.post('/control/login?next=/control/events/', {
'email': 'dummy@dummy.dummy',
'password': 'dummy',
})
self.assertEqual(response.status_code, 302)
self.assertIn('/control/events/', response['Location'])
response = c.get('/control/login')
self.assertEqual(response.status_code, 302)
response = c.get('/control/login?next=/control/events/')
self.assertEqual(response.status_code, 302)
self.assertIn('/control/events/', response['Location'])
def test_logout(self):
c = Client()
response = c.post('/control/login', {
'email': 'dummy@dummy.dummy',
'password': 'dummy',
})
self.assertEqual(response.status_code, 302)
response = c.get('/control/logout')
self.assertEqual(response.status_code, 302)
response = c.get('/control/login')
self.assertEqual(response.status_code, 200)

View File

@@ -0,0 +1,61 @@
import datetime
from pretix.base.models import User, Organizer, Event, OrganizerPermission, EventPermission
from pretix.base.tests import BrowserTest, on_platforms
@on_platforms()
class EventsTest(BrowserTest):
def setUp(self):
super().setUp()
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy@dummy.dummy', 'dummy')
self.orga1 = Organizer.objects.create(name='CCC', slug='ccc')
self.orga2 = Organizer.objects.create(name='MRM', slug='mrm')
self.event1 = Event.objects.create(
organizer=self.orga1, name='30C3', slug='30c3',
date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc),
)
self.event2 = Event.objects.create(
organizer=self.orga1, name='31C3', slug='31c3',
date_from=datetime.datetime(2014, 12, 26, tzinfo=datetime.timezone.utc),
)
self.event3 = Event.objects.create(
organizer=self.orga2, name='MRMCD14', slug='mrmcd14',
date_from=datetime.datetime(2014, 9, 5, tzinfo=datetime.timezone.utc),
)
OrganizerPermission.objects.create(organizer=self.orga1, user=self.user)
EventPermission.objects.create(event=self.event1, user=self.user, can_change_items=True,
can_change_settings=True)
self.driver.implicitly_wait(10)
self.driver.get('%s%s' % (self.live_server_url, '/control/login'))
username_input = self.driver.find_element_by_name("email")
username_input.send_keys('dummy@dummy.dummy')
password_input = self.driver.find_element_by_name("password")
password_input.send_keys('dummy')
self.driver.find_element_by_css_selector('button[type="submit"]').click()
self.driver.find_element_by_class_name("navbar-right")
def test_event_list(self):
self.driver.get('%s%s' % (self.live_server_url, '/control/events/'))
tabletext = self.driver.find_element_by_css_selector(".container .table").text
self.assertIn("30C3", tabletext)
self.assertNotIn("31C3", tabletext)
self.assertNotIn("MRMCD14", tabletext)
def test_settings(self):
self.driver.get('%s/control/event/%s/%s/settings/' % (self.live_server_url, self.orga1.slug,
self.event1.slug))
self.driver.find_element_by_name("date_to").send_keys("2013-12-30 17:00:00")
self.driver.find_element_by_class_name("btn-save").click()
self.driver.find_element_by_class_name("alert-success")
self.assertIn("2013-12-30 17:00:00", self.driver.find_element_by_name("date_to").get_attribute("value"))
def test_plugins(self):
self.driver.get('%s/control/event/%s/%s/settings/plugins' % (self.live_server_url, self.orga1.slug,
self.event1.slug))
self.assertIn("Restriction by time", self.driver.find_element_by_class_name("form-plugins").text)
self.assertIn("Enable", self.driver.find_element_by_name("plugin:pretix.plugins.timerestriction").text)
self.driver.find_element_by_name("plugin:pretix.plugins.timerestriction").click()
self.assertIn("Disable", self.driver.find_element_by_name("plugin:pretix.plugins.timerestriction").text)
self.driver.find_element_by_name("plugin:pretix.plugins.timerestriction").click()
self.assertIn("Enable", self.driver.find_element_by_name("plugin:pretix.plugins.timerestriction").text)

View File

@@ -0,0 +1,241 @@
import os
import time
import datetime
from django.utils import unittest
from selenium.webdriver.support.select import Select
from pretix.base.models import User, Organizer, Event, OrganizerPermission, EventPermission, ItemCategory, Property, \
PropertyValue, Question, Quota, Item
from pretix.base.tests import BrowserTest, on_platforms
class ItemFormTest(BrowserTest):
def setUp(self):
super().setUp()
self.driver.set_window_size(1920, 1080)
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy@dummy.dummy', 'dummy')
self.orga1 = Organizer.objects.create(name='CCC', slug='ccc')
self.orga2 = Organizer.objects.create(name='MRM', slug='mrm')
self.event1 = Event.objects.create(
organizer=self.orga1, name='30C3', slug='30c3',
date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc),
)
OrganizerPermission.objects.create(organizer=self.orga1, user=self.user)
EventPermission.objects.create(event=self.event1, user=self.user, can_change_items=True,
can_change_settings=True)
self.driver.implicitly_wait(10)
self.driver.get('%s%s' % (self.live_server_url, '/control/login'))
username_input = self.driver.find_element_by_name("email")
username_input.send_keys('dummy@dummy.dummy')
password_input = self.driver.find_element_by_name("password")
password_input.send_keys('dummy')
self.driver.find_element_by_css_selector('button[type="submit"]').click()
self.driver.find_element_by_class_name("navbar-right")
def scroll_into_view(self, element):
"""Scroll element into view"""
y = element.location['y']
self.driver.execute_script('window.scrollTo(0, {0})'.format(y))
def scroll_and_click(self, element):
self.scroll_into_view(element)
time.sleep(0.5)
element.click()
@on_platforms()
class CategoriesTest(ItemFormTest):
def test_create(self):
self.driver.get('%s/control/event/%s/%s/categories/add' % (
self.live_server_url, self.orga1.slug, self.event1.slug
))
self.driver.find_element_by_name("name").send_keys('Entry tickets')
self.driver.find_element_by_class_name("btn-save").click()
self.driver.find_element_by_class_name("alert-success")
self.assertIn("Entry tickets", self.driver.find_element_by_css_selector(".container table").text)
def test_update(self):
c = ItemCategory.objects.create(event=self.event1, name="Entry tickets")
self.driver.get('%s/control/event/%s/%s/categories/%s/' % (
self.live_server_url, self.orga1.slug, self.event1.slug, c.identity
))
self.driver.find_element_by_name("name").clear()
self.driver.find_element_by_name("name").send_keys('T-Shirts')
self.scroll_and_click(self.driver.find_element_by_class_name("btn-save"))
self.driver.find_element_by_class_name("alert-success")
self.assertIn("T-Shirts", self.driver.find_element_by_css_selector(".container table").text)
self.assertNotIn("Entry tickets", self.driver.find_element_by_css_selector(".container table").text)
@unittest.skipIf('TRAVIS' in os.environ, 'See docstring for details.')
def test_sort(self):
"""
For unknown reasons, the first scoll_and_click() call sometimes results in the following exception
selenium.common.exceptions.ElementNotVisibleException:
Message: {"errorMessage":"Element is not currently visible and may not be manipulated", …}
This exception does not occur on either of my machines, but only when being run in Travis CI.
Raphael Michel, 2015-02-08
"""
ItemCategory.objects.create(event=self.event1, name="Entry tickets", position=0)
ItemCategory.objects.create(event=self.event1, name="T-Shirts", position=1)
self.driver.get('%s/control/event/%s/%s/categories/' % (
self.live_server_url, self.orga1.slug, self.event1.slug
))
self.assertIn("Entry tickets",
self.driver.find_element_by_css_selector("table > tbody > tr:nth-child(1)").text)
self.assertIn("T-Shirts",
self.driver.find_element_by_css_selector("table > tbody > tr:nth-child(2)").text)
self.scroll_and_click(self.driver.find_element_by_css_selector("table > tbody > tr:nth-child(1) a[href*='down']"))
time.sleep(1)
self.assertIn("Entry tickets",
self.driver.find_element_by_css_selector("table > tbody > tr:nth-child(2)").text)
self.assertIn("T-Shirts",
self.driver.find_element_by_css_selector("table > tbody > tr:nth-child(1)").text)
self.scroll_and_click(self.driver.find_element_by_css_selector("table > tbody > tr:nth-child(2) a[href*='up']"))
time.sleep(1)
self.assertIn("Entry tickets",
self.driver.find_element_by_css_selector("table > tbody > tr:nth-child(1)").text)
self.assertIn("T-Shirts",
self.driver.find_element_by_css_selector("table > tbody > tr:nth-child(2)").text)
def test_delete(self):
c = ItemCategory.objects.create(event=self.event1, name="Entry tickets")
self.driver.get('%s/control/event/%s/%s/categories/%s/delete' % (
self.live_server_url, self.orga1.slug, self.event1.slug, c.identity
))
self.driver.find_element_by_class_name("btn-danger").click()
self.driver.find_element_by_class_name("alert-success")
self.assertNotIn("Entry tickets", self.driver.find_element_by_css_selector(".container table").text)
@on_platforms()
class PropertiesTest(ItemFormTest):
def test_create(self):
self.driver.get('%s/control/event/%s/%s/properties/add' % (
self.live_server_url, self.orga1.slug, self.event1.slug
))
self.driver.find_element_by_css_selector("#id_name").send_keys('Size')
self.driver.find_element_by_name("values-0-value").send_keys('S')
self.driver.find_element_by_name("values-1-value").send_keys('M')
self.scroll_and_click(self.driver.find_element_by_class_name("btn-save"))
self.driver.find_element_by_class_name("alert-success")
self.assertIn("Size", self.driver.find_element_by_css_selector(".container table").text)
self.driver.find_element_by_partial_link_text("Size").click()
self.assertEqual("S", self.driver.find_element_by_name("values-0-value").get_attribute("value"))
self.assertEqual("M", self.driver.find_element_by_name("values-1-value").get_attribute("value"))
def test_update(self):
c = Property.objects.create(event=self.event1, name="Size")
PropertyValue.objects.create(prop=c, position=0, value="S")
PropertyValue.objects.create(prop=c, position=1, value="M")
self.driver.get('%s/control/event/%s/%s/properties/%s/' % (
self.live_server_url, self.orga1.slug, self.event1.slug, c.identity
))
self.driver.find_element_by_css_selector("#id_name").clear()
self.driver.find_element_by_css_selector("#id_name").send_keys('Color')
self.driver.find_elements_by_css_selector("div.form-group button.btn-danger")[0].click()
self.scroll_into_view(self.driver.find_element_by_name("values-1-value"))
self.driver.find_element_by_name("values-1-value").clear()
self.driver.find_element_by_name("values-1-value").send_keys('red')
self.driver.find_element_by_css_selector("button[data-formset-add]").click()
self.driver.find_element_by_name("values-2-value").send_keys('blue')
self.driver.find_element_by_class_name("btn-save").click()
self.driver.find_element_by_class_name("alert-success")
self.assertEqual("red", self.driver.find_element_by_name("values-0-value").get_attribute("value"))
self.assertEqual("blue", self.driver.find_element_by_name("values-1-value").get_attribute("value"))
def test_delete(self):
c = Property.objects.create(event=self.event1, name="Size")
self.driver.get('%s/control/event/%s/%s/properties/%s/delete' % (
self.live_server_url, self.orga1.slug, self.event1.slug, c.identity
))
self.driver.find_element_by_class_name("btn-danger").click()
self.driver.find_element_by_class_name("alert-success")
self.assertNotIn("Size", self.driver.find_element_by_css_selector(".container table").text)
@on_platforms()
class QuestionsTest(ItemFormTest):
def test_create(self):
self.driver.get('%s/control/event/%s/%s/questions/add' % (
self.live_server_url, self.orga1.slug, self.event1.slug
))
self.driver.find_element_by_name("question").send_keys('What is your shoe size?')
Select(self.driver.find_element_by_name("type")).select_by_value('N')
self.driver.find_element_by_class_name("btn-save").click()
self.driver.find_element_by_class_name("alert-success")
self.assertIn("shoe size", self.driver.find_element_by_css_selector(".container table").text)
def test_update(self):
c = Question.objects.create(event=self.event1, question="What is your shoe size?", type="N", required=True)
self.driver.get('%s/control/event/%s/%s/questions/%s/' % (
self.live_server_url, self.orga1.slug, self.event1.slug, c.identity
))
self.driver.find_element_by_name("question").clear()
self.driver.find_element_by_name("question").send_keys('How old are you?')
self.scroll_and_click(self.driver.find_element_by_class_name("btn-save"))
self.driver.find_element_by_class_name("alert-success")
self.assertIn("How old", self.driver.find_element_by_css_selector(".container table").text)
self.assertNotIn("shoe size", self.driver.find_element_by_css_selector(".container table").text)
c = Question.objects.current.get(identity=c.identity)
self.assertTrue(c.required)
def test_delete(self):
c = Question.objects.create(event=self.event1, question="What is your shoe size?", type="N", required=True)
self.driver.get('%s/control/event/%s/%s/questions/%s/delete' % (
self.live_server_url, self.orga1.slug, self.event1.slug, c.identity
))
self.driver.find_element_by_class_name("btn-danger").click()
self.driver.find_element_by_class_name("alert-success")
self.assertNotIn("shoe size", self.driver.find_element_by_css_selector(".container table").text)
@on_platforms()
class QuotaTest(ItemFormTest):
def test_create(self):
self.driver.get('%s/control/event/%s/%s/quotas/add' % (
self.live_server_url, self.orga1.slug, self.event1.slug
))
self.driver.find_element_by_name("name").send_keys('Full house')
self.driver.find_element_by_name("size").send_keys('500')
self.driver.find_element_by_class_name("btn-save").click()
self.driver.find_element_by_class_name("alert-success")
self.assertIn("Full house", self.driver.find_element_by_css_selector(".container table").text)
def test_update(self):
c = Quota.objects.create(event=self.event1, name="Full house", size=500)
item1 = Item.objects.create(event=self.event1, name="Standard")
item2 = Item.objects.create(event=self.event1, name="Business")
prop1 = Property.objects.create(event=self.event1, name="Level")
item2.properties.add(prop1)
PropertyValue.objects.create(prop=prop1, value="Silver")
PropertyValue.objects.create(prop=prop1, value="Gold")
self.driver.get('%s/control/event/%s/%s/quotas/%s/' % (
self.live_server_url, self.orga1.slug, self.event1.slug, c.identity
))
self.driver.find_element_by_name("size").clear()
self.driver.find_element_by_name("size").send_keys('350')
self.scroll_and_click(self.driver.find_element_by_css_selector('.panel-group .panel:nth-child(1) .panel-title a'))
time.sleep(1)
self.scroll_and_click(self.driver.find_element_by_name("item_%s" % item1.identity))
self.driver.find_element_by_css_selector('.panel-group .panel:nth-child(2) .panel-title a').click()
time.sleep(1)
self.scroll_and_click(self.driver.find_elements_by_css_selector("input[name=item_%s]" % item2.identity)[1])
self.scroll_and_click(self.driver.find_element_by_class_name("btn-save"))
self.driver.find_element_by_class_name("alert-success")
self.assertIn("350", self.driver.find_element_by_css_selector(".container table").text)
self.assertNotIn("500", self.driver.find_element_by_css_selector(".container table").text)
def test_delete(self):
c = Quota.objects.create(event=self.event1, name="Full house", size=500)
self.driver.get('%s/control/event/%s/%s/quotas/%s/delete' % (
self.live_server_url, self.orga1.slug, self.event1.slug, c.identity
))
self.driver.find_element_by_class_name("btn-danger").click()
self.driver.find_element_by_class_name("alert-success")
self.assertNotIn("Full house", self.driver.find_element_by_css_selector(".container table").text)

View File

@@ -0,0 +1,69 @@
from django.test import TestCase, Client
from django.utils.timezone import now
from pretix.base.models import Event, Organizer, User, EventPermission
class PermissionMiddlewareTest(TestCase):
"""
This test case tests various methods around the properties /
variations concept.
"""
def setUp(self):
o = Organizer.objects.create(name='Dummy', slug='dummy')
self.event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
date_from=now(),
)
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy@dummy.dummy', 'dummy')
def test_logged_out(self):
c = Client()
response = c.get('/control/login')
self.assertEqual(response.status_code, 200)
response = c.get('/control/events/')
self.assertEqual(response.status_code, 302)
def test_wrong_event(self):
c = Client()
response = c.post('/control/login', {
'email': 'dummy@dummy.dummy',
'password': 'dummy',
})
self.assertEqual(response.status_code, 302)
response = c.get('/control/event/dummy/dummy/settings/')
self.assertIn(response.status_code, (403, 404))
def test_wrong_event_permission(self):
EventPermission.objects.create(
event=self.event, user=self.user,
can_change_settings=False,
can_change_items=True,
)
c = Client()
response = c.post('/control/login', {
'email': 'dummy@dummy.dummy',
'password': 'dummy',
})
self.assertEqual(response.status_code, 302)
response = c.get('/control/event/dummy/dummy/settings/')
self.assertIn(response.status_code, (403, 404))
def test_correct(self):
EventPermission.objects.create(
event=self.event, user=self.user,
can_change_settings=True,
can_change_items=True,
)
c = Client()
response = c.post('/control/login', {
'email': 'dummy@dummy.dummy',
'password': 'dummy',
})
self.assertEqual(response.status_code, 302)
response = c.get('/control/event/dummy/dummy/settings/')
self.assertEqual(response.status_code, 200)

View File

@@ -0,0 +1,58 @@
from django.conf.urls import patterns, url, include
from pretix.control.views import main, event, item
urlpatterns = patterns('',)
urlpatterns += patterns(
'pretix.control.views.auth',
url(r'^logout$', 'logout', name='auth.logout'),
url(r'^login$', 'login', name='auth.login'),
)
urlpatterns += patterns(
'pretix.control.views.main',
url(r'^$', 'index', name='index'),
url(r'^events/$', main.EventList.as_view(), name='events'),
)
urlpatterns += patterns(
'pretix.control.views.event',
url(r'^event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include(
patterns(
'pretix.control.views',
url(r'^$', 'event.index', name='event.index'),
url(r'^settings/$', event.EventUpdate.as_view(), name='event.settings'),
url(r'^settings/plugins$', event.EventPlugins.as_view(), name='event.settings.plugins'),
url(r'^items/$', item.ItemList.as_view(), name='event.items'),
url(r'^items/add$', item.ItemCreate.as_view(), name='event.items.add'),
url(r'^items/(?P<item>[0-9a-f-]+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
url(r'^items/(?P<item>[0-9a-f-]+)/variations$', item.ItemVariations.as_view(),
name='event.item.variations'),
url(r'^items/(?P<item>[0-9a-f-]+)/restrictions$', item.ItemRestrictions.as_view(),
name='event.item.restrictions'),
url(r'^categories/$', item.CategoryList.as_view(), name='event.items.categories'),
url(r'^categories/(?P<category>[0-9a-f-]+)/delete$', item.CategoryDelete.as_view(),
name='event.items.categories.delete'),
url(r'^categories/(?P<category>[0-9a-f-]+)/up$', item.category_move_up, name='event.items.categories.up'),
url(r'^categories/(?P<category>[0-9a-f-]+)/down$', item.category_move_down,
name='event.items.categories.down'),
url(r'^categories/(?P<category>[0-9a-f-]+)/$', item.CategoryUpdate.as_view(),
name='event.items.categories.edit'),
url(r'^categories/add$', item.CategoryCreate.as_view(), name='event.items.categories.add'),
url(r'^questions/$', item.QuestionList.as_view(), name='event.items.questions'),
url(r'^questions/(?P<question>[0-9a-f-]+)/delete$', item.QuestionDelete.as_view(),
name='event.items.questions.delete'),
url(r'^questions/(?P<question>[0-9a-f-]+)/$', item.QuestionUpdate.as_view(),
name='event.items.questions.edit'),
url(r'^questions/add$', item.QuestionCreate.as_view(), name='event.items.questions.add'),
url(r'^properties/$', item.PropertyList.as_view(), name='event.items.properties'),
url(r'^properties/(?P<property>[0-9a-f-]+)/$', item.PropertyUpdate.as_view(),
name='event.items.properties.edit'),
url(r'^properties/(?P<property>[0-9a-f-]+)/delete$', item.PropertyDelete.as_view(),
name='event.items.properties.delete'),
url(r'^properties/add$', item.PropertyCreate.as_view(), name='event.items.properties.add'),
url(r'^quotas/$', item.QuotaList.as_view(), name='event.items.quotas'),
url(r'^quotas/(?P<quota>[0-9a-f-]+)/$', item.QuotaUpdate.as_view(), name='event.items.quotas.edit'),
url(r'^quotas/(?P<quota>[0-9a-f-]+)/delete$', item.QuotaDelete.as_view(),
name='event.items.quotas.delete'),
url(r'^quotas/add$', item.QuotaCreate.as_view(), name='event.items.quotas.add'),
)
))
)

View File

View File

@@ -0,0 +1,75 @@
from django.shortcuts import render, redirect
from django.contrib.auth.forms import AuthenticationForm as BaseAuthenticationForm
from django import forms
from django.utils.translation import ugettext as _
from django.contrib.auth import authenticate
from django.contrib.auth import login as auth_login
from django.contrib.auth import logout as auth_logout
class AuthenticationForm(BaseAuthenticationForm):
"""
The login form, providing an email and password field. The form already implements
validation for correct user data.
"""
email = forms.EmailField(label=_("E-mail address"), max_length=254)
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
username = None
error_messages = {
'invalid_login': _("Please enter a correct e-mail address and password."),
'inactive': _("This account is inactive."),
}
def __init__(self, request=None, *args, **kwargs):
self.request = request
self.user_cache = None
super(forms.Form, self).__init__(*args, **kwargs)
def clean(self):
email = self.cleaned_data.get('email')
password = self.cleaned_data.get('password')
if email and password:
self.user_cache = authenticate(identifier=email.lower(),
password=password)
if self.user_cache is None:
raise forms.ValidationError(
self.error_messages['invalid_login'],
code='invalid_login',
)
else:
self.confirm_login_allowed(self.user_cache)
return self.cleaned_data
def login(request):
"""
Render and process a most basic login form. Takes an URL as GET
parameter "next" for redirection after successful login
"""
ctx = {}
if request.user.is_authenticated():
if "next" in request.GET:
return redirect(request.GET.get("next", 'control:index'))
return redirect('control:index')
if request.method == 'POST':
form = AuthenticationForm(data=request.POST)
if form.is_valid() and form.user_cache:
auth_login(request, form.user_cache)
if "next" in request.GET:
return redirect(request.GET.get("next", 'control:index'))
return redirect('control:index')
else:
form = AuthenticationForm()
ctx['form'] = form
return render(request, 'pretixcontrol/auth/login.html', ctx)
def logout(request):
"""
Log the user out of the current session, then redirect to login page.
"""
auth_logout(request)
return redirect('control:auth.login')

View File

@@ -0,0 +1,111 @@
from django.shortcuts import render, redirect
from django.views.generic.edit import UpdateView
from django.views.generic.base import TemplateView
from django.views.generic.detail import SingleObjectMixin
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse
from pytz import common_timezones
from pretix.base.forms import VersionedModelForm
from pretix.base.models import Event
from pretix.control.permissions import EventPermissionRequiredMixin
class EventUpdateForm(VersionedModelForm):
timezone = forms.ChoiceField(
choices=((a, a) for a in common_timezones),
label=_("Default timezone"),
)
def clean_slug(self):
return self.instance.slug
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['slug'].widget.attrs['readonly'] = 'readonly'
class Meta:
model = Event
localized_fields = '__all__'
fields = [
'name',
'slug',
'locale',
'timezone',
'currency',
'date_from',
'date_to',
'show_date_to',
'show_times',
'presale_start',
'presale_end',
'payment_term_days',
'payment_term_last',
'max_items_per_order'
]
class EventUpdate(EventPermissionRequiredMixin, UpdateView):
model = Event
form_class = EventUpdateForm
template_name = 'pretixcontrol/event/settings.html'
permission = 'can_change_settings'
def get_object(self, queryset=None) -> Event:
return self.request.event
def get_success_url(self) -> str:
return reverse('control:event.settings', kwargs={
'organizer': self.get_object().organizer.slug,
'event': self.get_object().slug,
}) + '?success=true'
class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
model = Event
context_object_name = 'event'
permission = 'can_change_settings'
template_name = 'pretixcontrol/event/plugins.html'
def get_object(self, queryset=None) -> Event:
return self.request.event
def get_context_data(self, *args, **kwargs) -> dict:
from pretix.base.plugins import get_all_plugins
context = super().get_context_data(*args, **kwargs)
context['plugins'] = [p for p in get_all_plugins() if not p.name.startswith('.')]
context['plugins_active'] = self.object.get_plugins()
return context
def get(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
plugins_active = self.object.get_plugins()
for key, value in request.POST.items():
if key.startswith("plugin:"):
module = key.split(":")[1]
if value == "enable":
plugins_active.append(module)
else:
plugins_active.remove(module)
self.object.plugins = ",".join(plugins_active)
self.object.save()
return redirect(self.get_success_url())
def get_success_url(self) -> str:
return reverse('control:event.settings.plugins', kwargs={
'organizer': self.get_object().organizer.slug,
'event': self.get_object().slug,
}) + '?success=true'
def index(request, organizer, event):
return render(request, 'pretixcontrol/event/index.html', {})

View File

@@ -0,0 +1,389 @@
from itertools import product
from django import forms
from django.core.exceptions import ValidationError
from django.db import transaction
from django.forms.widgets import flatatt
from django.utils.encoding import force_text
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from pretix.base.forms import VersionedModelForm
from pretix.base.models import ItemVariation, PropertyValue, Item
class TolerantFormsetModelForm(VersionedModelForm):
"""
This is equivalent to a normal VersionedModelForm, but works around a problem that
arises when the form is used inside a FormSet with can_order=True and django-formset-js
enabled. In this configuration, even empty "extra" forms might have an ORDER value
sent and Django marks the form as empty and raises validation errors because the other
fields have not been filled.
"""
def has_changed(self) -> bool:
"""
Returns True if data differs from initial. Contrary to the default
implementation, the ORDER field is being ignored.
"""
for name, field in self.fields.items():
if name == 'ORDER' or name == 'id':
continue
prefixed_name = self.add_prefix(name)
data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
if not field.show_hidden_initial:
initial_value = self.initial.get(name, field.initial)
if callable(initial_value):
initial_value = initial_value()
else:
initial_prefixed_name = self.add_initial_prefix(name)
hidden_widget = field.hidden_widget()
try:
initial_value = field.to_python(hidden_widget.value_from_datadict(
self.data, self.files, initial_prefixed_name))
except forms.ValidationError:
# Always assume data has changed if validation fails.
self._changed_data.append(name)
continue
# We're using a private API of Django here. This is not nice, but no problem as it seems
# like this will become a public API in future Django.
if field._has_changed(initial_value, data_value):
return True
return False
class RestrictionForm(TolerantFormsetModelForm):
"""
The restriction form provides useful functionality for all forms
representing a restriction instance. To be concret, this form does
the necessary magic to make the 'variations' field work correctly
and look beautiful.
"""
def __init__(self, *args, **kwargs):
if 'item' in kwargs:
self.item = kwargs['item']
del kwargs['item']
super().__init__(*args, **kwargs)
if 'variations' in self.fields and isinstance(self.fields['variations'], VariationsField):
self.fields['variations'].set_item(self.item)
class RestrictionInlineFormset(forms.BaseInlineFormSet):
"""
This is the base class you should use for any formset you return
from a ``restriction_formset`` signal receiver that contains
RestrictionForm objects as its forms, as it correcly handles the
necessary item parameter for the RestrictionForm. While this could
be achieved with a regular formset, this also adds a
``initialized_empty_form`` method which is the only way to correctly
render a working empty form for a JavaScript-enabled restriction formset.
"""
def __init__(self, data=None, files=None, instance=None,
save_as_new=False, prefix=None, queryset=None, **kwargs):
super().__init__(
data, files, instance, save_as_new, prefix, queryset, **kwargs
)
if isinstance(self.instance, Item):
self.queryset = self.queryset.as_of().prefetch_related("variations")
def initialized_empty_form(self):
form = self.form(
auto_id=self.auto_id,
prefix=self.add_prefix('__prefix__'),
empty_permitted=True,
item=self.instance
)
self.add_fields(form, None)
return form
def _construct_form(self, i, **kwargs):
kwargs['item'] = self.instance
return super()._construct_form(i, **kwargs)
class Meta:
exclude = ['item']
class VariationsFieldRenderer(forms.widgets.CheckboxFieldRenderer):
"""
This is the default renderer for a VariationsField. Based on the choice input class
this renders a list or a matrix of checkboxes/radio buttons/...
"""
def __init__(self, name, value, attrs, choices):
self.name = name
self.value = value
self.attrs = attrs
self.choices = choices
def render(self):
"""
Outputs a grid for this set of choice fields.
"""
if len(self.choices) == 0:
raise ValueError("Can't handle empty lists")
variations = []
for key, value in self.choices:
value['key'] = key
variations.append(value)
properties = [v.prop for v in variations[0].relevant_values()]
dimension = len(properties)
id_ = self.attrs.get('id', None)
start_tag = format_html('<div class="variations" id="{0}">', id_) if id_ else '<div class="variations">'
output = [start_tag]
# TODO: This is very duplicate to pretixcontrol.views.item.ItemVariations.get_forms()
# Find a common abstraction to avoid the repetition.
if dimension == 0:
output.append(format_html('<em>{0}</em>', _("not applicable")))
elif dimension == 1:
output.append('<ul>')
for i, variation in enumerate(variations):
final_attrs = dict(
self.attrs.copy(), type=self.choice_input_class.input_type,
name=self.name, value=variation['key']
)
if variation['key'] in self.value:
final_attrs['checked'] = 'checked'
w = self.choice_input_class(
self.name, self.value, self.attrs.copy(),
(variation['key'], variation[properties[0].identity].value),
i
)
output.append(format_html('<li>{0}</li>', force_text(w)))
output.append('</ul>')
elif dimension >= 2:
# prop1 is the property on all the grid's y-axes
prop1 = properties[0]
prop1v = list(prop1.values.current.all())
# prop2 is the property on all the grid's x-axes
prop2 = properties[1]
prop2v = list(prop2.values.current.all())
def selector(values):
# Given an iterable of PropertyValue objects, this will return a
# list of their primary keys, ordered by the primary keys of the
# properties they belong to EXCEPT the value for the property prop2.
# We'll see later why we need this.
return [
v.identity for v in sorted(values, key=lambda v: v.prop.identity)
if v.prop.identity != prop2.identity
]
def sort(v):
# Given a list of variations, this will sort them by their position
# on the x-axis
return v[prop2.identity].identity
# We now iterate over the cartesian product of all the other
# properties which are NOT on the axes of the grid because we
# create one grid for any combination of them.
for gridrow in product(*[prop.values.current.all() for prop in properties[2:]]):
if len(gridrow) > 0:
output.append('<strong>')
output.append(", ".join([value.value for value in gridrow]))
output.append('</strong>')
output.append('<table class="table"><thead><tr><th></th>')
for val2 in prop2v:
output.append(format_html('<th>{0}</th>', val2.value))
output.append('</thead><tbody>')
for val1 in prop1v:
output.append(format_html('<tr><th>{0}</th>', val1.value))
# We are now inside one of the rows of the grid and have to
# select the variations to display in this row. In order to
# achieve this, we use the 'selector' lambda defined above.
# It gives us a normalized, comparable version of a set of
# PropertyValue objects. In this case, we compute the
# selector of our row as the selector of the sum of the
# values defining our grind and the value defining our row.
selection = selector(gridrow + (val1,))
# We now iterate over all variations who generate the same
# selector as 'selection'.
filtered = [v for v in variations if selector(v.relevant_values()) == selection]
for variation in sorted(filtered, key=sort):
final_attrs = dict(
self.attrs.copy(), type=self.choice_input_class.input_type,
name=self.name, value=variation['key']
)
if variation['key'] in self.value:
final_attrs['checked'] = 'checked'
output.append(format_html('<td><label><input{0} /></label></td>', flatatt(final_attrs)))
output.append('</td>')
output.append('</tbody></table>')
output.append(
('<div class="help-block"><a href="#" class="variations-select-all">{0}</a> · '
'<a href="#" class="variations-select-none">{1}</a></div></div>').format(
_("Select all"),
_("Deselect all")
)
)
return mark_safe('\n'.join(output))
class VariationsCheckboxRenderer(VariationsFieldRenderer):
"""
This is the same as VariationsFieldRenderer but with the choice input class
forced to checkboxes
"""
choice_input_class = forms.widgets.CheckboxChoiceInput
class VariationsSelectMultiple(forms.CheckboxSelectMultiple):
"""
This is the default widget for a VariationsField
"""
renderer = VariationsCheckboxRenderer
_empty_value = []
class VariationsField(forms.ModelMultipleChoiceField):
"""
This form field is intended to be used to let the user select a
variation of a certain item, for example in a restriction plugin.
As this field expects the non-standard keyword parameter ``item``
at initialization time, this is field is normally named ``variations``
and lives inside a ``pretixcontrol.views.forms.RestrictionForm``, which
does some magic to provide this parameter.
"""
def __init__(self, *args, item=None, **kwargs):
self.item = item
if 'widget' not in args or kwargs['widget'] is None:
kwargs['widget'] = VariationsSelectMultiple
super().__init__(*args, **kwargs)
def set_item(self, item: Item):
assert isinstance(item, Item)
self.item = item
self._set_choices(self._get_choices())
def _get_choices(self) -> "list[(str, VariationDict)]":
"""
We can't use a normal QuerySet as there theoretically might be
two types of variations: Some who already have a ItemVariation
object associated with them and some who don't. We therefore use
the item's ``get_all_variations`` method. In the first case, we
use the ItemVariation objects primary key as our choice, key,
in the latter case we use a string constructed from the values
(see VariationDict.key() for implementation details).
"""
if self.item is None:
return ()
variations = self.item.get_all_variations(use_cache=True)
return (
(
v['variation'].identity if 'variation' in v else v.key(),
v
) for v in variations
)
def clean(self, value: "list[int]"):
"""
At cleaning time, we have to clean up the mess we produced with our
_get_choices implementation. In the case of ItemVariation object ids
we don't to anything to them, but if one of the selected items is a
list of PropertyValue objects (see _get_choices), we need to create
a new ItemVariation object for this combination and then add this to
our list of selected items.
"""
if self.item is None:
raise ValueError(
"VariationsField object was not properly initialized. Please"
"use a pretixcontrol.views.forms.RestrictionForm form instead of"
"a plain Django ModelForm"
)
# Standard validation foo
if self.required and not value:
raise ValidationError(self.error_messages['required'], code='required')
elif not self.required and not value:
return []
if not isinstance(value, (list, tuple)):
raise ValidationError(self.error_messages['list'], code='list')
# Build up a cache of variations having an ItemVariation object
# For implementation details, see ItemVariation.get_all_variations()
# which uses a very similar method
all_variations = self.item.variations.all().prefetch_related("values")
variations_cache = {}
for var in all_variations:
key = []
for v in var.values.all():
key.append((v.prop_id, v.identity))
key = tuple(sorted(key))
variations_cache[key] = var.identity
cleaned_value = []
# Wrap this in a transaction to prevent strange database state if we
# get a ValidationError half-way through
with transaction.atomic():
for pk in value:
if ":" in pk:
# A combination of PropertyValues was given
# Hash the combination in the same way as in our cache above
key = []
for pair in pk.split(","):
key.append(tuple([i for i in pair.split(":")]))
key = tuple(sorted(key))
if key in variations_cache:
# An ItemVariation object already exists for this variation,
# so use this. (This might occur if the variation object was
# created _after_ the user loaded the form but _before_ he
# submitted it.)
cleaned_value.append(str(variations_cache[key]))
continue
# No ItemVariation present, create one!
var = ItemVariation()
var.item_id = self.item.identity
var.save()
# Add the values to the ItemVariation object
for pair in pk.split(","):
prop, value = pair.split(":")
try:
var.values.add(
PropertyValue.objects.current.get(
identity=value,
prop_id=prop
)
)
except PropertyValue.DoesNotExist:
raise ValidationError(
self.error_messages['invalid_pk_value'],
code='invalid_pk_value',
params={'pk': value},
)
variations_cache[key] = var.identity
cleaned_value.append(str(var.identity))
else:
# An ItemVariation id was given
cleaned_value.append(pk)
qs = self.item.variations.current.filter(identity__in=cleaned_value)
# Re-check for consistency
pks = set(force_text(getattr(o, "identity")) for o in qs)
for val in cleaned_value:
if force_text(val) not in pks:
raise ValidationError(
self.error_messages['invalid_choice'],
code='invalid_choice',
params={'value': val},
)
# Since this overrides the inherited ModelChoiceField.clean
# we run custom validators here
self.run_validators(cleaned_value)
return qs
choices = property(_get_choices, forms.ChoiceField._set_choices)

View File

@@ -0,0 +1,899 @@
from itertools import product
from django.db import transaction
from django.forms import BooleanField, ModelForm
from django.utils.functional import cached_property
from django.views.generic import ListView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.views.generic.base import TemplateView
from django.views.generic.detail import SingleObjectMixin
from django.core.urlresolvers import resolve, reverse
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.shortcuts import redirect
from django.forms.models import inlineformset_factory
from django.utils.translation import ugettext_lazy as _
from pretix.base.forms import VersionedModelForm
from pretix.base.models import (
Item, ItemCategory, Property, ItemVariation, PropertyValue, Question, Quota,
Versionable)
from pretix.control.permissions import EventPermissionRequiredMixin, event_permission_required
from pretix.control.views.forms import TolerantFormsetModelForm, VariationsField
from pretix.control.signals import restriction_formset
class ItemList(ListView):
model = Item
context_object_name = 'items'
template_name = 'pretixcontrol/items/index.html'
def get_queryset(self):
return Item.objects.current.filter(
event=self.request.event
).prefetch_related("category")
class CategoryForm(VersionedModelForm):
class Meta:
model = ItemCategory
localized_fields = '__all__'
fields = [
'name'
]
class CategoryDelete(EventPermissionRequiredMixin, DeleteView):
model = ItemCategory
form_class = CategoryForm
template_name = 'pretixcontrol/items/category_delete.html'
permission = 'can_change_items'
context_object_name = 'category'
def get_object(self, queryset=None) -> ItemCategory:
url = resolve(self.request.path_info)
return self.request.event.categories.current.get(
identity=url.kwargs['category']
)
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
for item in self.object.items.current.all():
# TODO: Clone!?
item.category = None
item.save()
success_url = self.get_success_url()
self.object.delete()
return HttpResponseRedirect(success_url)
def get_success_url(self) -> str:
return reverse('control:event.items.categories', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?deleted=true'
class CategoryUpdate(EventPermissionRequiredMixin, UpdateView):
model = ItemCategory
form_class = CategoryForm
template_name = 'pretixcontrol/items/category.html'
permission = 'can_change_items'
context_object_name = 'category'
def get_object(self, queryset=None) -> ItemCategory:
url = resolve(self.request.path_info)
return self.request.event.categories.current.get(
identity=url.kwargs['category']
)
def get_success_url(self) -> str:
return reverse('control:event.items.categories', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?updated=true'
class CategoryCreate(EventPermissionRequiredMixin, CreateView):
model = ItemCategory
form_class = CategoryForm
template_name = 'pretixcontrol/items/category.html'
permission = 'can_change_items'
context_object_name = 'category'
def get_success_url(self) -> str:
return reverse('control:event.items.categories', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?created=true'
def form_valid(self, form):
form.instance.event = self.request.event
return super().form_valid(form)
class CategoryList(ListView):
model = ItemCategory
context_object_name = 'categories'
template_name = 'pretixcontrol/items/categories.html'
def get_queryset(self):
return self.request.event.categories.current.all()
def category_move(request, category, up=True):
"""
This is a helper function to avoid duplicating code in category_move_up and
category_move_down. It takes a category and a direction and then tries to bring
all categories for this event in a new order.
"""
category = request.event.categories.current.get(
identity=category
)
categories = list(request.event.categories.current.order_by("position"))
index = categories.index(category)
if index != 0 and up:
categories[index - 1], categories[index] = categories[index], categories[index - 1]
elif index != len(categories) - 1 and not up:
categories[index + 1], categories[index] = categories[index], categories[index + 1]
for i, cat in enumerate(categories):
if cat.position != i:
cat.position = i
cat.save() # TODO: Clone or document sloppiness?
@event_permission_required("can_change_items")
def category_move_up(request, organizer, event, category):
category_move(request, category, up=True)
return redirect(reverse('control:event.items.categories', kwargs={
'organizer': request.event.organizer.slug,
'event': request.event.slug,
}) + '?ordered=true')
@event_permission_required("can_change_items")
def category_move_down(request, organizer, event, category):
category_move(request, category, up=False)
return redirect(reverse('control:event.items.categories', kwargs={
'organizer': request.event.organizer.slug,
'event': request.event.slug,
}) + '?ordered=true')
class PropertyList(ListView):
model = Property
context_object_name = 'properties'
template_name = 'pretixcontrol/items/properties.html'
def get_queryset(self):
return Property.objects.current.filter(
event=self.request.event
)
class PropertyForm(VersionedModelForm):
class Meta:
model = Property
localized_fields = '__all__'
fields = [
'name',
]
class PropertyValueForm(TolerantFormsetModelForm):
class Meta:
model = PropertyValue
localized_fields = '__all__'
fields = [
'value',
]
class PropertyUpdate(EventPermissionRequiredMixin, UpdateView):
model = Property
form_class = PropertyForm
template_name = 'pretixcontrol/items/property.html'
permission = 'can_change_items'
context_object_name = 'property'
def get_object(self, queryset=None) -> Property:
url = resolve(self.request.path_info)
return self.request.event.properties.current.get(
identity=url.kwargs['property']
)
def get_success_url(self) -> str:
url = resolve(self.request.path_info)
return reverse('control:event.items.properties.edit', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
'property': url.kwargs['property']
}) + '?success=true'
def get_formset(self):
formsetclass = inlineformset_factory(
Property, PropertyValue,
form=PropertyValueForm,
can_order=True,
extra=0,
)
kwargs = self.get_form_kwargs()
kwargs['queryset'] = self.object.values.current.all()
formset = formsetclass(**kwargs)
return formset
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['formset'] = self.get_formset()
return context
@transaction.atomic()
def form_valid(self, form, formset):
for f in formset.deleted_forms:
f.instance.delete()
f.instance.pk = None
for i, f in enumerate(formset.ordered_forms):
if f.instance.pk is not None:
f.instance = f.instance.clone()
f.instance.position = i
f.instance.save()
return super().form_valid(form)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form_class = self.get_form_class()
form = self.get_form(form_class)
formset = self.get_formset()
if form.is_valid() and formset.is_valid():
return self.form_valid(form, formset)
else:
return self.form_invalid(form)
class PropertyCreate(EventPermissionRequiredMixin, CreateView):
model = Property
form_class = PropertyForm
template_name = 'pretixcontrol/items/property.html'
permission = 'can_change_items'
context_object_name = 'property'
def get_success_url(self) -> str:
return reverse('control:event.items.properties', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?created=true'
def get_formset(self):
formsetclass = inlineformset_factory(
Property, PropertyValue,
form=PropertyValueForm,
can_order=True,
extra=3,
)
formset = formsetclass(**self.get_form_kwargs())
return formset
def get_context_data(self, *args, **kwargs) -> dict:
self.object = None
context = super().get_context_data(*args, **kwargs)
context['formset'] = self.get_formset()
return context
@transaction.atomic()
def form_valid(self, form, formset):
form.instance.event = self.request.event
resp = super().form_valid(form)
for i, f in enumerate(formset.ordered_forms):
f.instance.position = i
f.instance.prop = form.instance
f.instance.save()
return resp
def post(self, request, *args, **kwargs):
form_class = self.get_form_class()
form = self.get_form(form_class)
formset = self.get_formset()
if form.is_valid() and formset.is_valid():
return self.form_valid(form, formset)
else:
return self.form_invalid(form)
class PropertyDelete(EventPermissionRequiredMixin, DeleteView):
model = Property
form_class = PropertyForm
template_name = 'pretixcontrol/items/property_delete.html'
permission = 'can_change_items'
context_object_name = 'property'
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['dependent'] = self.get_object().items.current.all()
context['possible'] = self.is_allowed()
return context
def is_allowed(self) -> bool:
return self.get_object().items.current.count() == 0
def get_object(self, queryset=None) -> Property:
if not hasattr(self, 'object') or not self.object:
url = resolve(self.request.path_info)
self.object = self.request.event.properties.current.get(
identity=url.kwargs['property']
)
return self.object
def delete(self, request, *args, **kwargs):
if self.is_allowed():
success_url = self.get_success_url()
self.get_object().delete()
return HttpResponseRedirect(success_url)
else:
return HttpResponseForbidden()
def get_success_url(self) -> str:
return reverse('control:event.items.properties', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?deleted=true'
class QuestionList(ListView):
model = Question
context_object_name = 'questions'
template_name = 'pretixcontrol/items/questions.html'
def get_queryset(self):
return self.request.event.questions.current.all()
class QuestionForm(VersionedModelForm):
class Meta:
model = Question
localized_fields = '__all__'
fields = [
'question',
'type',
'required',
]
class QuestionDelete(EventPermissionRequiredMixin, DeleteView):
model = Question
template_name = 'pretixcontrol/items/question_delete.html'
permission = 'can_change_items'
context_object_name = 'question'
def get_object(self, queryset=None) -> Question:
url = resolve(self.request.path_info)
return self.request.event.questions.current.get(
identity=url.kwargs['question']
)
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['dependent'] = list(self.get_object().items.current.all())
return context
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
success_url = self.get_success_url()
self.object.delete()
return HttpResponseRedirect(success_url)
def get_success_url(self) -> str:
return reverse('control:event.items.questions', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?deleted=true'
class QuestionUpdate(EventPermissionRequiredMixin, UpdateView):
model = Question
form_class = QuestionForm
template_name = 'pretixcontrol/items/question.html'
permission = 'can_change_items'
context_object_name = 'question'
def get_object(self, queryset=None) -> Question:
url = resolve(self.request.path_info)
return self.request.event.questions.current.get(
identity=url.kwargs['question']
)
def get_success_url(self) -> str:
return reverse('control:event.items.questions', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?updated=true'
class QuestionCreate(EventPermissionRequiredMixin, CreateView):
model = Question
form_class = QuestionForm
template_name = 'pretixcontrol/items/question.html'
permission = 'can_change_items'
context_object_name = 'question'
def get_success_url(self) -> str:
return reverse('control:event.items.questions', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?created=true'
def form_valid(self, form):
form.instance.event = self.request.event
return super().form_valid(form)
class QuotaList(ListView):
model = Quota
context_object_name = 'quotas'
template_name = 'pretixcontrol/items/quotas.html'
def get_queryset(self):
return Quota.objects.current.filter(
event=self.request.event
).prefetch_related("items")
class QuotaForm(ModelForm):
def __init__(self, **kwargs):
items = kwargs['items']
del kwargs['items']
super().__init__(**kwargs)
if hasattr(self, 'instance'):
active_items = set(self.instance.items.all())
active_variations = set(self.instance.variations.all())
else:
active_items = set()
active_variations = set()
for item in items:
if len(item.properties.all()) > 0:
self.fields['item_%s' % item.identity] = VariationsField(
item, label=_("Activate for"),
required=False,
initial=active_variations
)
self.fields['item_%s' % item.identity].set_item(item)
else:
self.fields['item_%s' % item.identity] = BooleanField(
label=_("Activate"),
required=False,
initial=(item in active_items)
)
def save(self, commit=True):
if self.instance.pk is not None and isinstance(self.instance, Versionable):
if self.has_changed():
self.instance = self.instance.clone_shallow()
# TODO: order_cache, lock_cache are emptied by that but you'll have
# to rebuild them anyway
return super().save(commit)
class Meta:
model = Quota
localized_fields = '__all__'
fields = [
'name',
'size',
]
class QuotaEditorMixin:
@cached_property
def items(self) -> "List[Item]":
return list(self.request.event.items.all().prefetch_related("properties", "variations"))
def get_form(self, form_class):
if not hasattr(self, '_form'):
kwargs = self.get_form_kwargs()
kwargs['items'] = self.items
self._form = form_class(**kwargs)
return self._form
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['items'] = self.items
for item in context['items']:
item.field = self.get_form(QuotaForm)['item_%s' % item.identity]
return context
@transaction.atomic()
def form_valid(self, form):
res = super().form_valid(form)
# The following commented-out checks are not necessary as both self.object.items
# and self.object.variations can be expected empty due to the performance
# optimization of pretixbase.models.Versionable.clone_shallow()
# items = self.object.items.all()
# variations = self.object.variations.all()
self.object = form.instance
for item in self.items:
field = form.fields['item_%s' % item.identity]
data = form.cleaned_data['item_%s' % item.identity]
if isinstance(field, VariationsField):
self.object.variations.add(*data)
# for v in data:
# if v not in variations:
# self.object.variations.add(v)
# for v in variations:
# if v not in data:
# self.object.variations.remove(v)
if data: # and item not in items:
self.object.items.add(item)
# elif not data and item in items:
# self.object.items.remove(item)
return res
class QuotaCreate(EventPermissionRequiredMixin, QuotaEditorMixin, CreateView):
model = Quota
form_class = QuotaForm
template_name = 'pretixcontrol/items/quota.html'
permission = 'can_change_items'
context_object_name = 'quota'
def get_success_url(self) -> str:
return reverse('control:event.items.quotas', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?created=true'
def form_valid(self, form):
form.instance.event = self.request.event
return super().form_valid(form)
class QuotaUpdate(EventPermissionRequiredMixin, QuotaEditorMixin, UpdateView):
model = Quota
form_class = QuotaForm
template_name = 'pretixcontrol/items/quota.html'
permission = 'can_change_items'
context_object_name = 'quota'
def get_object(self, queryset=None) -> Quota:
url = resolve(self.request.path_info)
return self.request.event.quotas.current.get(
identity=url.kwargs['quota']
)
def get_success_url(self) -> str:
return reverse('control:event.items.quotas', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?updated=true'
class QuotaDelete(EventPermissionRequiredMixin, DeleteView):
model = Quota
template_name = 'pretixcontrol/items/quota_delete.html'
permission = 'can_change_items'
context_object_name = 'quota'
def get_object(self, queryset=None) -> Quota:
url = resolve(self.request.path_info)
return self.request.event.quotas.current.get(
identity=url.kwargs['quota']
)
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['dependent'] = list(self.get_object().items.current.all())
return context
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
success_url = self.get_success_url()
self.object.delete()
return HttpResponseRedirect(success_url)
def get_success_url(self) -> str:
return reverse('control:event.items.quotas', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?deleted=true'
class ItemDetailMixin(SingleObjectMixin):
model = Item
context_object_name = 'item'
def get_object(self, queryset=None) -> Item:
if not hasattr(self, 'object') or not self.object:
url = resolve(self.request.path_info)
self.item = self.request.event.items.current.get(
identity=url.kwargs['item']
)
self.object = self.item
return self.object
class ItemFormGeneral(VersionedModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['category'].queryset = self.instance.event.categories.current.all()
self.fields['properties'].queryset = self.instance.event.properties.current.all()
self.fields['questions'].queryset = self.instance.event.questions.current.all()
class Meta:
model = Item
localized_fields = '__all__'
fields = [
'category',
'name',
'active',
'short_description',
'long_description',
'default_price',
'tax_rate',
'properties',
'questions',
]
class ItemCreate(EventPermissionRequiredMixin, CreateView):
form_class = ItemFormGeneral
template_name = 'pretixcontrol/item/index.html'
permission = 'can_change_items'
def get_success_url(self) -> str:
return reverse('control:event.item', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
'item': self.object.identity,
}) + '?success=true'
def get_form_kwargs(self):
"""
Returns the keyword arguments for instantiating the form.
"""
newinst = Item(event=self.request.event)
kwargs = super().get_form_kwargs()
kwargs.update({'instance': newinst})
return kwargs
class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateView):
form_class = ItemFormGeneral
template_name = 'pretixcontrol/item/index.html'
permission = 'can_change_items'
def get_success_url(self) -> str:
return reverse('control:event.item', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
'item': self.get_object().identity,
}) + '?success=true'
class ItemVariationForm(VersionedModelForm):
class Meta:
model = ItemVariation
localized_fields = '__all__'
fields = [
'active',
'default_price',
]
class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
permission = 'can_change_items'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.item = None
def get_form(self, variation, data=None) -> ItemVariationForm:
"""
Return the dict for one given variation. Variations are expected to be
dictionaries in the format of Item.get_all_variations()
"""
# Values are all dictionary ite
values = [i[1] for i in sorted([it for it in variation.items() if it[0] != 'variation'])]
if 'variation' in variation:
form = ItemVariationForm(
data,
instance=variation['variation'],
prefix=",".join([str(i.identity) for i in values]),
)
else:
inst = ItemVariation(item=self.object)
inst.item_id = self.object.identity
inst.creation = True
form = ItemVariationForm(
data,
instance=inst,
prefix=",".join([str(i.identity) for i in values]),
)
form.values = values
return form
def get_forms(self) -> tuple:
"""
Returns one form per possible item variation. The forms are returned
twice: The first entry in the returned tuple contains a 1-, 2- or
3-dimensional list, depending on the number of properties associated
with this item (this is being used for form display), the second
contains all forms in one single list (this is used for processing).
The first, hierarchical list, is a list of dicts on all levels but the
last one, where the dict contains the two entries 'row' containing a
string describing this layer and 'forms' which contains the forms or
the next list of dicts.
"""
forms = []
forms_flat = []
variations = self.object.get_all_variations()
data = self.request.POST if self.request.method == 'POST' else None
if self.dimension == 1:
# For one-dimensional structures we just have a list of forms
for variation in variations:
form = self.get_form(variation, data)
forms.append(form)
forms_flat = forms
elif self.dimension >= 2:
# For 2 or more dimensional structures we display a list of grids
# of forms
# prop1 is the property on all the grid's y-axes
prop1 = self.properties[0]
# prop2 is the property on all the grid's x-axes
prop2 = self.properties[1]
def selector(values):
# Given an iterable of PropertyValue objects, this will return a
# list of their primary keys, ordered by the primary keys of the
# properties they belong to EXCEPT the value for the property prop2.
# We'll see later why we need this.
return [
v.identity for v in sorted(values, key=lambda v: v.prop.identity)
if v.prop.identity != prop2.identity
]
def sort(v):
# Given a list of variations, this will sort them by their position
# on the x-axis
return v[prop2.identity].identity
# We now iterate over the cartesian product of all the other
# properties which are NOT on the axes of the grid because we
# create one grid for any combination of them.
for gridrow in product(*[prop.values.current.all() for prop in self.properties[2:]]):
grids = []
for val1 in prop1.values.current.all():
formrow = []
# We are now inside one of the rows of the grid and have to
# select the variations to display in this row. In order to
# achieve this, we use the 'selector' lambda defined above.
# It gives us a normalized, comparable version of a set of
# PropertyValue objects. In this case, we compute the
# selector of our row as the selector of the sum of the
# values defining our grind and the value defining our row.
selection = selector(gridrow + (val1,))
# We now iterate over all variations who generate the same
# selector as 'selection'.
filtered = [v for v in variations if selector(v.relevant_values()) == selection]
for variation in sorted(filtered, key=sort):
form = self.get_form(variation, data)
formrow.append(form)
forms_flat.append(form)
grids.append({'row': val1, 'forms': formrow})
forms.append({'row': ", ".join([value.value for value in gridrow]), 'forms': grids})
return forms, forms_flat
def main(self, request, *args, **kwargs):
self.object = self.get_object()
self.properties = list(self.object.properties.current.all().prefetch_related("values"))
self.dimension = len(self.properties)
self.forms, self.forms_flat = self.get_forms()
def get(self, request, *args, **kwargs):
self.main(request, *args, **kwargs)
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
self.main(request, *args, **kwargs)
context = self.get_context_data(object=self.object)
with transaction.atomic():
for form in self.forms_flat:
if form.is_valid() and form.has_changed():
form.save()
if hasattr(form.instance, 'creation') and form.instance.creation:
# We need this special 'creation' field set to true in get_form
# for newly created items as cleanerversion does already set the
# primary key in its post_init hook
form.instance.values.add(*form.values)
# TODO: Redirect to success message
return self.render_to_response(context)
def get_template_names(self) -> "List[str]":
if self.dimension == 0:
return ['pretixcontrol/item/variations_0d.html']
elif self.dimension == 1:
return ['pretixcontrol/item/variations_1d.html']
elif self.dimension >= 2:
return ['pretixcontrol/item/variations_nd.html']
def get_context_data(self, **kwargs) -> dict:
context = super().get_context_data(**kwargs)
context['forms'] = self.forms
context['properties'] = self.properties
return context
class ItemRestrictions(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
permission = 'can_change_items'
template_name = 'pretixcontrol/item/restrictions.html'
def get_formsets(self):
responses = restriction_formset.send(self.object.event, item=self.object)
formsets = []
for receiver, response in responses:
response['formset'] = response['formsetclass'](
self.request.POST if self.request.method == 'POST' else None,
instance=self.object,
prefix=response['prefix'],
)
formsets.append(response)
return formsets
def main(self, request, *args, **kwargs):
self.object = self.get_object()
self.request = request
self.formsets = self.get_formsets()
def get(self, request, *args, **kwargs):
self.main(request, *args, **kwargs)
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
@transaction.atomic()
def post(self, request, *args, **kwargs):
self.main(request, *args, **kwargs)
valid = True
for f in self.formsets:
valid &= f['formset'].is_valid()
if valid:
for f in self.formsets:
for form in f['formset']:
if 'DELETE' in form.cleaned_data and form.cleaned_data['DELETE'] is True:
if form.instance.pk is None:
continue
form.instance.delete()
else:
form.instance.event = request.event
form.instance.item = self.object
form.save()
return redirect(self.get_success_url())
else:
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['formsets'] = self.formsets
return context
def get_success_url(self) -> str:
return reverse('control:event.item.restrictions', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
'item': self.object.identity
}) + '?success=true'

View File

@@ -0,0 +1,21 @@
from django.shortcuts import render
from django.views.generic import ListView
from pretix.base.models import Event
class EventList(ListView):
model = Event
context_object_name = 'events'
template_name = 'pretixcontrol/events/index.html'
def get_queryset(self):
return Event.objects.current.filter(
permitted__id__exact=self.request.user.pk
).prefetch_related(
"organizer",
)
def index(request):
return render(request, 'pretixcontrol/base.html', {})

View File

View File

@@ -0,0 +1,11 @@
from compressor.filters.base import CompilerFilter
from compressor.filters.css_default import CssAbsoluteFilter
class LessFilter(CompilerFilter):
def __init__(self, content, attrs, **kwargs):
super(LessFilter, self).__init__(content, command='lessc {infile} {outfile}', **kwargs)
def input(self, **kwargs):
content = super(LessFilter, self).input(**kwargs)
return CssAbsoluteFilter(content).input(**kwargs)

View File

@@ -0,0 +1,2 @@
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)

View File

@@ -0,0 +1,17 @@
from django.apps import AppConfig
from pretix.base.plugins import PluginType
class TestDummyApp(AppConfig):
name = 'pretix.plugins.testdummy'
verbose_name = '.testdummy'
class PretixPluginMeta:
type = PluginType.RESTRICTION
name = '.testdummy'
version = '1.0.0'
def ready(self):
from . import signals # NOQA
default_app_config = 'pretix.plugins.testdummy.TestDummyApp'

View File

View File

@@ -0,0 +1,9 @@
from django.dispatch import receiver
from pretix.base.signals import determine_availability
@receiver(determine_availability)
def availability_handler(sender, **kwargs):
kwargs['sender'] = sender
return kwargs

View File

@@ -0,0 +1,22 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
from pretix.base.plugins import PluginType
class TimeRestrictionApp(AppConfig):
name = 'pretix.plugins.timerestriction'
verbose_name = _("Time restriction")
class PretixPluginMeta:
type = PluginType.RESTRICTION
name = _("Restriction by time")
author = _("the pretix team")
version = '1.0.0'
description = _("This plugin adds the possibility to restrict the sale " +
"of a given item or variation to a certain timeframe " +
"or change its price during a certain period.")
def ready(self):
from . import signals # NOQA
default_app_config = 'pretix.plugins.timerestriction.TimeRestrictionApp'

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import pretix.base.models
import versions.models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='TimeRestriction',
fields=[
('id', models.CharField(serialize=False, primary_key=True, max_length=36)),
('identity', models.CharField(max_length=36)),
('version_start_date', models.DateTimeField()),
('version_end_date', models.DateTimeField(null=True, blank=True, default=None)),
('version_birth_date', models.DateTimeField()),
('timeframe_from', models.DateTimeField(verbose_name='Start of time frame')),
('timeframe_to', models.DateTimeField(verbose_name='End of time frame')),
('price', models.DecimalField(null=True, blank=True, verbose_name='Price in time frame', max_digits=7, decimal_places=2)),
('event', versions.models.VersionedForeignKey(to='pretixbase.Event', related_name='restrictions_timerestriction_timerestriction', verbose_name='Event')),
('item', versions.models.VersionedForeignKey(to='pretixbase.Item', blank=True, null=True, related_name='restrictions_timerestriction_timerestriction', verbose_name='Item')),
('variations', pretix.base.models.VariationsField(to='pretixbase.ItemVariation', blank=True,
verbose_name='Variations', related_name='restrictions_timerestriction_timerestriction')),
],
options={
'verbose_name': 'Restriction',
'verbose_name_plural': 'Restrictions',
'abstract': False,
},
bases=(models.Model,),
),
]

View File

@@ -0,0 +1,24 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import BaseRestriction
class TimeRestriction(BaseRestriction):
"""
This restriction makes an item or variation only available
within a given time frame. The price of the item can be modified
during this time frame.
"""
timeframe_from = models.DateTimeField(
verbose_name=_("Start of time frame"),
)
timeframe_to = models.DateTimeField(
verbose_name=_("End of time frame"),
)
price = models.DecimalField(
null=True, blank=True,
max_digits=7, decimal_places=2,
verbose_name=_("Price in time frame"),
)

View File

@@ -0,0 +1,146 @@
from django.dispatch import receiver
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.forms.models import inlineformset_factory
from pretix.base.signals import determine_availability
from pretix.base.models import Item
from pretix.control.views.forms import RestrictionInlineFormset, RestrictionForm
from pretix.control.signals import restriction_formset
from .models import TimeRestriction
@receiver(determine_availability)
def availability_handler(sender, **kwargs):
# Handle the signal's input arguments
item = kwargs['item']
variations = kwargs['variations']
cache = kwargs['cache']
context = kwargs['context'] # NOQA
# Fetch all restriction objects applied to this item
restrictions = list(TimeRestriction.objects.current.filter(
item=item,
).prefetch_related('variations'))
# If we do not know anything about this item, we are done here.
if len(restrictions) == 0:
return variations
# IMPORTANT:
# We need to make a two-level deep copy of the variations list before we
# modify it, becuase we need to to copy the dictionaries. Otherwise, we'll
# interfere with other plugins.
variations = [d.copy() for d in variations]
# The maximum validity of our cached values is the next date, one of our
# timeframe_from or tiemframe_to actions happens
def timediff(restrictions):
for r in restrictions:
if r.timeframe_from >= now():
yield (r.timeframe_from - now()).total_seconds()
if r.timeframe_to >= now():
yield (r.timeframe_to - now()).total_seconds()
try:
cache_validity = min(timediff(restrictions))
except ValueError:
# empty sequence
# If we get here, there are restrictions available but nothing will
# change about them any more. If it were not for the case of no
# restriction for the base item but restrictions for special
# variations, we could quit here with 'item not available'.
cache_validity = 3600
# Walk through all variations we are asked for
for v in variations:
# If this point is reached, there ARE time restrictions for this item
# Therefore, it is only available inside one of the timeframes, but not
# without any timeframe
available = False
price = None
# Make up some unique key for this variation
cachekey = 'timerestriction:%s:%s' % (
item.identity,
v.identify(),
)
# Fetch from cache, if available
cached = cache.get(cachekey)
if cached is not None:
v['available'] = (cached.split(":")[0] == 'True')
try:
v['price'] = float(cached.split(":")[1])
except ValueError:
v['price'] = None
continue
# Walk through all restriction objects applied to this item
for restriction in restrictions:
applied_to = list(restriction.variations.current.all())
# Only take this restriction into consideration if it
# is directly applied to this variation or if the item
# has no variations
if not v.empty() and ('variation' not in v or v['variation'] not in applied_to):
continue
if restriction.timeframe_from <= now() <= restriction.timeframe_to:
# Selling this item is currently possible
available = True
# If multiple time frames are currently active, make sure to
# get the cheapest price:
if (restriction.price is not None
and (price is None or restriction.price < price)):
price = restriction.price
v['available'] = available
v['price'] = price
cache.set(
cachekey,
'%s:%s' % (
'True' if available else 'False',
str(price) if price else ''
),
cache_validity
)
return variations
class TimeRestrictionForm(RestrictionForm):
class Meta:
model = TimeRestriction
localized_fields = '__all__'
fields = [
'variations',
'timeframe_from',
'timeframe_to',
'price',
]
@receiver(restriction_formset)
def formset_handler(sender, **kwargs):
formset = inlineformset_factory(
Item,
TimeRestriction,
formset=RestrictionInlineFormset,
form=TimeRestrictionForm,
can_order=False,
can_delete=True,
extra=0,
)
return {
'title': _('Restriction by time'),
'formsetclass': formset,
'prefix': 'timerestriction',
'description': 'If you use this restriction type, the system will only sell variations, which are covered '
'by at least one of the timeframes you define below. You can also change the price of '
'variations for within the given timeframe. Please note, that if you change the price of '
'variations here, this will overrule the price set in the "Variations" section.'
}

View File

@@ -0,0 +1,286 @@
from datetime import timedelta
from django.test import TestCase
from django.utils.timezone import now
from pretix.base.models import (
Event, Organizer, Item, Property, PropertyValue, ItemVariation
)
# Do NOT use relative imports here
from pretix.plugins.timerestriction import signals
from pretix.plugins.timerestriction.models import TimeRestriction
class TimeRestrictionTest(TestCase):
"""
This test case tests the various aspects of the time restriction
plugin
"""
def setUp(self):
o = Organizer.objects.create(name='Dummy', slug='dummy')
self.event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
date_from=now(),
)
self.item = Item.objects.create(event=self.event, name='Dummy', default_price=14)
self.property = Property.objects.create(event=self.event, name='Size')
self.value1 = PropertyValue.objects.create(prop=self.property, value='S')
self.value2 = PropertyValue.objects.create(prop=self.property, value='M')
self.value3 = PropertyValue.objects.create(prop=self.property, value='L')
self.variation1 = ItemVariation.objects.create(item=self.item)
self.variation1.values.add(self.value1)
self.variation2 = ItemVariation.objects.create(item=self.item)
self.variation2.values.add(self.value2)
self.variation3 = ItemVariation.objects.create(item=self.item)
self.variation3.values.add(self.value3)
def test_nothing(self):
result = signals.availability_handler(
None, item=self.item,
variations=self.item.get_all_variations(),
context=None, cache=self.event.get_cache()
)
self.assertEqual(len(result), 1)
self.assertTrue('available' not in result[0] or result[0]['available'] is True)
def test_simple_case_available(self):
r = TimeRestriction.objects.create(
timeframe_from=now() - timedelta(days=3),
timeframe_to=now() + timedelta(days=3),
event=self.event,
price=12
)
r.item = self.item
r.save()
result = signals.availability_handler(
self.event, item=self.item,
variations=self.item.get_all_variations(),
context=None, cache=self.event.get_cache()
)
self.assertEqual(len(result), 1)
self.assertIn('available', result[0])
self.assertTrue(result[0]['available'])
self.assertEqual(result[0]['price'], 12)
def test_cached_result(self):
r = TimeRestriction.objects.create(
timeframe_from=now() - timedelta(days=3),
timeframe_to=now() + timedelta(days=3),
event=self.event,
price=12
)
r.item = self.item
r.save()
result = signals.availability_handler(
self.event, item=self.item,
variations=self.item.get_all_variations(),
context=None, cache=self.event.get_cache()
)
self.assertEqual(len(result), 1)
self.assertIn('available', result[0])
self.assertTrue(result[0]['available'])
self.assertEqual(result[0]['price'], 12)
result = signals.availability_handler(
self.event, item=self.item,
variations=self.item.get_all_variations(),
context=None, cache=self.event.get_cache()
)
self.assertEqual(len(result), 1)
self.assertIn('available', result[0])
self.assertTrue(result[0]['available'])
self.assertEqual(result[0]['price'], 12)
def test_simple_case_unavailable(self):
r = TimeRestriction.objects.create(
timeframe_from=now() - timedelta(days=5),
timeframe_to=now() - timedelta(days=3),
event=self.event,
price=12
)
r.item = self.item
r.save()
result = signals.availability_handler(
self.event, item=self.item,
variations=self.item.get_all_variations(),
context=None, cache=self.event.get_cache()
)
self.assertEqual(len(result), 1)
self.assertIn('available', result[0])
self.assertFalse(result[0]['available'])
def test_multiple_overlapping_now(self):
r1 = TimeRestriction.objects.create(
timeframe_from=now() - timedelta(days=5),
timeframe_to=now() + timedelta(days=3),
event=self.event,
price=12
)
r1.item = self.item
r1.save()
r2 = TimeRestriction.objects.create(
timeframe_from=now() - timedelta(days=3),
timeframe_to=now() + timedelta(days=5),
event=self.event,
price=8
)
r2.item = self.item
r2.save()
result = signals.availability_handler(
self.event, item=self.item,
variations=self.item.get_all_variations(),
context=None, cache=self.event.get_cache()
)
self.assertEqual(len(result), 1)
self.assertIn('available', result[0])
self.assertTrue(result[0]['available'])
self.assertEqual(result[0]['price'], 8)
def test_multiple_overlapping_tomorrow(self):
r1 = TimeRestriction.objects.create(
timeframe_from=now() - timedelta(days=5),
timeframe_to=now() + timedelta(days=5),
event=self.event,
price=12
)
r1.item = self.item
r1.save()
r2 = TimeRestriction.objects.create(
timeframe_from=now() + timedelta(days=1),
timeframe_to=now() + timedelta(days=7),
event=self.event,
price=8
)
r2.item = self.item
r2.save()
result = signals.availability_handler(
self.event, item=self.item,
variations=self.item.get_all_variations(),
context=None, cache=self.event.get_cache()
)
self.assertEqual(len(result), 1)
self.assertIn('available', result[0])
self.assertTrue(result[0]['available'])
self.assertEqual(result[0]['price'], 12)
def test_multiple_distinct_available(self):
r1 = TimeRestriction.objects.create(
timeframe_from=now() - timedelta(days=5),
timeframe_to=now() + timedelta(days=2),
event=self.event,
price=12
)
r1.item = self.item
r1.save()
r2 = TimeRestriction.objects.create(
timeframe_from=now() + timedelta(days=4),
timeframe_to=now() + timedelta(days=7),
event=self.event,
price=8
)
r2.item = self.item
r2.save()
result = signals.availability_handler(
self.event, item=self.item,
variations=self.item.get_all_variations(),
context=None, cache=self.event.get_cache()
)
self.assertEqual(len(result), 1)
self.assertIn('available', result[0])
self.assertTrue(result[0]['available'])
self.assertEqual(result[0]['price'], 12)
def test_multiple_distinct_unavailable(self):
r1 = TimeRestriction.objects.create(
timeframe_from=now() - timedelta(days=5),
timeframe_to=now() - timedelta(days=1),
event=self.event,
price=12
)
r1.item = self.item
r1.save()
r2 = TimeRestriction.objects.create(
timeframe_from=now() + timedelta(days=4),
timeframe_to=now() + timedelta(days=7),
event=self.event,
price=8
)
r2.item = self.item
r2.save()
result = signals.availability_handler(
self.event, item=self.item,
variations=self.item.get_all_variations(),
context=None, cache=self.event.get_cache()
)
self.assertEqual(len(result), 1)
self.assertIn('available', result[0])
self.assertFalse(result[0]['available'])
def test_variation_specific(self):
self.item.properties.add(self.property)
r1 = TimeRestriction.objects.create(
timeframe_from=now() - timedelta(days=5),
timeframe_to=now() + timedelta(days=1),
event=self.event,
price=12
)
r1.item = self.item
r1.save()
r1.variations.add(self.variation1)
result = signals.availability_handler(
self.event, item=self.item,
variations=self.item.get_all_variations(),
context=None, cache=self.event.get_cache()
)
self.assertEqual(len(result), 3)
for v in result:
if 'variation' in v and v['variation'].pk == self.variation1.pk:
self.assertTrue(v['available'])
self.assertEqual(v['price'], 12)
else:
self.assertFalse(v['available'])
def test_variation_specifics(self):
self.item.properties.add(self.property)
r1 = TimeRestriction.objects.create(
timeframe_from=now() - timedelta(days=5),
timeframe_to=now() + timedelta(days=1),
event=self.event,
price=12
)
r1.item = self.item
r1.save()
r1.variations.add(self.variation1)
r2 = TimeRestriction.objects.create(
timeframe_from=now() - timedelta(days=5),
timeframe_to=now() + timedelta(days=1),
event=self.event,
price=8
)
r2.item = self.item
r2.save()
r2.variations.add(self.variation1)
r3 = TimeRestriction.objects.create(
timeframe_from=now() - timedelta(days=5),
timeframe_to=now() - timedelta(days=1),
event=self.event,
price=8
)
r3.item = self.item
r3.save()
r3.variations.add(self.variation3)
result = signals.availability_handler(
self.event, item=self.item,
variations=self.item.get_all_variations(),
context=None, cache=self.event.get_cache()
)
self.assertEqual(len(result), 3)
for v in result:
if 'variation' in v and v['variation'].pk == self.variation1.pk:
self.assertTrue(v['available'])
self.assertEqual(v['price'], 8)
else:
self.assertFalse(v['available'])

View File

@@ -0,0 +1,8 @@
from django.apps import AppConfig
class PretixPresaleConfig(AppConfig):
name = 'pretix.presale'
label = 'pretixpresale'
default_app_config = 'pretix.presale.PretixPresaleConfig'

View File

@@ -0,0 +1,22 @@
from django.core.urlresolvers import resolve
from django.http import HttpResponseNotFound
from pretix.base.models import Event
class EventMiddleware:
def process_request(self, request):
url = resolve(request.path_info)
url_namespace = url.namespace
url_name = url.url_name
if url_namespace != 'presale':
return
if 'event.' in url_name and 'event' in url.kwargs:
try:
request.event = Event.objects.current.filter(
slug=url.kwargs['event'],
organizer__slug=url.kwargs['organizer'],
).select_related('organizer')[0]
except IndexError:
return HttpResponseNotFound() # TODO: Provide error message

View File

@@ -0,0 +1,3 @@
"use strict";
$(function () {
});

View File

@@ -0,0 +1,49 @@
.product-row {
border-top: 1px solid @table-border-color;
&.headline, &.simple {
border-top: 2px solid @table-border-color;
}
&:last-child {
border-bottom: 2px solid @table-border-color;
}
p:last-child {
margin-bottom: 0;
}
.input-item-count {
text-align: center;
}
.availability-box {
text-align: center;
&.gone {
color: @alert-danger-text;
}
&.unavailable {
color: @alert-warning-text;
}
}
}
.cart-row, .product-row {
padding: 10px 0;
.count form {
display: inline;
}
.price, .count {
text-align: right;
}
.price small,
.availability-box small {
display: block;
line-height: 1;
}
&.total {
border-top: 1px solid @table-border-color;
}
}
.checkout-button-row {
padding: 15px 0;
}

View File

@@ -0,0 +1,5 @@
@import "../../../../base/static/bootstrap/less/bootstrap.less";
@import "../../../../base/static/fontawesome/less/font-awesome.less";
@fa-font-path: "../../fontawesome/fonts";
@import "event.less";

View File

@@ -0,0 +1,31 @@
{% load compress %}
{% load staticfiles %}
{% load i18n %}
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}{% endblock %}{% if url_name != "event.index" %} :: {% endif %}{{ event.name }}</title>
{% compress css %}
<link rel="stylesheet" type="text/less" href="{% static "pretixpresale/less/main.less" %}" />
{% endcompress %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
<script type="text/javascript" src="{% static "js/jquery.formset.js" %}"></script>
<script type="text/javascript" src="{% static "bootstrap/dist/js/bootstrap.js" %}"></script>
{% endcompress %}
</head>
<body>
<div class="container event">
<h1>{{ event.name }} <small>{{ event.date_from|date }}{% if event.show_date_to %} {{ event.date_to|date }}{% endif %}</small></h1>
{% if messages %}
{% for message in messages %}
<div class="alert {{ message.tags }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% block content %}
{% endblock %}
</div>
</body>
</html>

View File

@@ -0,0 +1,13 @@
{% load i18n %}
{% if avail == 0 %}
<div class="col-md-2 col-xs-6 availability-box gone">
<strong>{% trans "SOLD OUT" %}</strong>
</div>
{% elif avail < 100 %}
<div class="col-md-2 col-xs-6 availability-box unavailable">
<strong>{% trans "Unavailable" %}</strong><br />
<small>
{% trans "This item is currently unavailable but might become available again." %}
</small>
</div>
{% endif %}

View File

@@ -0,0 +1,63 @@
{% load i18n %}
{% for line in cart.positions %}
<div class="row-fluid cart-row">
<div class="col-md-4 col-xs-6">
<strong>{{ line.item }}</strong>
{% if line.variation %}
{{ line.variation }}
{% endif %}
</div>
<div class="col-md-3 col-xs-6 price">
{{ event.currency }} {{ line.price|floatformat:2 }}
</div>
<div class="col-md-2 col-xs-6 count">
{% if editable %}
<form action="{% url "presale:event.cart.remove" event=event.slug organizer=event.organizer.slug %}"
method="post">
{% csrf_token %}
{% if line.variation %}
<input type="hidden" name="variation_{{ line.item.identity }}_{{ line.variation.identity }}"
value="1" />
{% else %}
<input type="hidden" name="item_{{ line.item.identity }}"
value="1" />
{% endif %}
<button class="btn btn-mini btn-link"><i class="fa fa-minus"></i></button>
</form>
{% endif %}
{{ line.count }}
{% if editable %}
<form action="{% url "presale:event.cart.add" event=event.slug organizer=event.organizer.slug %}"
method="post">
{% csrf_token %}
{% if line.variation %}
<input type="hidden" name="variation_{{ line.item.identity }}_{{ line.variation.identity }}"
value="1" />
{% else %}
<input type="hidden" name="item_{{ line.item.identity }}"
value="1" />
{% endif %}
<button class="btn btn-mini btn-link"><i class="fa fa-plus"></i></button>
</form>
{% endif %}
</div>
<div class="col-md-3 col-xs-6 price">
<strong>{{ event.currency }} {{ line.total|floatformat:2 }}</strong>
{% if line.item.tax_rate %}
<br /><small>{% blocktrans trimmed with rate=line.item.tax_rate %}
incl. {{ rate }}% taxes
{% endblocktrans %}</small>
{% endif %}
</div>
<div class="clearfix"></div>
</div>
{% endfor %}
<div class="row-fluid cart-row total">
<div class="col-md-4 col-xs-6">
<strong>{% trans "Total" %}</strong>
</div>
<div class="col-md-3 col-xs-6 col-md-offset-5 price">
<strong>{{ event.currency }} {{ cart.total|floatformat:2 }}</strong>
</div>
<div class="clearfix"></div>
</div>

View File

@@ -0,0 +1,110 @@
{% extends "pretixpresale/event/base.html" %}
{% load i18n %}
{% block content %}
{% if cart.positions %}
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Your cart" %}</h3>
</div>
<div class="panel-body">
{% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event editable=True %}
<div class="row-fluid">
<div class="col-md-6 col-xs-12">
{% if cart.minutes_left > 0 %}
<em>{% blocktrans trimmed with minutes=cart.minutes_left %}
The items in your cart are reserved for you for {{ minutes }} minutes.
{% endblocktrans %}</em>
{% else %}
<em>{% trans "The items in your cart are no longer reserved for you." %}</em>
{% endif %}
</div>
<div class="col-md-4 col-md-offset-2 col-xs-12">
<a class="btn btn-block btn-primary btn-lg"
href="{% url "presale:event.checkout.start" organizer=request.event.organizer.slug event=request.event.slug %}">
<i class="fa fa-shopping-cart"></i> {% trans "Proceed with checkout" %}
</a>
</div>
<div class="clearfix"></div>
</div>
</div>
</div>
{% endif %}
<form method="post"
action="{% url "presale:event.cart.add" organizer=request.event.organizer.slug event=request.event.slug %}?next={{ request.path_info|urlencode }}">
{% csrf_token %}
{% for tup in items_by_category %}
<section>
<h3>{{ tup.0.name }}</h3>
{% for item in tup.1 %}
{% if item.has_variations %}
<div class="row-fluid product-row headline">
<div class="col-md-8 col-xs-12">
<strong>{{ item.name }}</strong>
{% if item.short_description %}<p>{{ item.short_description }}</p>{% endif %}
</div>
<div class="clearfix"></div>
</div>
{% for var in item.available_variations %}
<div class="row-fluid product-row variation">
<div class="col-md-8 col-xs-12">
{{ var }}
</div>
<div class="col-md-2 col-xs-6 price">
{{ event.currency }} {{ var.price|floatformat:2 }}
{% if item.tax_rate %}
<br /><small>{% blocktrans trimmed with rate=item.tax_rate %}
incl. {{ rate }}% taxes
{% endblocktrans %}</small>
{% endif %}
</div>
{% if var.cached_availability.0 == 100 %}
<div class="col-md-2 col-xs-6 availability-box available">
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
max="{{ var.cached_availability.1 }}"
name="variation_{{ item.identity }}_{{ var.variation.identity }}">
</div>
{% else %}
{% include "pretixpresale/event/fragment_availability.html" with avail=var.cached_availability.0 %}
{% endif %}
<div class="clearfix"></div>
</div>
{% endfor %}
{% else %}
<div class="row-fluid product-row simple">
<div class="col-md-8 col-xs-12">
<strong>{{ item.name }}</strong>
{% if item.short_description %}<p>{{ item.short_description }}</p>{% endif %}
</div>
<div class="col-md-2 col-xs-6 price">
{{ event.currency }} {{ item.price|floatformat:2 }}
{% if item.tax_rate %}
<br /><small>{% blocktrans trimmed with rate=item.tax_rate %}
incl. {{ rate }}% taxes
{% endblocktrans %}</small>
{% endif %}
</div>
{% if item.cached_availability.0 == 100 %}
<div class="col-md-2 col-xs-6 availability-box available">
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
max="{{ item.cached_availability.1 }}" name="item_{{ item.identity }}">
</div>
{% else %}
{% include "pretixpresale/event/fragment_availability.html" with avail=item.cached_availability.0 %}
{% endif %}
<div class="clearfix"></div>
</div>
{% endif %}
{% endfor %}
</section>
{% endfor %}
<div class="row-fluid checkout-button-row">
<div class="col-md-4 col-md-offset-8">
<button class="btn btn-block btn-primary btn-lg">
<i class="fa fa-shopping-cart"></i> {% trans "Add to cart" %}
</button>
</div>
<div class="clearfix"></div>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

Some files were not shown because too many files have changed in this diff Show More