Change data model to versioned tables

This commit is contained in:
Raphael Michel
2015-01-06 16:38:15 +01:00
parent 02c0fcbd1e
commit 3ec770a73a
48 changed files with 267 additions and 1302 deletions

View File

@@ -3,6 +3,7 @@ Django>=1.7
pytz pytz
django-bootstrap3 django-bootstrap3
-e git+https://github.com/tixl/django-formset-js.git@master#egg=django-formset-js -e git+https://github.com/tixl/django-formset-js.git@master#egg=django-formset-js
-e git+https://github.com/tixl/cleanerversion.git@tixl#egg=cleanerversion
# Deployment / static file compilation requirements # Deployment / static file compilation requirements
django-compressor django-compressor

View File

@@ -22,7 +22,7 @@ class EventRelatedCache:
def __init__(self, event: Event, cache: str='default'): def __init__(self, event: Event, cache: str='default'):
self.cache = caches[cache] self.cache = caches[cache]
self.event = event self.event = event
self.prefixkey = 'event:%d' % self.event.pk self.prefixkey = 'event:%s' % self.event.pk
def _prefix_key(self, original_key: str) -> str: def _prefix_key(self, original_key: str) -> str:
# Race conditions can happen here, but should be very very rare. # Race conditions can happen here, but should be very very rare.
@@ -34,7 +34,7 @@ class EventRelatedCache:
if prefix is None: if prefix is None:
prefix = int(time.time()) prefix = int(time.time())
self.cache.set(self.prefixkey, prefix) self.cache.set(self.prefixkey, prefix)
key = 'event:%d:%d:%s' % (self.event.pk, prefix, original_key) key = 'event:%s:%d:%s' % (self.event.pk, prefix, original_key)
if len(key) > 200: # Hash long keys, as memcached has a length limit if len(key) > 200: # Hash long keys, as memcached has a length limit
# TODO: Use a more efficient, non-cryptographic hash algorithm # TODO: Use a more efficient, non-cryptographic hash algorithm
key = hashlib.sha256(key.encode("UTF-8")).hexdigest() key = hashlib.sha256(key.encode("UTF-8")).hexdigest()

15
src/tixlbase/forms.py Normal file
View File

@@ -0,0 +1,15 @@
from django.forms.models import ModelFormMetaclass, BaseModelForm
from django.utils import six
from versions.models import Versionable
class VersionedBaseModelForm(BaseModelForm):
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()
super().save(commit)
class VersionedModelForm(six.with_metaclass(ModelFormMetaclass, VersionedBaseModelForm)):
pass

View File

@@ -30,7 +30,7 @@ class LocaleMiddleware(BaseLocaleMiddleware):
url = resolve(request.path_info) url = resolve(request.path_info)
if 'event' in url.kwargs and 'organizer' in url.kwargs: if 'event' in url.kwargs and 'organizer' in url.kwargs:
try: try:
request.event = Event.objects.get( request.event = Event.objects.current.get(
slug=url.kwargs['event'], slug=url.kwargs['event'],
organizer__slug=url.kwargs['organizer'], organizer__slug=url.kwargs['organizer'],
) )

View File

@@ -1,101 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.utils.timezone
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('auth', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)),
('password', models.CharField(verbose_name='password', max_length=128)),
('last_login', models.DateTimeField(verbose_name='last login', default=django.utils.timezone.now)),
('is_superuser', models.BooleanField(verbose_name='superuser status', default=False, help_text='Designates that this user has all permissions without explicitly assigning them.')),
('identifier', models.CharField(unique=True, max_length=255)),
('username', models.CharField(max_length=120)),
('email', models.EmailField(blank=True, null=True, db_index=True, max_length=75)),
('is_active', models.BooleanField(default=True)),
('is_staff', models.BooleanField(default=False)),
('date_joined', models.DateTimeField(auto_now_add=True)),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Event',
fields=[
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)),
('name', models.CharField(max_length=200)),
('slug', models.CharField(db_index=True, max_length=50)),
('locale', models.CharField(max_length=10)),
('currency', models.CharField(max_length=10)),
('date_from', models.DateTimeField()),
('date_to', models.DateTimeField(blank=True, null=True)),
('show_date_to', models.BooleanField(default=True)),
('show_times', models.BooleanField(default=True)),
('presale_end', models.DateTimeField(blank=True, null=True)),
('presale_start', models.DateTimeField(blank=True, null=True)),
('payment_term_days', models.IntegerField(default=14)),
('payment_term_last', models.DateTimeField(blank=True, null=True)),
],
options={
'ordering': ('date_from', 'name'),
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Organizer',
fields=[
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)),
('name', models.CharField(max_length=200)),
('slug', models.CharField(unique=True, db_index=True, max_length=50)),
('owner', models.ForeignKey(blank=True, null=True, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ('name',),
},
bases=(models.Model,),
),
migrations.AddField(
model_name='event',
name='organizer',
field=models.ForeignKey(to='tixlbase.Organizer', related_name='events'),
preserve_default=True,
),
migrations.AlterUniqueTogether(
name='event',
unique_together=set([('organizer', 'slug')]),
),
migrations.AddField(
model_name='user',
name='event',
field=models.ForeignKey(to='tixlbase.Event', blank=True, null=True, related_name='users'),
preserve_default=True,
),
migrations.AddField(
model_name='user',
name='groups',
field=models.ManyToManyField(verbose_name='groups', related_name='user_set', related_query_name='user', blank=True, to='auth.Group', help_text='The groups this user belongs to. A user will get all permissions granted to each of his/her group.'),
preserve_default=True,
),
migrations.AddField(
model_name='user',
name='user_permissions',
field=models.ManyToManyField(verbose_name='user permissions', related_name='user_set', related_query_name='user', blank=True, to='auth.Permission', help_text='Specific permissions for this user.'),
preserve_default=True,
),
migrations.AlterUniqueTogether(
name='user',
unique_together=set([('event', 'username')]),
),
]

View File

@@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('tixlbase', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='user',
name='familyname',
field=models.CharField(blank=True, max_length=255, null=True),
preserve_default=True,
),
migrations.AddField(
model_name='user',
name='givenname',
field=models.CharField(blank=True, max_length=255, null=True),
preserve_default=True,
),
]

View File

@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('tixlbase', '0002_auto_20140910_1628'),
]
operations = [
migrations.AlterField(
model_name='user',
name='username',
field=models.CharField(blank=True, max_length=120, null=True, help_text='Letters, digits and @/./+/-/_ only.'),
),
]

View File

@@ -1,61 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tixlbase', '0003_auto_20140910_1649'),
]
operations = [
migrations.CreateModel(
name='OrganizerPermission',
fields=[
('id', models.AutoField(auto_created=True, verbose_name='ID', serialize=False, primary_key=True)),
('can_create_events', models.BooleanField(default=True)),
('organizer', models.ForeignKey(to='tixlbase.Organizer', related_name='perms')),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='organizer_perms')),
],
options={
},
bases=(models.Model,),
),
migrations.AlterUniqueTogether(
name='organizerpermission',
unique_together=set([('organizer', 'user')]),
),
migrations.RemoveField(
model_name='organizer',
name='owner',
),
migrations.AlterField(
model_name='event',
name='organizer',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='events', to='tixlbase.Organizer'),
),
migrations.AlterField(
model_name='user',
name='email',
field=models.EmailField(null=True, blank=True, db_index=True, verbose_name='E-mail', max_length=75),
),
migrations.AlterField(
model_name='user',
name='event',
field=models.ForeignKey(null=True, blank=True, on_delete=django.db.models.deletion.PROTECT, related_name='users', to='tixlbase.Event'),
),
migrations.AlterField(
model_name='user',
name='familyname',
field=models.CharField(null=True, blank=True, verbose_name='Family name', max_length=255),
),
migrations.AlterField(
model_name='user',
name='givenname',
field=models.CharField(null=True, blank=True, verbose_name='Given name', max_length=255),
),
]

View File

@@ -1,48 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('tixlbase', '0004_auto_20140911_2037'),
]
operations = [
migrations.CreateModel(
name='EventPermission',
fields=[
('id', models.AutoField(auto_created=True, serialize=False, primary_key=True, verbose_name='ID')),
('can_change_settings', models.BooleanField(default=True)),
('organizer', models.ForeignKey(to='tixlbase.Event')),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_perms')),
],
options={
},
bases=(models.Model,),
),
migrations.AlterUniqueTogether(
name='eventpermission',
unique_together=set([('organizer', 'user')]),
),
migrations.AddField(
model_name='event',
name='permitted',
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL, related_name='events', through='tixlbase.EventPermission'),
preserve_default=True,
),
migrations.AddField(
model_name='organizer',
name='permitted',
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL, related_name='organizers', through='tixlbase.OrganizerPermission'),
preserve_default=True,
),
migrations.AlterField(
model_name='organizerpermission',
name='organizer',
field=models.ForeignKey(to='tixlbase.Organizer'),
),
]

View File

