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

View File

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

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

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