forked from CGM_Public/pretix_original
Restructure our python module. A lot.
This commit is contained in:
8
src/pretix/base/__init__.py
Normal file
8
src/pretix/base/__init__.py
Normal 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
134
src/pretix/base/admin.py
Normal 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
88
src/pretix/base/cache.py
Normal 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
25
src/pretix/base/forms.py
Normal 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
|
||||
107
src/pretix/base/middleware.py
Normal file
107
src/pretix/base/middleware.py
Normal 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
|
||||
456
src/pretix/base/migrations/0001_initial.py
Normal file
456
src/pretix/base/migrations/0001_initial.py
Normal 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')]),
|
||||
),
|
||||
]
|
||||
41
src/pretix/base/migrations/0002_auto_20150211_2031.py
Normal file
41
src/pretix/base/migrations/0002_auto_20150211_2031.py
Normal 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([]),
|
||||
),
|
||||
]
|
||||
24
src/pretix/base/migrations/0003_auto_20150211_2042.py
Normal file
24
src/pretix/base/migrations/0003_auto_20150211_2042.py
Normal 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',
|
||||
),
|
||||
]
|
||||
78
src/pretix/base/migrations/0004_auto_20150211_2330.py
Normal file
78
src/pretix/base/migrations/0004_auto_20150211_2330.py
Normal 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,
|
||||
),
|
||||
]
|
||||
28
src/pretix/base/migrations/0005_auto_20150212_0901.py
Normal file
28
src/pretix/base/migrations/0005_auto_20150212_0901.py
Normal 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,
|
||||
),
|
||||
]
|
||||
22
src/pretix/base/migrations/0006_auto_20150212_0908.py
Normal file
22
src/pretix/base/migrations/0006_auto_20150212_0908.py
Normal 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,
|
||||
),
|
||||
]
|
||||
22
src/pretix/base/migrations/0007_auto_20150212_0939.py
Normal file
22
src/pretix/base/migrations/0007_auto_20150212_0939.py
Normal 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,
|
||||
),
|
||||
]
|
||||
20
src/pretix/base/migrations/0008_quota_locked.py
Normal file
20
src/pretix/base/migrations/0008_quota_locked.py
Normal 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,
|
||||
),
|
||||
]
|
||||
0
src/pretix/base/migrations/__init__.py
Normal file
0
src/pretix/base/migrations/__init__.py
Normal file
1322
src/pretix/base/models.py
Normal file
1322
src/pretix/base/models.py
Normal file
File diff suppressed because it is too large
Load Diff
23
src/pretix/base/plugins.py
Normal file
23
src/pretix/base/plugins.py
Normal 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
|
||||
53
src/pretix/base/signals.py
Normal file
53
src/pretix/base/signals.py
Normal 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"]
|
||||
)
|
||||
1
src/pretix/base/static/bootstrap
Submodule
1
src/pretix/base/static/bootstrap
Submodule
Submodule src/pretix/base/static/bootstrap added at 4ed95f5fa2
1
src/pretix/base/static/fontawesome
Submodule
1
src/pretix/base/static/fontawesome
Submodule
Submodule src/pretix/base/static/fontawesome added at a65bd93d81
4
src/pretix/base/static/jquery/js/jquery-2.1.1.min.js
vendored
Normal file
4
src/pretix/base/static/jquery/js/jquery-2.1.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
110
src/pretix/base/tests/__init__.py
Normal file
110
src/pretix/base/tests/__init__.py
Normal 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()
|
||||
47
src/pretix/base/tests/test_cache.py
Normal file
47
src/pretix/base/tests/test_cache.py
Normal 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()))
|
||||
72
src/pretix/base/tests/test_middleware.py
Normal file
72
src/pretix/base/tests/test_middleware.py
Normal 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)
|
||||
294
src/pretix/base/tests/test_models.py
Normal file
294
src/pretix/base/tests/test_models.py
Normal file
@@ -0,0 +1,294 @@
|
||||
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
|
||||
)
|
||||
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))
|
||||
57
src/pretix/base/tests/test_plugins.py
Normal file
57
src/pretix/base/tests/test_plugins.py
Normal 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
93
src/pretix/base/types.py
Normal 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
|
||||
8
src/pretix/control/__init__.py
Normal file
8
src/pretix/control/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PretixControlConfig(AppConfig):
|
||||
name = 'pretix.control'
|
||||
label = 'pretixcontrol'
|
||||
|
||||
default_app_config = 'pretix.control.PretixControlConfig'
|
||||
14
src/pretix/control/context.py
Normal file
14
src/pretix/control/context.py
Normal 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
|
||||
59
src/pretix/control/middleware.py
Normal file
59
src/pretix/control/middleware.py
Normal 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."))
|
||||
0
src/pretix/control/migrations/__init__.py
Normal file
0
src/pretix/control/migrations/__init__.py
Normal file
43
src/pretix/control/permissions.py
Normal file
43
src/pretix/control/permissions.py
Normal 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)
|
||||
10
src/pretix/control/signals.py
Normal file
10
src/pretix/control/signals.py
Normal 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"]
|
||||
)
|
||||
27
src/pretix/control/static/pretixcontrol/js/ui/main.js
Normal file
27
src/pretix/control/static/pretixcontrol/js/ui/main.js
Normal 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();
|
||||
});
|
||||
28
src/pretix/control/static/pretixcontrol/less/auth.less
Normal file
28
src/pretix/control/static/pretixcontrol/less/auth.less
Normal 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;
|
||||
}
|
||||
}
|
||||
78
src/pretix/control/static/pretixcontrol/less/forms.less
Normal file
78
src/pretix/control/static/pretixcontrol/less/forms.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
4
src/pretix/control/static/pretixcontrol/less/main.less
Normal file
4
src/pretix/control/static/pretixcontrol/less/main.less
Normal 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";
|
||||
25
src/pretix/control/templates/pretixcontrol/auth/base.html
Normal file
25
src/pretix/control/templates/pretixcontrol/auth/base.html
Normal 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>
|
||||
16
src/pretix/control/templates/pretixcontrol/auth/login.html
Normal file
16
src/pretix/control/templates/pretixcontrol/auth/login.html
Normal 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 %}
|
||||
50
src/pretix/control/templates/pretixcontrol/base.html
Normal file
50
src/pretix/control/templates/pretixcontrol/base.html
Normal 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>
|
||||
21
src/pretix/control/templates/pretixcontrol/event/base.html
Normal file
21
src/pretix/control/templates/pretixcontrol/event/base.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
27
src/pretix/control/templates/pretixcontrol/events/index.html
Normal file
27
src/pretix/control/templates/pretixcontrol/events/index.html
Normal 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 %}
|
||||
28
src/pretix/control/templates/pretixcontrol/item/base.html
Normal file
28
src/pretix/control/templates/pretixcontrol/item/base.html
Normal 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 %}
|
||||
36
src/pretix/control/templates/pretixcontrol/item/index.html
Normal file
36
src/pretix/control/templates/pretixcontrol/item/index.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
14
src/pretix/control/templates/pretixcontrol/items/base.html
Normal file
14
src/pretix/control/templates/pretixcontrol/items/base.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
26
src/pretix/control/templates/pretixcontrol/items/index.html
Normal file
26
src/pretix/control/templates/pretixcontrol/items/index.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
54
src/pretix/control/templates/pretixcontrol/items/quota.html
Normal file
54
src/pretix/control/templates/pretixcontrol/items/quota.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
52
src/pretix/control/templates/pretixcontrol/items/quotas.html
Normal file
52
src/pretix/control/templates/pretixcontrol/items/quotas.html
Normal 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 %}
|
||||
0
src/pretix/control/tests/__init__.py
Normal file
0
src/pretix/control/tests/__init__.py
Normal file
108
src/pretix/control/tests/test_auth.py
Normal file
108
src/pretix/control/tests/test_auth.py
Normal 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)
|
||||
61
src/pretix/control/tests/test_events.py
Normal file
61
src/pretix/control/tests/test_events.py
Normal 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)
|
||||
241
src/pretix/control/tests/test_items.py
Normal file
241
src/pretix/control/tests/test_items.py
Normal 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)
|
||||
69
src/pretix/control/tests/test_permissions.py
Normal file
69
src/pretix/control/tests/test_permissions.py
Normal 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)
|
||||
58
src/pretix/control/urls.py
Normal file
58
src/pretix/control/urls.py
Normal 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'),
|
||||
)
|
||||
))
|
||||
)
|
||||
0
src/pretix/control/views/__init__.py
Normal file
0
src/pretix/control/views/__init__.py
Normal file
75
src/pretix/control/views/auth.py
Normal file
75
src/pretix/control/views/auth.py
Normal 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')
|
||||
111
src/pretix/control/views/event.py
Normal file
111
src/pretix/control/views/event.py
Normal 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', {})
|
||||
389
src/pretix/control/views/forms.py
Normal file
389
src/pretix/control/views/forms.py
Normal 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)
|
||||
899
src/pretix/control/views/item.py
Normal file
899
src/pretix/control/views/item.py
Normal 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'
|
||||
21
src/pretix/control/views/main.py
Normal file
21
src/pretix/control/views/main.py
Normal 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', {})
|
||||
0
src/pretix/helpers/__init__.py
Normal file
0
src/pretix/helpers/__init__.py
Normal file
11
src/pretix/helpers/lessabsolutefilter.py
Normal file
11
src/pretix/helpers/lessabsolutefilter.py
Normal 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)
|
||||
2
src/pretix/plugins/__init__.py
Normal file
2
src/pretix/plugins/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from pkgutil import extend_path
|
||||
__path__ = extend_path(__path__, __name__)
|
||||
17
src/pretix/plugins/testdummy/__init__.py
Normal file
17
src/pretix/plugins/testdummy/__init__.py
Normal 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'
|
||||
0
src/pretix/plugins/testdummy/models.py
Normal file
0
src/pretix/plugins/testdummy/models.py
Normal file
9
src/pretix/plugins/testdummy/signals.py
Normal file
9
src/pretix/plugins/testdummy/signals.py
Normal 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
|
||||
22
src/pretix/plugins/timerestriction/__init__.py
Normal file
22
src/pretix/plugins/timerestriction/__init__.py
Normal 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'
|
||||
@@ -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,),
|
||||
),
|
||||
]
|
||||
24
src/pretix/plugins/timerestriction/models.py
Normal file
24
src/pretix/plugins/timerestriction/models.py
Normal 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"),
|
||||
)
|
||||
146
src/pretix/plugins/timerestriction/signals.py
Normal file
146
src/pretix/plugins/timerestriction/signals.py
Normal 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.'
|
||||
}
|
||||
286
src/pretix/plugins/timerestriction/tests.py
Normal file
286
src/pretix/plugins/timerestriction/tests.py
Normal 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'])
|
||||
8
src/pretix/presale/__init__.py
Normal file
8
src/pretix/presale/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PretixPresaleConfig(AppConfig):
|
||||
name = 'pretix.presale'
|
||||
label = 'pretixpresale'
|
||||
|
||||
default_app_config = 'pretix.presale.PretixPresaleConfig'
|
||||
22
src/pretix/presale/middleware.py
Normal file
22
src/pretix/presale/middleware.py
Normal 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
|
||||
3
src/pretix/presale/static/pretixpresale/js/ui/main.js
Normal file
3
src/pretix/presale/static/pretixpresale/js/ui/main.js
Normal file
@@ -0,0 +1,3 @@
|
||||
"use strict";
|
||||
$(function () {
|
||||
});
|
||||
49
src/pretix/presale/static/pretixpresale/less/event.less
Normal file
49
src/pretix/presale/static/pretixpresale/less/event.less
Normal 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;
|
||||
}
|
||||
5
src/pretix/presale/static/pretixpresale/less/main.less
Normal file
5
src/pretix/presale/static/pretixpresale/less/main.less
Normal 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";
|
||||
31
src/pretix/presale/templates/pretixpresale/event/base.html
Normal file
31
src/pretix/presale/templates/pretixpresale/event/base.html
Normal 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>
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
92
src/pretix/presale/templates/pretixpresale/event/index.html
Normal file
92
src/pretix/presale/templates/pretixpresale/event/index.html
Normal file
@@ -0,0 +1,92 @@
|
||||
{% 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>
|
||||
</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 %}
|
||||
3
src/pretix/presale/tests.py
Normal file
3
src/pretix/presale/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
16
src/pretix/presale/urls.py
Normal file
16
src/pretix/presale/urls.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from django.conf.urls import patterns, url, include
|
||||
|
||||
import pretix.presale.views.event
|
||||
import pretix.presale.views.cart
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include(
|
||||
patterns(
|
||||
'pretix.presale.views.event',
|
||||
url(r'^$', pretix.presale.views.event.EventIndex.as_view(), name='event.index'),
|
||||
url(r'^cart/add$', pretix.presale.views.cart.CartAdd.as_view(), name='event.cart.add'),
|
||||
url(r'^cart/remove$', pretix.presale.views.cart.CartRemove.as_view(), name='event.cart.remove'),
|
||||
)
|
||||
)),
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user