@@ -1,138 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('tixlbase', '0005_auto_20140911_2052'),
]
operations = [
migrations.AlterModelOptions(
name='event',
options={'verbose_name_plural': 'Events', 'verbose_name': 'Event', 'ordering': ('date_from', 'name')},
),
migrations.AlterModelOptions(
name='eventpermission',
options={'verbose_name_plural': 'Event permissions', 'verbose_name': 'Event permission'},
),
migrations.AlterModelOptions(
name='organizer',
options={'verbose_name_plural': 'Organizers', 'verbose_name': 'Organizer', 'ordering': ('name',)},
),
migrations.AlterModelOptions(
name='organizerpermission',
options={'verbose_name_plural': 'Organizer permissions', 'verbose_name': 'Organizer permission'},
),
migrations.AlterModelOptions(
name='user',
options={'verbose_name_plural': 'Users', 'verbose_name': 'User'},
),
migrations.RenameField(
model_name='eventpermission',
old_name='organizer',
new_name='event',
),
migrations.AlterField(
model_name='event',
name='currency',
field=models.CharField(max_length=10, verbose_name='Default currency'),
),
migrations.AlterField(
model_name='event',
name='date_from',
field=models.DateTimeField(verbose_name='Event start time'),
),
migrations.AlterField(
model_name='event',
name='date_to',
field=models.DateTimeField(blank=True, null=True, verbose_name='Event end time'),
),
migrations.AlterField(
model_name='event',
name='locale',
field=models.CharField(max_length=10, verbose_name='Default locale', choices=[('de', 'German'), ('en', 'English')]),
),
migrations.AlterField(
model_name='event',
name='name',
field=models.CharField(max_length=200, verbose_name='Name'),
),
migrations.AlterField(
model_name='event',
name='payment_term_days',
field=models.IntegerField(verbose_name='Payment term in days', default=14),
),
migrations.AlterField(
model_name='event',
name='payment_term_last',
field=models.DateTimeField(blank=True, null=True, verbose_name='Last date of payments'),
),
migrations.AlterField(
model_name='event',
name='presale_end',
field=models.DateTimeField(blank=True, null=True, verbose_name='End of presale'),
),
migrations.AlterField(
model_name='event',
name='presale_start',
field=models.DateTimeField(blank=True, null=True, verbose_name='Start of presale'),
),
migrations.AlterField(
model_name='event',
name='show_date_to',
field=models.BooleanField(verbose_name='Show event end date', default=True),
),
migrations.AlterField(
model_name='event',
name='show_times',
field=models.BooleanField(verbose_name='Show dates with time', default=True),
),
migrations.AlterField(
model_name='event',
name='slug',
field=models.CharField(db_index=True, max_length=50, verbose_name='Slug'),
),
migrations.AlterField(
model_name='eventpermission',
name='can_change_settings',
field=models.BooleanField(verbose_name='Can change event settings', default=True),
),
migrations.AlterField(
model_name='organizer',
name='name',
field=models.CharField(max_length=200, verbose_name='Name'),
),
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.CharField(db_index=True, max_length=50, verbose_name='Slug', unique=True),
),
migrations.AlterField(
model_name='organizerpermission',
name='can_create_events',
field=models.BooleanField(verbose_name='Can create events', default=True),
),
migrations.AlterField(
model_name='user',
name='date_joined',
field=models.DateTimeField(verbose_name='Date joined', auto_now_add=True),
),
migrations.AlterField(
model_name='user',
name='is_active',
field=models.BooleanField(verbose_name='Is active', default=True),
),
migrations.AlterField(
model_name='user',
name='is_staff',
field=models.BooleanField(verbose_name='Is site admin', default=False),
),
migrations.AlterUniqueTogether(
name='eventpermission',
unique_together=set([('event', 'user')]),
),
]

View File

@@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('tixlbase', '0006_auto_20140912_1855'),
]
operations = [
migrations.AddField(
model_name='user',
name='locale',
field=models.CharField(choices=[('de', 'German'), ('en', 'English')], max_length=50, default='en'),
preserve_default=True,
),
migrations.AddField(
model_name='user',
name='timezone',
field=models.CharField(max_length=100, default='UTC'),
preserve_default=True,
),
]

View File

@@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('tixlbase', '0007_auto_20140914_1301'),
]
operations = [
migrations.AddField(
model_name='event',
name='timezone',
field=models.CharField(max_length=100, default='UTC', verbose_name='Default timezone'),
preserve_default=True,
),
migrations.AlterField(
model_name='user',
name='locale',
field=models.CharField(max_length=50, verbose_name='Language', default='en', choices=[('de', 'German'), ('en', 'English')]),
),
migrations.AlterField(
model_name='user',
name='timezone',
field=models.CharField(max_length=100, default='UTC', verbose_name='Timezone'),
),
]

View File

@@ -1,144 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.core.validators
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tixlbase', '0008_auto_20140914_1304'),
]
operations = [
migrations.CreateModel(
name='Item',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, auto_created=True, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Item name')),
('active', models.BooleanField(default=True)),
('deleted', models.BooleanField(default=False)),
('short_description', models.TextField(help_text='This is shown below the item name in lists.', blank=True, null=True, verbose_name='Short description')),
('long_description', models.TextField(blank=True, null=True, verbose_name='Long description')),
('default_price', models.DecimalField(decimal_places=2, blank=True, max_digits=7, null=True, verbose_name='Default price')),
('tax_rate', models.DecimalField(decimal_places=2, blank=True, max_digits=7, null=True, verbose_name='Included taxes in percent')),
],
options={
'verbose_name_plural': 'Items',
'verbose_name': 'Item',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='ItemCategory',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, auto_created=True, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Category name')),
('position', models.IntegerField(blank=True, null=True)),
('event', models.ForeignKey(to='tixlbase.Event')),
],
options={
'verbose_name_plural': 'Item categories',
'ordering': ('position',),
'verbose_name': 'Item category',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='ItemFlavor',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, auto_created=True, verbose_name='ID')),
('active', models.BooleanField(default=True)),
('default_price', models.DecimalField(decimal_places=2, blank=True, max_digits=7, null=True, verbose_name='Default price')),
('item', models.ForeignKey(to='tixlbase.Item', related_name='flavors')),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Property',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, auto_created=True, verbose_name='ID')),
('name', models.CharField(max_length=250, verbose_name='Property name')),
('event', models.ForeignKey(to='tixlbase.Event')),
],
options={
'verbose_name_plural': 'Item properties',
'verbose_name': 'Item property',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='PropertyValue',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, auto_created=True, verbose_name='ID')),
('value', models.CharField(max_length=250, verbose_name='Value')),
('prop', models.ForeignKey(to='tixlbase.Property', related_name='values')),
],
options={
},
bases=(models.Model,),
),
migrations.AddField(
model_name='itemflavor',
name='prop',
field=models.ManyToManyField(related_name='values', to='tixlbase.PropertyValue'),
preserve_default=True,
),
migrations.AddField(
model_name='item',
name='category',
field=models.ForeignKey(blank=True, to='tixlbase.ItemCategory', on_delete=django.db.models.deletion.PROTECT, null=True),
preserve_default=True,
),
migrations.AddField(
model_name='item',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='tixlbase.Event'),
preserve_default=True,
),
migrations.AddField(
model_name='item',
name='properties',
field=models.ManyToManyField(related_name='items', to='tixlbase.Property'),
preserve_default=True,
),
migrations.AlterField(
model_name='event',
name='payment_term_days',
field=models.IntegerField(help_text='The number of days after placing an order the user has to pay to preserve his reservation.', default=14, verbose_name='Payment term in days'),
),
migrations.AlterField(
model_name='event',
name='payment_term_last',
field=models.DateTimeField(help_text='The last date any payments are accepted. This has precedence over the number of days configured above.', blank=True, null=True, verbose_name='Last date of payments'),
),
migrations.AlterField(
model_name='event',
name='presale_end',
field=models.DateTimeField(help_text='No items will be sold after this date.', blank=True, null=True, verbose_name='End of presale'),
),
migrations.AlterField(
model_name='event',
name='presale_start',
field=models.DateTimeField(help_text='No items will be sold before this date.', blank=True, null=True, verbose_name='Start of presale'),
),
migrations.AlterField(
model_name='event',
name='show_date_to',
field=models.BooleanField(help_text="If disabled, only event's start date will be displayed to the public.", default=True, verbose_name='Show event end date'),
),
migrations.AlterField(
model_name='event',
name='show_times',
field=models.BooleanField(help_text="If disabled, the event's start and end date will be displayed without the time of day.", default=True, verbose_name='Show dates with time'),
),
migrations.AlterField(
model_name='event',
name='slug',
field=models.CharField(db_index=True, 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.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug', max_length=50),
),
]

View File

@@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('tixlbase', '0009_auto_20140916_2120'),
]
operations = [
migrations.RemoveField(
model_name='itemflavor',
name='prop',
),
migrations.AddField(
model_name='eventpermission',
name='can_change_items',
field=models.BooleanField(verbose_name='Can change item settings', default=True),
preserve_default=True,
),
migrations.AddField(
model_name='itemflavor',
name='values',
field=models.ManyToManyField(to='tixlbase.PropertyValue', related_name='flavors'),
preserve_default=True,
),
]

View File

@@ -1,49 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tixlbase', '0010_auto_20140927_1006'),
]
operations = [
migrations.CreateModel(
name='ItemVariation',
fields=[
('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)),
('active', models.BooleanField(default=True)),
('default_price', models.DecimalField(max_digits=7, decimal_places=2, blank=True, null=True, verbose_name='Default price')),
('item', models.ForeignKey(related_name='variations', to='tixlbase.Item')),
('values', models.ManyToManyField(related_name='variations', to='tixlbase.PropertyValue')),
],
options={
},
bases=(models.Model,),
),
migrations.RemoveField(
model_name='itemflavor',
name='item',
),
migrations.RemoveField(
model_name='itemflavor',
name='values',
),
migrations.DeleteModel(
name='ItemFlavor',
),
migrations.AlterField(
model_name='item',
name='category',
field=models.ForeignKey(null=True, blank=True, on_delete=django.db.models.deletion.PROTECT, related_name='items', to='tixlbase.ItemCategory'),
),
migrations.AlterField(
model_name='item',
name='event',
field=models.ForeignKey(to='tixlbase.Event', on_delete=django.db.models.deletion.PROTECT, related_name='items'),
),
]

View File

@@ -1,66 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.db.models.deletion
def setposition(apps, schema_editor):
ItemCategory = apps.get_model("tixlbase", "ItemCategory")
for cat in ItemCategory.objects.all():
cat.position = 0
cat.save()
class Migration(migrations.Migration):
dependencies = [
('tixlbase', '0011_auto_20140927_1013'),
]
operations = [
migrations.RunPython(setposition),
migrations.AlterField(
model_name='item',
name='active',
field=models.BooleanField(default=True, verbose_name='Active'),
),
migrations.AlterField(
model_name='item',
name='category',
field=models.ForeignKey(blank=True, null=True, verbose_name='Category', related_name='items', to='tixlbase.ItemCategory', on_delete=django.db.models.deletion.PROTECT),
),
migrations.AlterField(
model_name='item',
name='event',
field=models.ForeignKey(to='tixlbase.Event', related_name='items', verbose_name='Event', on_delete=django.db.models.deletion.PROTECT),
),
migrations.AlterField(
model_name='item',
name='properties',
field=models.ManyToManyField(to='tixlbase.Property', 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.", blank=True, verbose_name='Properties', related_name='items'),
),
migrations.AlterField(
model_name='item',
name='tax_rate',
field=models.DecimalField(max_digits=7, verbose_name='Taxes included in percent', blank=True, null=True, decimal_places=2),
),
migrations.AlterField(
model_name='itemcategory',
name='event',
field=models.ForeignKey(related_name='categories', to='tixlbase.Event'),
),
migrations.AlterField(
model_name='itemcategory',
name='position',
field=models.IntegerField(default=0),
),
migrations.AlterField(
model_name='itemvariation',
name='active',
field=models.BooleanField(default=True, verbose_name='Active'),
),
migrations.AlterField(
model_name='property',
name='event',
field=models.ForeignKey(related_name='properties', to='tixlbase.Event'),
),
]

View File

@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('tixlbase', '0012_auto_20140929_1935'),
]
operations = [
migrations.AddField(
model_name='propertyvalue',
name='position',
field=models.IntegerField(default=0),
preserve_default=True,
),
]

View File

@@ -1,43 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('tixlbase', '0013_propertyvalue_position'),
]
operations = [
migrations.CreateModel(
name='Question',
fields=[
('id', models.AutoField(primary_key=True, auto_created=True, verbose_name='ID', serialize=False)),
('question', models.TextField(verbose_name='Question')),
('type', models.CharField(choices=[('N', 'Number'), ('S', 'Text (one line)'), ('T', 'Multiline text'), ('B', 'Yes/No')], max_length=5, verbose_name='Question type')),
('required', models.BooleanField(default=False, verbose_name='Required question')),
('event', models.ForeignKey(to='tixlbase.Event', related_name='events')),
],
options={
'verbose_name': 'Question',
'verbose_name_plural': 'Questions',
},
bases=(models.Model,),
),
migrations.AlterModelOptions(
name='itemvariation',
options={'verbose_name': 'Item variation', 'verbose_name_plural': 'Item variations'},
),
migrations.AlterModelOptions(
name='propertyvalue',
options={'ordering': ('position',), 'verbose_name': 'Property value', 'verbose_name_plural': 'Property values'},
),
migrations.AddField(
model_name='item',
name='questions',
field=models.ManyToManyField(to='tixlbase.Question', related_name='questions', blank=True, verbose_name='Questions', help_text='The user will be asked to fill in answers for the selected questions'),
preserve_default=True,
),
]

View File

@@ -1,24 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('tixlbase', '0014_auto_20141005_1037'),
]
operations = [
migrations.AlterField(
model_name='item',
name='questions',
field=models.ManyToManyField(blank=True, related_name='items', verbose_name='Questions', help_text='The user will be asked to fill in answers for the selected questions', to='tixlbase.Question'),
),
migrations.AlterField(
model_name='question',
name='event',
field=models.ForeignKey(to='tixlbase.Event', related_name='questions'),
),
]

View File

@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('tixlbase', '0015_auto_20141006_2205'),
]
operations = [
migrations.AddField(
model_name='event',
name='plugins',
field=models.TextField(blank=True, verbose_name='Plugins', null=True),
preserve_default=True,
),
]

View File

@@ -1,125 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import tixlbase.models
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('tixlbase', '0016_event_plugins'),
]
operations = [
migrations.CreateModel(
name='CartPosition',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)),
('session', models.CharField(null=True, max_length=255, blank=True, verbose_name='Session key')),
('total', models.DecimalField(max_digits=10, verbose_name='Price', decimal_places=2)),
('datetime', models.DateTimeField(verbose_name='Datetime')),
('expires', models.DateTimeField(verbose_name='Expiration date')),
('event', models.ForeignKey(to='tixlbase.Event', verbose_name='Event')),
('item', models.ForeignKey(to='tixlbase.Item', verbose_name='Item')),
('user', models.ForeignKey(null=True, blank=True, to=settings.AUTH_USER_MODEL, verbose_name='User')),
('variation', models.ForeignKey(null=True, blank=True, to='tixlbase.ItemVariation', verbose_name='Variation')),
],
options={
'verbose_name_plural': 'Cart positions',
'verbose_name': 'Cart position',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Order',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)),
('status', models.CharField(max_length=3, choices=[('p', 'pending'), ('n', 'paid'), ('e', 'expired'), ('c', 'cancelled')], verbose_name='Status')),
('datetime', models.DateTimeField(auto_now_add=True, verbose_name='Date')),
('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(max_digits=10, verbose_name='Total amount', decimal_places=2)),
('event', models.ForeignKey(to='tixlbase.Event', verbose_name='Event')),
('user', models.ForeignKey(null=True, blank=True, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name_plural': 'Orders',
'verbose_name': 'Order',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='OrderPosition',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)),
('price', models.DecimalField(max_digits=10, verbose_name='Price', decimal_places=2)),
],
options={
'verbose_name_plural': 'Order positions',
'verbose_name': 'Order position',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='QuestionAnswer',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)),
('answer', models.TextField()),
('cartposition', models.ForeignKey(null=True, blank=True, to='tixlbase.CartPosition')),
('orderposition', models.ForeignKey(null=True, blank=True, to='tixlbase.OrderPosition')),
('question', models.ForeignKey(to='tixlbase.Question')),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Quota',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)),
('name', models.CharField(max_length=200, verbose_name='Name')),
('size', models.PositiveIntegerField(verbose_name='Total capacity')),
('items', models.ManyToManyField(to='tixlbase.Item', blank=True, verbose_name='Item')),
('lock_cache', models.ManyToManyField(to='tixlbase.CartPosition', blank=True)),
('order_cache', models.ManyToManyField(to='tixlbase.OrderPosition', blank=True)),
('variations', tixlbase.models.VariationsField(to='tixlbase.ItemVariation', blank=True, verbose_name='Variations')),
],
options={
'verbose_name_plural': 'Quotas',
'verbose_name': 'Quota',
},
bases=(models.Model,),
),
migrations.AddField(
model_name='orderposition',
name='answers',
field=models.ManyToManyField(to='tixlbase.Question', through='tixlbase.QuestionAnswer', verbose_name='Answers'),
preserve_default=True,
),
migrations.AddField(
model_name='orderposition',
name='item',
field=models.ForeignKey(to='tixlbase.Item', verbose_name='Item'),
preserve_default=True,
),
migrations.AddField(
model_name='orderposition',
name='order',
field=models.ForeignKey(to='tixlbase.Order', verbose_name='Order'),
preserve_default=True,
),
migrations.AddField(
model_name='orderposition',
name='variation',
field=models.ForeignKey(null=True, blank=True, to='tixlbase.ItemVariation', verbose_name='Variation'),
preserve_default=True,
),
migrations.AlterField(
model_name='event',
name='payment_term_days',
field=models.PositiveIntegerField(verbose_name='Payment term in days', default=14, help_text='The number of days after placing an order the user has to pay to preserve his reservation.'),
),
]

View File

@@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('tixlbase', '0017_auto_20141017_2148'),
]
operations = [
migrations.AddField(
model_name='quota',
name='event',
field=models.ForeignKey(to='tixlbase.Event', default=1, verbose_name='Event', related_name='quotas'),
preserve_default=False,
),
migrations.AlterField(
model_name='cartposition',
name='datetime',
field=models.DateTimeField(verbose_name='Date'),
),
]

View File

@@ -6,6 +6,7 @@ from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, Permis
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.template.defaultfilters import date as _date from django.template.defaultfilters import date as _date
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from versions.models import Versionable, VersionedForeignKey, VersionedManyToManyField
from tixlbase.types import VariationDict from tixlbase.types import VariationDict
@@ -141,7 +142,7 @@ class User(AbstractBaseUser, PermissionsMixin):
return self.username return self.username
class Organizer(models.Model): class Organizer(Versionable):
""" """
This model represents an entity organizing events, like a company. This model represents an entity organizing events, like a company.
Any organizer has a unique slug, which is a short name (alphanumeric, Any organizer has a unique slug, which is a short name (alphanumeric,
@@ -165,13 +166,13 @@ class Organizer(models.Model):
return self.name return self.name
class OrganizerPermission(models.Model): class OrganizerPermission(Versionable):
""" """
The relation between an Organizer and an User who has permissions to The relation between an Organizer and an User who has permissions to
access an organizer profile. access an organizer profile.
""" """
organizer = models.ForeignKey(Organizer) organizer = VersionedForeignKey(Organizer)
user = models.ForeignKey(User, related_name="organizer_perms") user = models.ForeignKey(User, related_name="organizer_perms")
can_create_events = models.BooleanField( can_create_events = models.BooleanField(
default=True, default=True,
@@ -181,7 +182,6 @@ class OrganizerPermission(models.Model):
class Meta: class Meta:
verbose_name = _("Organizer permission") verbose_name = _("Organizer permission")
verbose_name_plural = _("Organizer permissions") verbose_name_plural = _("Organizer permissions")
unique_together = (("organizer", "user"),)
def __str__(self): def __str__(self):
return _("%(name)s on %(object)s") % { return _("%(name)s on %(object)s") % {
@@ -190,7 +190,7 @@ class OrganizerPermission(models.Model):
} }
class Event(models.Model): class Event(Versionable):
""" """
This model represents an event. An event is anything you can buy This model represents an event. An event is anything you can buy
tickets for. It belongs to one orgnaizer and has a name and a slug, tickets for. It belongs to one orgnaizer and has a name and a slug,
@@ -216,8 +216,8 @@ class Event(models.Model):
matter when they were ordered (and thus, ignoring payment_term_days). matter when they were ordered (and thus, ignoring payment_term_days).
""" """
organizer = models.ForeignKey(Organizer, related_name="events", organizer = VersionedForeignKey(Organizer, related_name="events",
on_delete=models.PROTECT) on_delete=models.PROTECT)
name = models.CharField(max_length=200, name = models.CharField(max_length=200,
verbose_name=_("Name")) verbose_name=_("Name"))
slug = models.CharField( slug = models.CharField(
@@ -284,7 +284,7 @@ class Event(models.Model):
class Meta: class Meta:
verbose_name = _("Event") verbose_name = _("Event")
verbose_name_plural = _("Events") verbose_name_plural = _("Events")
unique_together = (("organizer", "slug"),) # unique_together = (("organizer", "slug"),) # TODO: Enforce manually
ordering = ("date_from", "name") ordering = ("date_from", "name")
def __str__(self): def __str__(self):
@@ -319,13 +319,13 @@ class Event(models.Model):
return EventRelatedCache(self) return EventRelatedCache(self)
class EventPermission(models.Model): class EventPermission(Versionable):
""" """
The relation between an Event and an User who has permissions to The relation between an Event and an User who has permissions to
access an event. access an event.
""" """
event = models.ForeignKey(Event) event = VersionedForeignKey(Event)
user = models.ForeignKey(User, related_name="event_perms") user = models.ForeignKey(User, related_name="event_perms")
can_change_settings = models.BooleanField( can_change_settings = models.BooleanField(
default=True, default=True,
@@ -339,7 +339,6 @@ class EventPermission(models.Model):
class Meta: class Meta:
verbose_name = _("Event permission") verbose_name = _("Event permission")
verbose_name_plural = _("Event permissions") verbose_name_plural = _("Event permissions")
unique_together = (("event", "user"),)
def __str__(self): def __str__(self):
return _("%(name)s on %(object)s") % { return _("%(name)s on %(object)s") % {
@@ -348,11 +347,11 @@ class EventPermission(models.Model):
} }
class ItemCategory(models.Model): class ItemCategory(Versionable):
""" """
Items can be sorted into categories Items can be sorted into categories
""" """
event = models.ForeignKey( event = VersionedForeignKey(
Event, Event,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='categories', related_name='categories',
@@ -373,20 +372,25 @@ class ItemCategory(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
def save(self, *args, **kwargs): def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
if self.event:
self.event.get_cache().clear()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.event: if self.event:
self.event.get_cache().clear() self.event.get_cache().clear()
return super().save(*args, **kwargs)
class Property(models.Model): class Property(Versionable):
""" """
A property is a modifier which can be applied to an A property is a modifier which can be applied to an
Item. For example 'Size' would be a property associated Item. For example 'Size' would be a property associated
with the item 'T-Shirt'. with the item 'T-Shirt'.
""" """
event = models.ForeignKey( event = VersionedForeignKey(
Event, Event,
related_name="properties", related_name="properties",
) )
@@ -402,19 +406,24 @@ class Property(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
def save(self, *args, **kwargs): def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
if self.event:
self.event.get_cache().clear()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.event: if self.event:
self.event.get_cache().clear() self.event.get_cache().clear()
return super().save(*args, **kwargs)
class PropertyValue(models.Model): class PropertyValue(Versionable):
""" """
A value of a property. If the property would be 'T-Shirt size', A value of a property. If the property would be 'T-Shirt size',
this could be 'M' or 'L' this could be 'M' or 'L'
""" """
prop = models.ForeignKey( prop = VersionedForeignKey(
Property, Property,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="values" related_name="values"
@@ -435,13 +444,18 @@ class PropertyValue(models.Model):
def __str__(self): def __str__(self):
return "%s: %s" % (self.prop.name, self.value) return "%s: %s" % (self.prop.name, self.value)
def save(self, *args, **kwargs): def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
if self.prop:
self.prop.event.get_cache().clear()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.prop: if self.prop:
self.prop.event.get_cache().clear() self.prop.event.get_cache().clear()
return super().save(*args, **kwargs)
class Question(models.Model): class Question(Versionable):
""" """
A question is an input field that can be used to extend a ticket A question is an input field that can be used to extend a ticket
by custom information, e.g. "Attendee name" or "Attendee age". by custom information, e.g. "Attendee name" or "Attendee age".
@@ -457,7 +471,7 @@ class Question(models.Model):
(TYPE_BOOLEAN, _("Yes/No")), (TYPE_BOOLEAN, _("Yes/No")),
) )
event = models.ForeignKey( event = VersionedForeignKey(
Event, Event,
related_name="questions", related_name="questions",
) )
@@ -481,13 +495,18 @@ class Question(models.Model):
def __str__(self): def __str__(self):
return self.question return self.question
def save(self, *args, **kwargs): def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
if self.event:
self.event.get_cache().clear()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.event: if self.event:
self.event.get_cache().clear() self.event.get_cache().clear()
return super().save(*args, **kwargs)
class Item(models.Model): class Item(Versionable):
""" """
An item is a thing which can be sold. It belongs to an An item is a thing which can be sold. It belongs to an
event and may or may not belong to a category. event and may or may not belong to a category.
@@ -499,13 +518,13 @@ class Item(models.Model):
inconsistencies. Instead, they have an attribute "deleted". inconsistencies. Instead, they have an attribute "deleted".
Deleted items will not be shown anywhere. Deleted items will not be shown anywhere.
""" """
event = models.ForeignKey( event = VersionedForeignKey(
Event, Event,
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name="items", related_name="items",
verbose_name=_("Event"), verbose_name=_("Event"),
) )
category = models.ForeignKey( category = VersionedForeignKey(
ItemCategory, ItemCategory,
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name="items", related_name="items",
@@ -540,7 +559,7 @@ class Item(models.Model):
verbose_name=_("Taxes included in percent"), verbose_name=_("Taxes included in percent"),
max_digits=7, decimal_places=2 max_digits=7, decimal_places=2
) )
properties = models.ManyToManyField( properties = VersionedManyToManyField(
Property, Property,
related_name='items', related_name='items',
verbose_name=_("Properties"), verbose_name=_("Properties"),
@@ -551,7 +570,7 @@ class Item(models.Model):
+ '\'Variations\' tab to configure the details.' + '\'Variations\' tab to configure the details.'
) )
) )
questions = models.ManyToManyField( questions = VersionedManyToManyField(
Question, Question,
related_name='items', related_name='items',
verbose_name=_("Questions"), verbose_name=_("Questions"),
@@ -570,14 +589,16 @@ class Item(models.Model):
return self.name return self.name
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.event: if self.event:
self.event.get_cache().clear() self.event.get_cache().clear()
return super().save(*args, **kwargs)
def delete(self): def delete(self):
self.deleted = True self.deleted = True
self.active = False self.active = False
return super().save() super().save()
if self.event:
self.event.get_cache().clear()
def get_all_variations(self, use_cache: bool=False) -> "list[VariationDict]": def get_all_variations(self, use_cache: bool=False) -> "list[VariationDict]":
""" """
@@ -593,26 +614,26 @@ class Item(models.Model):
if use_cache and hasattr(self, '_get_all_variations_cache'): if use_cache and hasattr(self, '_get_all_variations_cache'):
return self._get_all_variations_cache return self._get_all_variations_cache
all_variations = self.variations.all().prefetch_related("values") all_variations = self.variations.current.all().prefetch_related("values")
all_properties = self.properties.all().prefetch_related("values") all_properties = self.properties.current.all().prefetch_related("values")
variations_cache = {} variations_cache = {}
for var in all_variations: for var in all_variations:
key = [] key = []
for v in var.values.all(): for v in var.values.current.all():
key.append((v.prop_id, v.pk)) key.append((v.prop_id, v.identity))
key = tuple(sorted(key)) key = tuple(sorted(key))
variations_cache[key] = var variations_cache[key] = var
result = [] result = []
for comb in product(*[prop.values.all() for prop in all_properties]): for comb in product(*[prop.values.current.all() for prop in all_properties]):
if len(comb) == 0: if len(comb) == 0:
result.append(VariationDict()) result.append(VariationDict())
continue continue
key = [] key = []
var = VariationDict() var = VariationDict()
for v in comb: for v in comb:
key.append((v.prop.pk, v.pk)) key.append((v.prop.identity, v.identity))
var[v.prop.pk] = v var[v.prop.identity] = v
key = tuple(sorted(key)) key = tuple(sorted(key))
if key in variations_cache: if key in variations_cache:
var['variation'] = variations_cache[key] var['variation'] = variations_cache[key]
@@ -622,7 +643,7 @@ class Item(models.Model):
return result return result
class ItemVariation(models.Model): class ItemVariation(Versionable):
""" """
A variation is an item combined with values for all properties A variation is an item combined with values for all properties
associated with the item. For example, if your item is 'T-Shirt' associated with the item. For example, if your item is 'T-Shirt'
@@ -641,11 +662,11 @@ class ItemVariation(models.Model):
Restrictions can be not only set to items but also directly to variations. Restrictions can be not only set to items but also directly to variations.
""" """
item = models.ForeignKey( item = VersionedForeignKey(
Item, Item,
related_name='variations' related_name='variations'
) )
values = models.ManyToManyField( values = VersionedManyToManyField(
PropertyValue, PropertyValue,
related_name='variations', related_name='variations',
) )
@@ -663,13 +684,18 @@ class ItemVariation(models.Model):
verbose_name = _("Item variation") verbose_name = _("Item variation")
verbose_name_plural = _("Item variations") verbose_name_plural = _("Item variations")
def save(self, *args, **kwargs): def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
if self.item:
self.item.event.get_cache().clear()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.item: if self.item:
self.item.event.get_cache().clear() self.item.event.get_cache().clear()
return super().save(*args, **kwargs)
class VariationsField(models.ManyToManyField): class VariationsField(VersionedManyToManyField):
""" """
This is a ManyToManyField using the tixlcontrol.views.forms.VariationsField This is a ManyToManyField using the tixlcontrol.views.forms.VariationsField
form field by default. form field by default.
@@ -690,31 +716,31 @@ class VariationsField(models.ManyToManyField):
initial = defaults['initial'] initial = defaults['initial']
if callable(initial): if callable(initial):
initial = initial() initial = initial()
defaults['initial'] = [i.pk for i in initial] defaults['initial'] = [i.identity for i in initial]
# Skip ManyToManyField in dependency chain # Skip ManyToManyField in dependency chain
return super(RelatedField, self).formfield(**defaults) return super(RelatedField, self).formfield(**defaults)
class BaseRestriction(models.Model): class BaseRestriction(Versionable):
""" """
A restriction is the abstract concept of a rule that limits the availability A restriction is the abstract concept of a rule that limits the availability
of Items or ItemVariations. This model is just an abstract base class to be of Items or ItemVariations. This model is just an abstract base class to be
extended by restriction plugins. extended by restriction plugins.
""" """
event = models.ForeignKey( event = VersionedForeignKey(
Event, Event,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="restrictions_%(app_label)s_%(class)s", related_name="restrictions_%(app_label)s_%(class)s",
verbose_name=_("Event"), verbose_name=_("Event"),
) )
item = models.ForeignKey( item = VersionedForeignKey(
Item, Item,
blank=True, null=True, blank=True, null=True,
verbose_name=_("Item"), verbose_name=_("Item"),
related_name="restrictions_%(app_label)s_%(class)s", related_name="restrictions_%(app_label)s_%(class)s",
) )
variations = VariationsField( variations = VariationsField(
ItemVariation, 'tixlbase.ItemVariation',
blank=True, blank=True,
verbose_name=_("Variations"), verbose_name=_("Variations"),
related_name="restrictions_%(app_label)s_%(class)s", related_name="restrictions_%(app_label)s_%(class)s",
@@ -725,13 +751,18 @@ class BaseRestriction(models.Model):
verbose_name = _("Restriction") verbose_name = _("Restriction")
verbose_name_plural = _("Restrictions") verbose_name_plural = _("Restrictions")
def save(self, *args, **kwargs): def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
if self.event:
self.event.get_cache().clear()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.event: if self.event:
self.event.get_cache().clear() self.event.get_cache().clear()
return super().save(*args, **kwargs)
class Quota(models.Model): class Quota(Versionable):
""" """
A quota is a "pool of tickets". It is there to limit the number of items A quota is a "pool of tickets". It is there to limit the number of items
of a certain type to be sold. For example, you could have a quota of 500 of a certain type to be sold. For example, you could have a quota of 500
@@ -750,7 +781,7 @@ class Quota(models.Model):
implementation specific and are considered private. It is planned that they implementation specific and are considered private. It is planned that they
are being used as a fallback solution if redis is not available. are being used as a fallback solution if redis is not available.
""" """
event = models.ForeignKey( event = VersionedForeignKey(
Event, Event,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="quotas", related_name="quotas",
@@ -763,7 +794,7 @@ class Quota(models.Model):
size = models.PositiveIntegerField( size = models.PositiveIntegerField(
verbose_name=_("Total capacity") verbose_name=_("Total capacity")
) )
items = models.ManyToManyField( items = VersionedManyToManyField(
Item, Item,
verbose_name=_("Item"), verbose_name=_("Item"),
blank=True blank=True
@@ -790,7 +821,7 @@ class Quota(models.Model):
return self.name return self.name
class Order(models.Model): class Order(Versionable):
""" """
An order is created when a user clicks 'buy' on his cart. It holds An order is created when a user clicks 'buy' on his cart. It holds
several OrderPositions and is connected to an user. It has an several OrderPositions and is connected to an user. It has an
@@ -817,7 +848,7 @@ class Order(models.Model):
choices=STATUS_CHOICE, choices=STATUS_CHOICE,
verbose_name=_("Status") verbose_name=_("Status")
) )
event = models.ForeignKey( event = VersionedForeignKey(
Event, Event,
verbose_name=_("Event") verbose_name=_("Event")
) )
@@ -848,13 +879,13 @@ class Order(models.Model):
verbose_name_plural = _("Orders") verbose_name_plural = _("Orders")
class QuestionAnswer(models.Model): class QuestionAnswer(Versionable):
""" """
The answer to a Question, connected to an OrderPosition or CartPosition The answer to a Question, connected to an OrderPosition or CartPosition
""" """
orderposition = models.ForeignKey('OrderPosition', null=True, blank=True) orderposition = models.ForeignKey('OrderPosition', null=True, blank=True)
cartposition = models.ForeignKey('CartPosition', null=True, blank=True) cartposition = models.ForeignKey('CartPosition', null=True, blank=True)
question = models.ForeignKey(Question) question = VersionedForeignKey(Question)
answer = models.TextField() answer = models.TextField()
@@ -866,15 +897,15 @@ class OrderPosition(models.Model):
Important: An OrderPosition holds its total monetary value, as an order is a Important: An OrderPosition holds its total monetary value, as an order is a
piece of 'history' and must not change due to a change in item prices. piece of 'history' and must not change due to a change in item prices.
""" """
order = models.ForeignKey( order = VersionedForeignKey(
Order, Order,
verbose_name=_("Order") verbose_name=_("Order")
) )
item = models.ForeignKey( item = VersionedForeignKey(
Item, Item,
verbose_name=_("Item") verbose_name=_("Item")
) )
variation = models.ForeignKey( variation = VersionedForeignKey(
ItemVariation, ItemVariation,
null=True, blank=True, null=True, blank=True,
verbose_name=_("Variation") verbose_name=_("Variation")
@@ -883,7 +914,7 @@ class OrderPosition(models.Model):
decimal_places=2, max_digits=10, decimal_places=2, max_digits=10,
verbose_name=_("Price") verbose_name=_("Price")
) )
answers = models.ManyToManyField( answers = VersionedManyToManyField(
Question, Question,
through=QuestionAnswer, through=QuestionAnswer,
verbose_name=_("Answers") verbose_name=_("Answers")
@@ -903,7 +934,7 @@ class CartPosition(models.Model):
as we do not want to throw out users while they're clicking through as we do not want to throw out users while they're clicking through
the checkout process. the checkout process.
""" """
event = models.ForeignKey( event = VersionedForeignKey(
Event, Event,
verbose_name=_("Event") verbose_name=_("Event")
) )
@@ -915,11 +946,11 @@ class CartPosition(models.Model):
max_length=255, null=True, blank=True, max_length=255, null=True, blank=True,
verbose_name=_("Session key") verbose_name=_("Session key")
) )
item = models.ForeignKey( item = VersionedForeignKey(
Item, Item,
verbose_name=_("Item") verbose_name=_("Item")
) )
variation = models.ForeignKey( variation = VersionedForeignKey(
ItemVariation, ItemVariation,
null=True, blank=True, null=True, blank=True,
verbose_name=_("Variation") verbose_name=_("Variation")

View File

@@ -5,17 +5,16 @@ class VariationDict(dict):
returned by ``Item.get_all_variations()`` to avoid duplicate code in the returned by ``Item.get_all_variations()`` to avoid duplicate code in the
code calling this method. code calling this method.
""" """
IGNORE_KEYS = ('variation', 'key')
def relevant_items(self) -> "list[(int, PropertyValue)]": def relevant_items(self) -> "list[(str, PropertyValue)]":
""" """
Iterate over all items with numeric keys. Iterate over all items with numeric keys.
This is in use because the variation dictionaries use property ids This is in use because the variation dictionaries use property ids
as key and have some special keys like 'variation'. as key and have some special keys like 'variation'.
""" """
for i in self.items(): return (i for i in self.items() if i[0] not in self.IGNORE_KEYS)
if type(i[0]) is int:
yield i
def relevant_values(self) -> "list[PropertyValue]": def relevant_values(self) -> "list[PropertyValue]":
""" """
@@ -24,9 +23,7 @@ class VariationDict(dict):
This is in use because the variation dictionaries use property ids This is in use because the variation dictionaries use property ids
as key and have some special keys like 'variation'. as key and have some special keys like 'variation'.
""" """
for i in self.items(): return (i[1] for i in self.items() if i[0] not in self.IGNORE_KEYS)
if type(i[0]) is int:
yield i[1]
def identify(self) -> str: def identify(self) -> str:
""" """

View File

@@ -46,11 +46,11 @@ class PermissionMiddleware:
return redirect_to_login( return redirect_to_login(
path, resolved_login_url, REDIRECT_FIELD_NAME) path, resolved_login_url, REDIRECT_FIELD_NAME)
request.user.events_cache = request.user.events.order_by( request.user.events_cache = request.user.events.current.order_by(
"organizer", "date_from").prefetch_related("organizer") "organizer", "date_from").prefetch_related("organizer")
if 'event.' in url_name and 'event' in url.kwargs: if 'event.' in url_name and 'event' in url.kwargs:
try: try:
request.event = Event.objects.get( request.event = Event.objects.current.get(
slug=url.kwargs['event'], slug=url.kwargs['event'],
permitted__id__exact=request.user.id, permitted__id__exact=request.user.id,
organizer__slug=url.kwargs['organizer'], organizer__slug=url.kwargs['organizer'],

View File

@@ -4,9 +4,9 @@
{% block content %} {% block content %}
<h1>{% trans "Modify item:" %} {{ item.name }}</h1> <h1>{% trans "Modify item:" %} {{ item.name }}</h1>
<ul class="nav nav-pills"> <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.pk %}">{% trans "General information" %}</a></li> <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.pk %}">{% trans "Variations" %}</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.pk %}">{% trans "Restrictions" %}</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> </ul>
{% block inside %} {% block inside %}
{% endblock %} {% endblock %}

View File

@@ -30,12 +30,12 @@
<tbody> <tbody>
{% for c in categories %} {% for c in categories %}
<tr> <tr>
<td><strong><a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.pk %}">{{ c.name }}</a></strong></td> <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> <td>
<a href="{% url "control:event.items.categories.up" organizer=request.event.organizer.slug event=request.event.slug category=c.pk %}" 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.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.pk %}" class="btn btn-default btn-sm {% if forloop.revcounter0 == 0 %}disabled{% endif %}"><i class="fa fa-arrow-down"></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>
<td class="text-right"><a href="{% url "control:event.items.categories.delete" organizer=request.event.organizer.slug event=request.event.slug category=c.pk %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@@ -6,12 +6,12 @@
<h1>{% trans "Delete item category" %}</h1> <h1>{% trans "Delete item category" %}</h1>
<form action="" method="post" class="form-horizontal"> <form action="" method="post" class="form-horizontal">
{% csrf_token %} {% csrf_token %}
<p>{% blocktrans %}Are you sure you want to the category <strong>{{ category.name }}</strong>?{% endblocktrans %}</p> <p>{% blocktrans %}Are you sure you want to delete the category <strong>{{ category.name }}</strong>?{% endblocktrans %}</p>
<div class="form-group submit-group"> <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"> <a href="{% url "control:event.items.categories" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %} {% trans "Cancel" %}
</a> </a>
<button type="submit" class="btn btn-primary btn-save"> <button type="submit" class="btn btn-danger btn-save">
{% trans "Delete" %} {% trans "Delete" %}
</button> </button>
</div> </div>

View File

@@ -13,7 +13,8 @@
<tbody> <tbody>
{% for i in items %} {% for i in items %}
<tr> <tr>
<td><strong><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.pk %}">{{ i.name }}</a></strong></td> <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>{{ i.category }}</td> <td>{{ i.category }}</td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@@ -29,8 +29,10 @@
<tbody> <tbody>
{% for p in properties %} {% for p in properties %}
<tr> <tr>
<td><strong><a href="{% url "control:event.items.properties.edit" organizer=request.event.organizer.slug event=request.event.slug property=p.pk %}">{{ p.name }}</a></strong></td> <td><strong><a href="
<td class="text-right"><a href="{% url "control:event.items.properties.delete" organizer=request.event.organizer.slug event=request.event.slug property=p.pk %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a></td> {% 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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@@ -19,7 +19,7 @@
<a href="{% url "control:event.items.properties" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel"> <a href="{% url "control:event.items.properties" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %} {% trans "Cancel" %}
</a> </a>
<button type="submit" class="btn btn-primary btn-save"> <button type="submit" class="btn btn-danger btn-save">
{% trans "Delete" %} {% trans "Delete" %}
</button> </button>
</div> </div>

View File

@@ -8,7 +8,7 @@
{% csrf_token %} {% csrf_token %}
<p>{% blocktrans %}Are you sure you want to the question <strong>{{ question }}</strong>?{% endblocktrans %}</p> <p>{% blocktrans %}Are you sure you want to the question <strong>{{ question }}</strong>?{% endblocktrans %}</p>
{% if dependent|length > 0 %} {% if dependent|length > 0 %}
<p>{% blocktrans %}All answers to the question given by the buyers of the following tickets will be <strong>permanently lost</strong>.{% endblocktrans %}</p> <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 %} {% 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> <li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.pk %}">{{ item.name }}</a></li>
{% endfor %} {% endfor %}
@@ -17,7 +17,7 @@
<a href="{% url "control:event.items.questions" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel"> <a href="{% url "control:event.items.questions" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %} {% trans "Cancel" %}
</a> </a>
<button type="submit" class="btn btn-primary btn-save"> <button type="submit" class="btn btn-danger btn-save">
{% trans "Delete" %} {% trans "Delete" %}
</button> </button>
</div> </div>

View File

@@ -30,9 +30,11 @@
<tbody> <tbody>
{% for q in questions %} {% for q in questions %}
<tr> <tr>
<td><strong><a href="{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=q.pk %}">{{ q.question }}</a></strong></td> <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>{{ 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.pk %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a></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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@@ -17,7 +17,7 @@
<a href="{% url "control:event.items.quotas" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel"> <a href="{% url "control:event.items.quotas" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %} {% trans "Cancel" %}
</a> </a>
<button type="submit" class="btn btn-primary btn-save"> <button type="submit" class="btn btn-danger btn-save">
{% trans "Delete" %} {% trans "Delete" %}
</button> </button>
</div> </div>

View File

@@ -32,11 +32,11 @@
<tbody> <tbody>
{% for q in quotas %} {% for q in quotas %}
<tr> <tr>
<td><strong><a href="{% url "control:event.items.quotas.edit" organizer=request.event.organizer.slug event=request.event.slug quota=q.pk %}">{{ q.name }}</a></strong></td> <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></td> <td></td>
<td>{{ q.size }}</td> <td>{{ q.size }}</td>
<td></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.pk %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a></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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@@ -21,26 +21,26 @@ urlpatterns += patterns(
url(r'^settings/$', event.EventUpdate.as_view(), name='event.settings'), url(r'^settings/$', event.EventUpdate.as_view(), name='event.settings'),
url(r'^settings/plugins$', event.EventPlugins.as_view(), name='event.settings.plugins'), 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/$', item.ItemList.as_view(), name='event.items'),
url(r'^items/(?P<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'), url(r'^items/(?P<item>[0-9a-f-]+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
url(r'^items/(?P<item>\d+)/variations$', item.ItemVariations.as_view(), name='event.item.variations'), url(r'^items/(?P<item>[0-9a-f-]+)/variations$', item.ItemVariations.as_view(), name='event.item.variations'),
url(r'^items/(?P<item>\d+)/restrictions$', item.ItemRestrictions.as_view(), name='event.item.restrictions'), 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/$', item.CategoryList.as_view(), name='event.items.categories'),
url(r'^categories/(?P<category>\d+)/delete$', item.CategoryDelete.as_view(), name='event.items.categories.delete'), url(r'^categories/(?P<category>[0-9a-f-]+)/delete$', item.CategoryDelete.as_view(), name='event.items.categories.delete'),
url(r'^categories/(?P<category>\d+)/up$', item.category_move_up, name='event.items.categories.up'), url(r'^categories/(?P<category>[0-9a-f-]+)/up$', item.category_move_up, name='event.items.categories.up'),
url(r'^categories/(?P<category>\d+)/down$', item.category_move_down, name='event.items.categories.down'), url(r'^categories/(?P<category>[0-9a-f-]+)/down$', item.category_move_down, name='event.items.categories.down'),
url(r'^categories/(?P<category>\d+)/$', item.CategoryUpdate.as_view(), name='event.items.categories.edit'), 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'^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/$', item.QuestionList.as_view(), name='event.items.questions'),
url(r'^questions/(?P<question>\d+)/delete$', item.QuestionDelete.as_view(), name='event.items.questions.delete'), url(r'^questions/(?P<question>[0-9a-f-]+)/delete$', item.QuestionDelete.as_view(), name='event.items.questions.delete'),
url(r'^questions/(?P<question>\d+)/$', item.QuestionUpdate.as_view(), name='event.items.questions.edit'), 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'^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/$', item.PropertyList.as_view(), name='event.items.properties'),
url(r'^properties/(?P<property>\d+)/$', item.PropertyUpdate.as_view(), name='event.items.properties.edit'), url(r'^properties/(?P<property>[0-9a-f-]+)/$', item.PropertyUpdate.as_view(), name='event.items.properties.edit'),
url(r'^properties/(?P<property>\d+)/delete$', item.PropertyDelete.as_view(), name='event.items.properties.delete'), 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'^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/$', item.QuotaList.as_view(), name='event.items.quotas'),
url(r'^quotas/(?P<quota>\d+)/$', item.QuotaUpdate.as_view(), name='event.items.quotas.edit'), url(r'^quotas/(?P<quota>[0-9a-f-]+)/$', item.QuotaUpdate.as_view(), name='event.items.quotas.edit'),
url(r'^quotas/(?P<quota>\d+)/delete$', item.QuotaDelete.as_view(), url(r'^quotas/(?P<quota>[0-9a-f-]+)/delete$', item.QuotaDelete.as_view(),
name='event.items.quotas.delete'), name='event.items.quotas.delete'),
url(r'^quotas/add$', item.QuotaCreate.as_view(), name='event.items.quotas.add'), url(r'^quotas/add$', item.QuotaCreate.as_view(), name='event.items.quotas.add'),
) )

View File

@@ -7,12 +7,13 @@ from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from pytz import common_timezones from pytz import common_timezones
from tixlbase.forms import VersionedModelForm
from tixlbase.models import Event from tixlbase.models import Event
from tixlcontrol.permissions import EventPermissionRequiredMixin from tixlcontrol.permissions import EventPermissionRequiredMixin
class EventUpdateForm(forms.ModelForm): class EventUpdateForm(VersionedModelForm):
timezone = forms.ChoiceField( timezone = forms.ChoiceField(
choices=((a, a) for a in common_timezones), choices=((a, a) for a in common_timezones),

View File

@@ -7,18 +7,19 @@ from django.utils.encoding import force_text
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from tixlbase.forms import VersionedModelForm
from tixlbase.models import ItemVariation, PropertyValue, Item from tixlbase.models import ItemVariation, PropertyValue, Item
class TolerantFormsetModelForm(forms.ModelForm): class TolerantFormsetModelForm(VersionedModelForm):
def has_changed(self) -> bool: def has_changed(self) -> bool:
""" """
Returns True if data differs from initial. Contrary to the default Returns True if data differs from initial. Contrary to the default
implementation, the ORDER field is being ignored. implementation, the ORDER field is being ignored.
""" """
for name, field in self.fields.items(): for name, field in self.fields.items():
if name == 'ORDER': if name == 'ORDER' or name == 'id':
continue continue
prefixed_name = self.add_prefix(name) prefixed_name = self.add_prefix(name)
data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name) data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
@@ -37,7 +38,7 @@ class TolerantFormsetModelForm(forms.ModelForm):
self._changed_data.append(name) self._changed_data.append(name)
continue continue
# We're using a private API of Django here. This is not nice, but no problem as it seems # 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 Django 1.7. # like this will become a public API in future Django.
if field._has_changed(initial_value, data_value): if field._has_changed(initial_value, data_value):
return True return True
return False return False
@@ -77,7 +78,7 @@ class RestrictionInlineFormset(forms.BaseInlineFormSet):
data, files, instance, save_as_new, prefix, queryset, **kwargs data, files, instance, save_as_new, prefix, queryset, **kwargs
) )
if isinstance(self.instance, Item): if isinstance(self.instance, Item):
self.queryset = self.queryset.prefetch_related("variations") self.queryset = self.queryset.as_of().prefetch_related("variations")
def initialized_empty_form(self): def initialized_empty_form(self):
form = self.form( form = self.form(
@@ -139,7 +140,7 @@ class VariationsFieldRenderer(forms.widgets.CheckboxFieldRenderer):
final_attrs['checked'] = 'checked' final_attrs['checked'] = 'checked'
w = self.choice_input_class( w = self.choice_input_class(
self.name, self.value, self.attrs.copy(), self.name, self.value, self.attrs.copy(),
(variation['key'], variation[properties[0].pk].value), (variation['key'], variation[properties[0].identity].value),
i i
) )
output.append(format_html('<li>{0}</li>', force_text(w))) output.append(format_html('<li>{0}</li>', force_text(w)))
@@ -148,28 +149,28 @@ class VariationsFieldRenderer(forms.widgets.CheckboxFieldRenderer):
elif dimension >= 2: elif dimension >= 2:
# prop1 is the property on all the grid's y-axes # prop1 is the property on all the grid's y-axes
prop1 = properties[0] prop1 = properties[0]
prop1v = list(prop1.values.all()) prop1v = list(prop1.values.current.all())
# prop2 is the property on all the grid's x-axes # prop2 is the property on all the grid's x-axes
prop2 = properties[1] prop2 = properties[1]
prop2v = list(prop2.values.all()) prop2v = list(prop2.values.current.all())
# Given an iterable of PropertyValue objects, this will return a # Given an iterable of PropertyValue objects, this will return a
# list of their primary keys, ordered by the primary keys of the # list of their primary keys, ordered by the primary keys of the
# properties they belong to EXCEPT the value for the property prop2. # properties they belong to EXCEPT the value for the property prop2.
# We'll see later why we need this. # We'll see later why we need this.
selector = lambda values: [ selector = lambda values: [
v.pk for v in sorted(values, key=lambda v: v.prop.pk) v.identity for v in sorted(values, key=lambda v: v.prop.identity)
if v.prop.pk != prop2.pk if v.prop.identity != prop2.identity
] ]
# Given a list of variations, this will sort them by their position # Given a list of variations, this will sort them by their position
# on the x-axis # on the x-axis
sort = lambda v: v[prop2.pk].pk sort = lambda v: v[prop2.identity].identity
# We now iterate over the cartesian product of all the other # We now iterate over the cartesian product of all the other
# properties which are NOT on the axes of the grid because we # properties which are NOT on the axes of the grid because we
# create one grid for any combination of them. # create one grid for any combination of them.
for gridrow in product(*[prop.values.all() for prop in properties[2:]]): for gridrow in product(*[prop.values.current.all() for prop in properties[2:]]):
if len(gridrow) > 0: if len(gridrow) > 0:
output.append('<strong>') output.append('<strong>')
output.append(", ".join([value.value for value in gridrow])) output.append(", ".join([value.value for value in gridrow]))
@@ -250,7 +251,7 @@ class VariationsField(forms.ModelMultipleChoiceField):
variations = self.item.get_all_variations(use_cache=True) variations = self.item.get_all_variations(use_cache=True)
return ( return (
( (
v['variation'].pk if 'variation' in v else v.key(), v['variation'].identity if 'variation' in v else v.key(),
v v
) for v in variations ) for v in variations
) )
@@ -287,9 +288,9 @@ class VariationsField(forms.ModelMultipleChoiceField):
for var in all_variations: for var in all_variations:
key = [] key = []
for v in var.values.all(): for v in var.values.all():
key.append((v.prop_id, v.pk)) key.append((v.prop_id, v.identity))
key = tuple(sorted(key)) key = tuple(sorted(key))
variations_cache[key] = var.pk variations_cache[key] = var.identity
cleaned_value = [] cleaned_value = []
@@ -303,7 +304,7 @@ class VariationsField(forms.ModelMultipleChoiceField):
# Hash the combination in the same way as in our cache above # Hash the combination in the same way as in our cache above
key = [] key = []
for pair in pk.split(","): for pair in pk.split(","):
key.append(tuple([int(i) for i in pair.split(":")])) key.append(tuple([i for i in pair.split(":")]))
key = tuple(sorted(key)) key = tuple(sorted(key))
if key in variations_cache: if key in variations_cache:
@@ -316,15 +317,15 @@ class VariationsField(forms.ModelMultipleChoiceField):
# No ItemVariation present, create one! # No ItemVariation present, create one!
var = ItemVariation() var = ItemVariation()
var.item = self.item var.item_id = self.item.identity
var.save() var.save()
# Add the values to the ItemVariation object # Add the values to the ItemVariation object
for pair in pk.split(","): for pair in pk.split(","):
prop, value = pair.split(":") prop, value = pair.split(":")
try: try:
var.values.add( var.values.add(
PropertyValue.objects.get( PropertyValue.objects.current.get(
pk=value, identity=value,
prop_id=prop prop_id=prop
) )
) )
@@ -334,16 +335,16 @@ class VariationsField(forms.ModelMultipleChoiceField):
code='invalid_pk_value', code='invalid_pk_value',
params={'pk': value}, params={'pk': value},
) )
variations_cache[key] = var.pk variations_cache[key] = var.identity
cleaned_value.append(str(var.pk)) cleaned_value.append(str(var.identity))
else: else:
# An ItemVariation id was given # An ItemVariation id was given
cleaned_value.append(pk) cleaned_value.append(pk)
qs = ItemVariation.objects.filter(item=self.item, pk__in=cleaned_value) qs = self.item.variations.current.filter(identity__in=cleaned_value)
# Re-check for consistency # Re-check for consistency
pks = set(force_text(getattr(o, "pk")) for o in qs) pks = set(force_text(getattr(o, "identity")) for o in qs)
for val in cleaned_value: for val in cleaned_value:
if force_text(val) not in pks: if force_text(val) not in pks:
raise ValidationError( raise ValidationError(

View File

@@ -1,4 +1,5 @@
from itertools import product from itertools import product
from django.db import transaction
from django.views.generic import ListView from django.views.generic import ListView
from django.views.generic.edit import CreateView, UpdateView, DeleteView from django.views.generic.edit import CreateView, UpdateView, DeleteView
@@ -6,9 +7,9 @@ from django.views.generic.base import TemplateView
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django.core.urlresolvers import resolve, reverse from django.core.urlresolvers import resolve, reverse
from django.http import HttpResponseRedirect, HttpResponseForbidden from django.http import HttpResponseRedirect, HttpResponseForbidden
from django import forms
from django.shortcuts import redirect from django.shortcuts import redirect
from django.forms.models import inlineformset_factory from django.forms.models import inlineformset_factory
from tixlbase.forms import VersionedModelForm
from tixlbase.models import ( from tixlbase.models import (
Item, ItemCategory, Property, ItemVariation, PropertyValue, Question, Quota Item, ItemCategory, Property, ItemVariation, PropertyValue, Question, Quota
@@ -24,12 +25,12 @@ class ItemList(ListView):
template_name = 'tixlcontrol/items/index.html' template_name = 'tixlcontrol/items/index.html'
def get_queryset(self): def get_queryset(self):
return Item.objects.filter( return Item.objects.current.filter(
event=self.request.event event=self.request.event
).prefetch_related("category") ).prefetch_related("category")
class CategoryForm(forms.ModelForm): class CategoryForm(VersionedModelForm):
class Meta: class Meta:
model = ItemCategory model = ItemCategory
@@ -48,13 +49,15 @@ class CategoryDelete(EventPermissionRequiredMixin, DeleteView):
def get_object(self, queryset=None): def get_object(self, queryset=None):
url = resolve(self.request.path_info) url = resolve(self.request.path_info)
return self.request.event.categories.get( return self.request.event.categories.current.get(
id=url.kwargs['category'] identity=url.kwargs['category']
) )
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
self.object.items.update(category=None) for item in self.object.items.current.all():
item.category = None
item.save()
success_url = self.get_success_url() success_url = self.get_success_url()
self.object.delete() self.object.delete()
return HttpResponseRedirect(success_url) return HttpResponseRedirect(success_url)
@@ -75,8 +78,8 @@ class CategoryUpdate(EventPermissionRequiredMixin, UpdateView):
def get_object(self, queryset=None): def get_object(self, queryset=None):
url = resolve(self.request.path_info) url = resolve(self.request.path_info)
return self.request.event.categories.get( return self.request.event.categories.current.get(
id=url.kwargs['category'] identity=url.kwargs['category']
) )
def get_success_url(self): def get_success_url(self):
@@ -110,14 +113,14 @@ class CategoryList(ListView):
template_name = 'tixlcontrol/items/categories.html' template_name = 'tixlcontrol/items/categories.html'
def get_queryset(self): def get_queryset(self):
return self.request.event.categories.all() return self.request.event.categories.current.all()
def category_move(request, organizer, event, category, up=True): def category_move(request, organizer, event, category, up=True):
category = request.event.categories.get( category = request.event.categories.current.get(
id=category identity=category
) )
categories = list(request.event.categories.order_by("position")) categories = list(request.event.categories.current.order_by("position"))
index = categories.index(category) index = categories.index(category)
if index != 0 and up: if index != 0 and up:
@@ -155,12 +158,12 @@ class PropertyList(ListView):
template_name = 'tixlcontrol/items/properties.html' template_name = 'tixlcontrol/items/properties.html'
def get_queryset(self): def get_queryset(self):
return Property.objects.filter( return Property.objects.current.filter(
event=self.request.event event=self.request.event
) )
class PropertyForm(forms.ModelForm): class PropertyForm(VersionedModelForm):
class Meta: class Meta:
model = Property model = Property
localized_fields = '__all__' localized_fields = '__all__'
@@ -187,8 +190,8 @@ class PropertyUpdate(EventPermissionRequiredMixin, UpdateView):
def get_object(self, queryset=None): def get_object(self, queryset=None):
url = resolve(self.request.path_info) url = resolve(self.request.path_info)
return self.request.event.properties.get( return self.request.event.properties.current.get(
id=url.kwargs['property'] identity=url.kwargs['property']
) )
def get_success_url(self): def get_success_url(self):
@@ -206,7 +209,9 @@ class PropertyUpdate(EventPermissionRequiredMixin, UpdateView):
can_order=True, can_order=True,
extra=0, extra=0,
) )
formset = formsetclass(**self.get_form_kwargs()) kwargs = self.get_form_kwargs()
kwargs['queryset'] = self.object.values.current.all()
formset = formsetclass(**kwargs)
return formset return formset
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
@@ -215,9 +220,15 @@ class PropertyUpdate(EventPermissionRequiredMixin, UpdateView):
return context return context
def form_valid(self, form, formset): 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): 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.position = i
formset.save() f.instance.save()
return super().form_valid(form) return super().form_valid(form)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@@ -288,18 +299,18 @@ class PropertyDelete(EventPermissionRequiredMixin, DeleteView):
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs) context = super().get_context_data(*args, **kwargs)
context['dependent'] = self.get_object().items.all() context['dependent'] = self.get_object().items.current.all()
context['possible'] = self.is_allowed() context['possible'] = self.is_allowed()
return context return context
def is_allowed(self): def is_allowed(self):
return self.get_object().items.count() == 0 return self.get_object().items.current.count() == 0
def get_object(self, queryset=None): def get_object(self, queryset=None):
if not hasattr(self, 'object') or not self.object: if not hasattr(self, 'object') or not self.object:
url = resolve(self.request.path_info) url = resolve(self.request.path_info)
self.object = self.request.event.properties.get( self.object = self.request.event.properties.current.get(
id=url.kwargs['property'] identity=url.kwargs['property']
) )
return self.object return self.object
@@ -324,10 +335,10 @@ class QuestionList(ListView):
template_name = 'tixlcontrol/items/questions.html' template_name = 'tixlcontrol/items/questions.html'
def get_queryset(self): def get_queryset(self):
return self.request.event.questions.all() return self.request.event.questions.current.all()
class QuestionForm(forms.ModelForm): class QuestionForm(VersionedModelForm):
class Meta: class Meta:
model = Question model = Question
@@ -347,18 +358,17 @@ class QuestionDelete(EventPermissionRequiredMixin, DeleteView):
def get_object(self, queryset=None): def get_object(self, queryset=None):
url = resolve(self.request.path_info) url = resolve(self.request.path_info)
return self.request.event.questions.get( return self.request.event.questions.current.get(
id=url.kwargs['question'] identity=url.kwargs['question']
) )
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs) context = super().get_context_data(*args, **kwargs)
context['dependent'] = list(self.get_object().items.all()) context['dependent'] = list(self.get_object().items.current.all())
return context return context
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
self.object.items.update(category=None)
success_url = self.get_success_url() success_url = self.get_success_url()
self.object.delete() self.object.delete()
return HttpResponseRedirect(success_url) return HttpResponseRedirect(success_url)
@@ -379,8 +389,8 @@ class QuestionUpdate(EventPermissionRequiredMixin, UpdateView):
def get_object(self, queryset=None): def get_object(self, queryset=None):
url = resolve(self.request.path_info) url = resolve(self.request.path_info)
return self.request.event.questions.get( return self.request.event.questions.current.get(
id=url.kwargs['question'] identity=url.kwargs['question']
) )
def get_success_url(self): def get_success_url(self):
@@ -414,12 +424,12 @@ class QuotaList(ListView):
template_name = 'tixlcontrol/items/quotas.html' template_name = 'tixlcontrol/items/quotas.html'
def get_queryset(self): def get_queryset(self):
return Quota.objects.filter( return Quota.objects.current.filter(
event=self.request.event event=self.request.event
) )
class QuotaForm(forms.ModelForm): class QuotaForm(VersionedModelForm):
class Meta: class Meta:
model = Quota model = Quota
@@ -457,8 +467,8 @@ class QuotaUpdate(EventPermissionRequiredMixin, UpdateView):
def get_object(self, queryset=None): def get_object(self, queryset=None):
url = resolve(self.request.path_info) url = resolve(self.request.path_info)
return self.request.event.quotas.get( return self.request.event.quotas.current.get(
id=url.kwargs['quota'] identity=url.kwargs['quota']
) )
def get_success_url(self): def get_success_url(self):
@@ -476,18 +486,17 @@ class QuotaDelete(EventPermissionRequiredMixin, DeleteView):
def get_object(self, queryset=None): def get_object(self, queryset=None):
url = resolve(self.request.path_info) url = resolve(self.request.path_info)
return self.request.event.quotas.get( return self.request.event.quotas.current.get(
id=url.kwargs['quota'] identity=url.kwargs['quota']
) )
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs) context = super().get_context_data(*args, **kwargs)
context['dependent'] = list(self.get_object().items.all()) context['dependent'] = list(self.get_object().items.current.all())
return context return context
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
self.object.items.update(category=None)
success_url = self.get_success_url() success_url = self.get_success_url()
self.object.delete() self.object.delete()
return HttpResponseRedirect(success_url) return HttpResponseRedirect(success_url)
@@ -506,20 +515,20 @@ class ItemDetailMixin(SingleObjectMixin):
def get_object(self, queryset=None): def get_object(self, queryset=None):
if not hasattr(self, 'object') or not self.object: if not hasattr(self, 'object') or not self.object:
url = resolve(self.request.path_info) url = resolve(self.request.path_info)
self.item = self.request.event.items.get( self.item = self.request.event.items.current.get(
id=url.kwargs['item'] identity=url.kwargs['item']
) )
self.object = self.item self.object = self.item
return self.object return self.object
class ItemUpdateFormGeneral(forms.ModelForm): class ItemUpdateFormGeneral(VersionedModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['category'].queryset = self.instance.event.categories.all() self.fields['category'].queryset = self.instance.event.categories.current.all()
self.fields['properties'].queryset = self.instance.event.properties.all() self.fields['properties'].queryset = self.instance.event.properties.current.all()
self.fields['questions'].queryset = self.instance.event.questions.all() self.fields['questions'].queryset = self.instance.event.questions.current.all()
class Meta: class Meta:
model = Item model = Item
@@ -546,11 +555,11 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie
return reverse('control:event.item', kwargs={ return reverse('control:event.item', kwargs={
'organizer': self.request.event.organizer.slug, 'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug, 'event': self.request.event.slug,
'item': self.get_object().pk, 'item': self.get_object().identity,
}) + '?success=true' }) + '?success=true'
class ItemVariationForm(forms.ModelForm): class ItemVariationForm(VersionedModelForm):
class Meta: class Meta:
model = ItemVariation model = ItemVariation
@@ -580,13 +589,16 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
form = ItemVariationForm( form = ItemVariationForm(
data, data,
instance=variation['variation'], instance=variation['variation'],
prefix=",".join([str(i.pk) for i in values]), prefix=",".join([str(i.identity) for i in values]),
) )
else: else:
inst = ItemVariation(item=self.object)
inst.item_id = self.object.identity
inst.creation = True
form = ItemVariationForm( form = ItemVariationForm(
data, data,
instance=ItemVariation(item=self.object), instance=inst,
prefix=",".join([str(i.pk) for i in values]), prefix=",".join([str(i.identity) for i in values]),
) )
form.values = values form.values = values
return form return form
@@ -630,20 +642,20 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
# properties they belong to EXCEPT the value for the property prop2. # properties they belong to EXCEPT the value for the property prop2.
# We'll see later why we need this. # We'll see later why we need this.
selector = lambda values: [ selector = lambda values: [
v.pk for v in sorted(values, key=lambda v: v.prop.pk) v.identity for v in sorted(values, key=lambda v: v.prop.identity)
if v.prop.pk != prop2.pk if v.prop.identity != prop2.identity
] ]
# Given a list of variations, this will sort them by their position # Given a list of variations, this will sort them by their position
# on the x-axis # on the x-axis
sort = lambda v: v[prop2.pk].pk sort = lambda v: v[prop2.identity].identity
# We now iterate over the cartesian product of all the other # We now iterate over the cartesian product of all the other
# properties which are NOT on the axes of the grid because we # properties which are NOT on the axes of the grid because we
# create one grid for any combination of them. # create one grid for any combination of them.
for gridrow in product(*[prop.values.all() for prop in self.properties[2:]]): for gridrow in product(*[prop.values.current.all() for prop in self.properties[2:]]):
grids = [] grids = []
for val1 in prop1.values.all(): for val1 in prop1.values.current.all():
formrow = [] formrow = []
# We are now inside one of the rows of the grid and have to # 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 # select the variations to display in this row. In order to
@@ -669,7 +681,7 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
def main(self, request, *args, **kwargs): def main(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
self.properties = list(self.object.properties.all().prefetch_related("values")) self.properties = list(self.object.properties.current.all().prefetch_related("values"))
self.dimension = len(self.properties) self.dimension = len(self.properties)
self.forms, self.forms_flat = self.get_forms() self.forms, self.forms_flat = self.get_forms()
@@ -681,13 +693,16 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.main(request, *args, **kwargs) self.main(request, *args, **kwargs)
context = self.get_context_data(object=self.object) context = self.get_context_data(object=self.object)
for form in self.forms_flat: with transaction.atomic():
if form.is_valid(): for form in self.forms_flat:
if form.instance.pk is None: if form.is_valid() and form.has_changed():
form.save()
form.instance.values.add(*form.values)
else:
form.save() 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) return self.render_to_response(context)
def get_template_names(self): def get_template_names(self):
@@ -762,5 +777,5 @@ class ItemRestrictions(ItemDetailMixin, EventPermissionRequiredMixin, TemplateVi
return reverse('control:event.item.restrictions', kwargs={ return reverse('control:event.item.restrictions', kwargs={
'organizer': self.request.event.organizer.slug, 'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug, 'event': self.request.event.slug,
'item': self.object.pk 'item': self.object.identity
}) + '?success=true' }) + '?success=true'

View File

@@ -10,7 +10,7 @@ class EventList(ListView):
template_name = 'tixlcontrol/events/index.html' template_name = 'tixlcontrol/events/index.html'
def get_queryset(self): def get_queryset(self):
return Event.objects.filter( return Event.objects.current.filter(
permitted__id__exact=self.request.user.pk permitted__id__exact=self.request.user.pk
).prefetch_related( ).prefetch_related(
"organizer", "organizer",

View File

@@ -1,32 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('tixlbase', '0015_auto_20141006_2205'),
]
operations = [
migrations.CreateModel(
name='TimeRestriction',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)),
('timeframe_from', models.DateTimeField(verbose_name='Start of time frame')),
('timeframe_to', models.DateTimeField(verbose_name='End of time frame')),
('price', models.DecimalField(max_digits=7, verbose_name='Price in time frame', decimal_places=2, null=True, blank=True)),
('event', models.ForeignKey(related_name='restrictions_timerestriction_timerestriction', to='tixlbase.Event', verbose_name='Event')),
('items', models.ManyToManyField(to='tixlbase.Item', related_name='restrictions_timerestriction_timerestriction')),
('variations', models.ManyToManyField(to='tixlbase.ItemVariation', related_name='restrictions_timerestriction_timerestriction')),
],
options={
'abstract': False,
'verbose_name_plural': 'Restrictions',
'verbose_name': 'Restriction',
},
bases=(models.Model,),
),
]

View File

@@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('tixlbase', '0016_event_plugins'),
('timerestriction', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='timerestriction',
name='items',
),
migrations.AddField(
model_name='timerestriction',
name='i',
field=models.ForeignKey(null=True, blank=True, related_name='restrictions_timerestriction_timerestriction', to='tixlbase.Item'),
preserve_default=True,
),
migrations.AlterField(
model_name='timerestriction',
name='variations',
field=models.ManyToManyField(blank=True, related_name='restrictions_timerestriction_timerestriction', to='tixlbase.ItemVariation'),
),
]

View File

@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('timerestriction', '0002_auto_20141013_1811'),
]
operations = [
migrations.RenameField(
model_name='timerestriction',
old_name='i',
new_name='item',
),
]

View File

@@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import tixlbase.models
class Migration(migrations.Migration):
dependencies = [
('timerestriction', '0003_auto_20141013_1811'),
]
operations = [
migrations.AlterField(
model_name='timerestriction',
name='item',
field=models.ForeignKey(null=True, blank=True, related_name='restrictions_timerestriction_timerestriction', to='tixlbase.Item', verbose_name='Item'),
),
migrations.AlterField(
model_name='timerestriction',
name='variations',
field=tixlbase.models.VariationsField(related_name='restrictions_timerestriction_timerestriction', to='tixlbase.ItemVariation', blank=True, verbose_name='Variations'),
),
]

View File

@@ -20,7 +20,7 @@ def availability_handler(sender, **kwargs):
context = kwargs['context'] # NOQA context = kwargs['context'] # NOQA
# Fetch all restriction objects applied to this item # Fetch all restriction objects applied to this item
restrictions = list(TimeRestriction.objects.filter( restrictions = list(TimeRestriction.objects.current.filter(
item=item, item=item,
).prefetch_related('variations')) ).prefetch_related('variations'))
@@ -79,7 +79,7 @@ def availability_handler(sender, **kwargs):
# Walk through all restriction objects applied to this item # Walk through all restriction objects applied to this item
for restriction in restrictions: for restriction in restrictions:
applied_to = list(restriction.variations.all()) applied_to = list(restriction.variations.current.all())
# Only take this restriction into consideration if it either # Only take this restriction into consideration if it either
# is directly applied to this variation OR is applied to all # is directly applied to this variation OR is applied to all