diff --git a/src/requirements.txt b/src/requirements.txt index 401637a530..8038937b1c 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -3,6 +3,7 @@ Django>=1.7 pytz django-bootstrap3 -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 django-compressor diff --git a/src/tixlbase/cache.py b/src/tixlbase/cache.py index 89ce3dc6be..c84369b256 100644 --- a/src/tixlbase/cache.py +++ b/src/tixlbase/cache.py @@ -22,7 +22,7 @@ class EventRelatedCache: def __init__(self, event: Event, cache: str='default'): self.cache = caches[cache] 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: # Race conditions can happen here, but should be very very rare. @@ -34,7 +34,7 @@ class EventRelatedCache: if prefix is None: prefix = int(time.time()) 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 # TODO: Use a more efficient, non-cryptographic hash algorithm key = hashlib.sha256(key.encode("UTF-8")).hexdigest() diff --git a/src/tixlbase/forms.py b/src/tixlbase/forms.py new file mode 100644 index 0000000000..ae46dc43da --- /dev/null +++ b/src/tixlbase/forms.py @@ -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 diff --git a/src/tixlbase/middleware.py b/src/tixlbase/middleware.py index c60c2ff3e5..40200069d5 100644 --- a/src/tixlbase/middleware.py +++ b/src/tixlbase/middleware.py @@ -30,7 +30,7 @@ class LocaleMiddleware(BaseLocaleMiddleware): url = resolve(request.path_info) if 'event' in url.kwargs and 'organizer' in url.kwargs: try: - request.event = Event.objects.get( + request.event = Event.objects.current.get( slug=url.kwargs['event'], organizer__slug=url.kwargs['organizer'], ) diff --git a/src/tixlbase/migrations/0001_initial.py b/src/tixlbase/migrations/0001_initial.py deleted file mode 100644 index d435f936d0..0000000000 --- a/src/tixlbase/migrations/0001_initial.py +++ /dev/null @@ -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')]), - ), - ] diff --git a/src/tixlbase/migrations/0002_auto_20140910_1628.py b/src/tixlbase/migrations/0002_auto_20140910_1628.py deleted file mode 100644 index d08eaa8e26..0000000000 --- a/src/tixlbase/migrations/0002_auto_20140910_1628.py +++ /dev/null @@ -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, - ), - ] diff --git a/src/tixlbase/migrations/0003_auto_20140910_1649.py b/src/tixlbase/migrations/0003_auto_20140910_1649.py deleted file mode 100644 index 826d1a1c9e..0000000000 --- a/src/tixlbase/migrations/0003_auto_20140910_1649.py +++ /dev/null @@ -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.'), - ), - ] diff --git a/src/tixlbase/migrations/0004_auto_20140911_2037.py b/src/tixlbase/migrations/0004_auto_20140911_2037.py deleted file mode 100644 index be2571dd55..0000000000 --- a/src/tixlbase/migrations/0004_auto_20140911_2037.py +++ /dev/null @@ -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), - ), - ] diff --git a/src/tixlbase/migrations/0005_auto_20140911_2052.py b/src/tixlbase/migrations/0005_auto_20140911_2052.py deleted file mode 100644 index e17580d85a..0000000000 --- a/src/tixlbase/migrations/0005_auto_20140911_2052.py +++ /dev/null @@ -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'), - ), - ] diff --git a/src/tixlbase/migrations/0006_auto_20140912_1855.py b/src/tixlbase/migrations/0006_auto_20140912_1855.py deleted file mode 100644 index 8546e2fd72..0000000000 --- a/src/tixlbase/migrations/0006_auto_20140912_1855.py +++ /dev/null @@ -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')]), - ), - ] diff --git a/src/tixlbase/migrations/0007_auto_20140914_1301.py b/src/tixlbase/migrations/0007_auto_20140914_1301.py deleted file mode 100644 index 2f12faca70..0000000000 --- a/src/tixlbase/migrations/0007_auto_20140914_1301.py +++ /dev/null @@ -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, - ), - ] diff --git a/src/tixlbase/migrations/0008_auto_20140914_1304.py b/src/tixlbase/migrations/0008_auto_20140914_1304.py deleted file mode 100644 index 6f75d80519..0000000000 --- a/src/tixlbase/migrations/0008_auto_20140914_1304.py +++ /dev/null @@ -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'), - ), - ] diff --git a/src/tixlbase/migrations/0009_auto_20140916_2120.py b/src/tixlbase/migrations/0009_auto_20140916_2120.py deleted file mode 100644 index 82612f4f1c..0000000000 --- a/src/tixlbase/migrations/0009_auto_20140916_2120.py +++ /dev/null @@ -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), - ), - ] diff --git a/src/tixlbase/migrations/0010_auto_20140927_1006.py b/src/tixlbase/migrations/0010_auto_20140927_1006.py deleted file mode 100644 index e119fbdc3f..0000000000 --- a/src/tixlbase/migrations/0010_auto_20140927_1006.py +++ /dev/null @@ -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, - ), - ] diff --git a/src/tixlbase/migrations/0011_auto_20140927_1013.py b/src/tixlbase/migrations/0011_auto_20140927_1013.py deleted file mode 100644 index 96b2a0d5b4..0000000000 --- a/src/tixlbase/migrations/0011_auto_20140927_1013.py +++ /dev/null @@ -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'), - ), - ] diff --git a/src/tixlbase/migrations/0012_auto_20140929_1935.py b/src/tixlbase/migrations/0012_auto_20140929_1935.py deleted file mode 100644 index 527a88195a..0000000000 --- a/src/tixlbase/migrations/0012_auto_20140929_1935.py +++ /dev/null @@ -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'), - ), - ] diff --git a/src/tixlbase/migrations/0013_propertyvalue_position.py b/src/tixlbase/migrations/0013_propertyvalue_position.py deleted file mode 100644 index 77b5091e9e..0000000000 --- a/src/tixlbase/migrations/0013_propertyvalue_position.py +++ /dev/null @@ -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, - ), - ] diff --git a/src/tixlbase/migrations/0014_auto_20141005_1037.py b/src/tixlbase/migrations/0014_auto_20141005_1037.py deleted file mode 100644 index 98694c1867..0000000000 --- a/src/tixlbase/migrations/0014_auto_20141005_1037.py +++ /dev/null @@ -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, - ), - ] diff --git a/src/tixlbase/migrations/0015_auto_20141006_2205.py b/src/tixlbase/migrations/0015_auto_20141006_2205.py deleted file mode 100644 index 5dee309cf0..0000000000 --- a/src/tixlbase/migrations/0015_auto_20141006_2205.py +++ /dev/null @@ -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'), - ), - ] diff --git a/src/tixlbase/migrations/0016_event_plugins.py b/src/tixlbase/migrations/0016_event_plugins.py deleted file mode 100644 index 753cc19e39..0000000000 --- a/src/tixlbase/migrations/0016_event_plugins.py +++ /dev/null @@ -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, - ), - ] diff --git a/src/tixlbase/migrations/0017_auto_20141017_2148.py b/src/tixlbase/migrations/0017_auto_20141017_2148.py deleted file mode 100644 index 0e14a1bdb9..0000000000 --- a/src/tixlbase/migrations/0017_auto_20141017_2148.py +++ /dev/null @@ -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.'), - ), - ] diff --git a/src/tixlbase/migrations/0018_auto_20141025_0908.py b/src/tixlbase/migrations/0018_auto_20141025_0908.py deleted file mode 100644 index 8220529200..0000000000 --- a/src/tixlbase/migrations/0018_auto_20141025_0908.py +++ /dev/null @@ -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'), - ), - ] diff --git a/src/tixlbase/migrations/__init__.py b/src/tixlbase/migrations/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/tixlbase/models.py b/src/tixlbase/models.py index 111509c1df..b09b570d94 100644 --- a/src/tixlbase/models.py +++ b/src/tixlbase/models.py @@ -6,6 +6,7 @@ from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, Permis from django.utils.translation import ugettext_lazy as _ from django.template.defaultfilters import date as _date from django.core.validators import RegexValidator +from versions.models import Versionable, VersionedForeignKey, VersionedManyToManyField from tixlbase.types import VariationDict @@ -141,7 +142,7 @@ class User(AbstractBaseUser, PermissionsMixin): return self.username -class Organizer(models.Model): +class Organizer(Versionable): """ This model represents an entity organizing events, like a company. Any organizer has a unique slug, which is a short name (alphanumeric, @@ -165,13 +166,13 @@ class Organizer(models.Model): return self.name -class OrganizerPermission(models.Model): +class OrganizerPermission(Versionable): """ The relation between an Organizer and an User who has permissions to access an organizer profile. """ - organizer = models.ForeignKey(Organizer) + organizer = VersionedForeignKey(Organizer) user = models.ForeignKey(User, related_name="organizer_perms") can_create_events = models.BooleanField( default=True, @@ -181,7 +182,6 @@ class OrganizerPermission(models.Model): class Meta: verbose_name = _("Organizer permission") verbose_name_plural = _("Organizer permissions") - unique_together = (("organizer", "user"),) def __str__(self): 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 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). """ - organizer = models.ForeignKey(Organizer, related_name="events", - on_delete=models.PROTECT) + organizer = VersionedForeignKey(Organizer, related_name="events", + on_delete=models.PROTECT) name = models.CharField(max_length=200, verbose_name=_("Name")) slug = models.CharField( @@ -284,7 +284,7 @@ class Event(models.Model): class Meta: verbose_name = _("Event") verbose_name_plural = _("Events") - unique_together = (("organizer", "slug"),) + # unique_together = (("organizer", "slug"),) # TODO: Enforce manually ordering = ("date_from", "name") def __str__(self): @@ -319,13 +319,13 @@ class Event(models.Model): return EventRelatedCache(self) -class EventPermission(models.Model): +class EventPermission(Versionable): """ The relation between an Event and an User who has permissions to access an event. """ - event = models.ForeignKey(Event) + event = VersionedForeignKey(Event) user = models.ForeignKey(User, related_name="event_perms") can_change_settings = models.BooleanField( default=True, @@ -339,7 +339,6 @@ class EventPermission(models.Model): class Meta: verbose_name = _("Event permission") verbose_name_plural = _("Event permissions") - unique_together = (("event", "user"),) def __str__(self): 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 """ - event = models.ForeignKey( + event = VersionedForeignKey( Event, on_delete=models.CASCADE, related_name='categories', @@ -373,20 +372,25 @@ class ItemCategory(models.Model): def __str__(self): 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: 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 Item. For example 'Size' would be a property associated with the item 'T-Shirt'. """ - event = models.ForeignKey( + event = VersionedForeignKey( Event, related_name="properties", ) @@ -402,19 +406,24 @@ class Property(models.Model): def __str__(self): 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: 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', this could be 'M' or 'L' """ - prop = models.ForeignKey( + prop = VersionedForeignKey( Property, on_delete=models.CASCADE, related_name="values" @@ -435,13 +444,18 @@ class PropertyValue(models.Model): def __str__(self): 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: 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 by custom information, e.g. "Attendee name" or "Attendee age". @@ -457,7 +471,7 @@ class Question(models.Model): (TYPE_BOOLEAN, _("Yes/No")), ) - event = models.ForeignKey( + event = VersionedForeignKey( Event, related_name="questions", ) @@ -481,13 +495,18 @@ class Question(models.Model): def __str__(self): 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: 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 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". Deleted items will not be shown anywhere. """ - event = models.ForeignKey( + event = VersionedForeignKey( Event, on_delete=models.PROTECT, related_name="items", verbose_name=_("Event"), ) - category = models.ForeignKey( + category = VersionedForeignKey( ItemCategory, on_delete=models.PROTECT, related_name="items", @@ -540,7 +559,7 @@ class Item(models.Model): verbose_name=_("Taxes included in percent"), max_digits=7, decimal_places=2 ) - properties = models.ManyToManyField( + properties = VersionedManyToManyField( Property, related_name='items', verbose_name=_("Properties"), @@ -551,7 +570,7 @@ class Item(models.Model): + '\'Variations\' tab to configure the details.' ) ) - questions = models.ManyToManyField( + questions = VersionedManyToManyField( Question, related_name='items', verbose_name=_("Questions"), @@ -570,14 +589,16 @@ class Item(models.Model): return self.name def save(self, *args, **kwargs): + super().save(*args, **kwargs) if self.event: self.event.get_cache().clear() - return super().save(*args, **kwargs) def delete(self): self.deleted = True 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]": """ @@ -593,26 +614,26 @@ class Item(models.Model): if use_cache and hasattr(self, '_get_all_variations_cache'): return self._get_all_variations_cache - all_variations = self.variations.all().prefetch_related("values") - all_properties = self.properties.all().prefetch_related("values") + all_variations = self.variations.current.all().prefetch_related("values") + all_properties = self.properties.current.all().prefetch_related("values") variations_cache = {} for var in all_variations: key = [] - for v in var.values.all(): - key.append((v.prop_id, v.pk)) + for v in var.values.current.all(): + key.append((v.prop_id, v.identity)) key = tuple(sorted(key)) variations_cache[key] = var 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: result.append(VariationDict()) continue key = [] var = VariationDict() for v in comb: - key.append((v.prop.pk, v.pk)) - var[v.prop.pk] = v + key.append((v.prop.identity, v.identity)) + var[v.prop.identity] = v key = tuple(sorted(key)) if key in variations_cache: var['variation'] = variations_cache[key] @@ -622,7 +643,7 @@ class Item(models.Model): return result -class ItemVariation(models.Model): +class ItemVariation(Versionable): """ A variation is an item combined with values for all properties 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. """ - item = models.ForeignKey( + item = VersionedForeignKey( Item, related_name='variations' ) - values = models.ManyToManyField( + values = VersionedManyToManyField( PropertyValue, related_name='variations', ) @@ -663,13 +684,18 @@ class ItemVariation(models.Model): verbose_name = _("Item variation") 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: 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 form field by default. @@ -690,31 +716,31 @@ class VariationsField(models.ManyToManyField): initial = defaults['initial'] if callable(initial): initial = initial() - defaults['initial'] = [i.pk for i in initial] + defaults['initial'] = [i.identity for i in initial] # Skip ManyToManyField in dependency chain 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 of Items or ItemVariations. This model is just an abstract base class to be extended by restriction plugins. """ - event = models.ForeignKey( + event = VersionedForeignKey( Event, on_delete=models.CASCADE, related_name="restrictions_%(app_label)s_%(class)s", verbose_name=_("Event"), ) - item = models.ForeignKey( + item = VersionedForeignKey( Item, blank=True, null=True, verbose_name=_("Item"), related_name="restrictions_%(app_label)s_%(class)s", ) variations = VariationsField( - ItemVariation, + 'tixlbase.ItemVariation', blank=True, verbose_name=_("Variations"), related_name="restrictions_%(app_label)s_%(class)s", @@ -725,13 +751,18 @@ class BaseRestriction(models.Model): verbose_name = _("Restriction") 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: 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 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 are being used as a fallback solution if redis is not available. """ - event = models.ForeignKey( + event = VersionedForeignKey( Event, on_delete=models.CASCADE, related_name="quotas", @@ -763,7 +794,7 @@ class Quota(models.Model): size = models.PositiveIntegerField( verbose_name=_("Total capacity") ) - items = models.ManyToManyField( + items = VersionedManyToManyField( Item, verbose_name=_("Item"), blank=True @@ -790,7 +821,7 @@ class Quota(models.Model): return self.name -class Order(models.Model): +class Order(Versionable): """ 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 @@ -817,7 +848,7 @@ class Order(models.Model): choices=STATUS_CHOICE, verbose_name=_("Status") ) - event = models.ForeignKey( + event = VersionedForeignKey( Event, verbose_name=_("Event") ) @@ -848,13 +879,13 @@ class Order(models.Model): verbose_name_plural = _("Orders") -class QuestionAnswer(models.Model): +class QuestionAnswer(Versionable): """ The answer to a Question, connected to an OrderPosition or CartPosition """ orderposition = models.ForeignKey('OrderPosition', null=True, blank=True) cartposition = models.ForeignKey('CartPosition', null=True, blank=True) - question = models.ForeignKey(Question) + question = VersionedForeignKey(Question) answer = models.TextField() @@ -866,15 +897,15 @@ class OrderPosition(models.Model): 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. """ - order = models.ForeignKey( + order = VersionedForeignKey( Order, verbose_name=_("Order") ) - item = models.ForeignKey( + item = VersionedForeignKey( Item, verbose_name=_("Item") ) - variation = models.ForeignKey( + variation = VersionedForeignKey( ItemVariation, null=True, blank=True, verbose_name=_("Variation") @@ -883,7 +914,7 @@ class OrderPosition(models.Model): decimal_places=2, max_digits=10, verbose_name=_("Price") ) - answers = models.ManyToManyField( + answers = VersionedManyToManyField( Question, through=QuestionAnswer, 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 the checkout process. """ - event = models.ForeignKey( + event = VersionedForeignKey( Event, verbose_name=_("Event") ) @@ -915,11 +946,11 @@ class CartPosition(models.Model): max_length=255, null=True, blank=True, verbose_name=_("Session key") ) - item = models.ForeignKey( + item = VersionedForeignKey( Item, verbose_name=_("Item") ) - variation = models.ForeignKey( + variation = VersionedForeignKey( ItemVariation, null=True, blank=True, verbose_name=_("Variation") diff --git a/src/tixlbase/types.py b/src/tixlbase/types.py index e2e4df23ab..6b229831c5 100644 --- a/src/tixlbase/types.py +++ b/src/tixlbase/types.py @@ -5,17 +5,16 @@ class VariationDict(dict): returned by ``Item.get_all_variations()`` to avoid duplicate code in the 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. This is in use because the variation dictionaries use property ids as key and have some special keys like 'variation'. """ - for i in self.items(): - if type(i[0]) is int: - yield i + return (i for i in self.items() if i[0] not in self.IGNORE_KEYS) def relevant_values(self) -> "list[PropertyValue]": """ @@ -24,9 +23,7 @@ class VariationDict(dict): This is in use because the variation dictionaries use property ids as key and have some special keys like 'variation'. """ - for i in self.items(): - if type(i[0]) is int: - yield i[1] + return (i[1] for i in self.items() if i[0] not in self.IGNORE_KEYS) def identify(self) -> str: """ diff --git a/src/tixlcontrol/middleware.py b/src/tixlcontrol/middleware.py index 4f0f39f67b..b38fa2fd59 100644 --- a/src/tixlcontrol/middleware.py +++ b/src/tixlcontrol/middleware.py @@ -46,11 +46,11 @@ class PermissionMiddleware: return redirect_to_login( 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") if 'event.' in url_name and 'event' in url.kwargs: try: - request.event = Event.objects.get( + request.event = Event.objects.current.get( slug=url.kwargs['event'], permitted__id__exact=request.user.id, organizer__slug=url.kwargs['organizer'], diff --git a/src/tixlcontrol/templates/tixlcontrol/item/base.html b/src/tixlcontrol/templates/tixlcontrol/item/base.html index df9e136eb3..7a7aa2c3d9 100644 --- a/src/tixlcontrol/templates/tixlcontrol/item/base.html +++ b/src/tixlcontrol/templates/tixlcontrol/item/base.html @@ -4,9 +4,9 @@ {% block content %}

{% trans "Modify item:" %} {{ item.name }}

{% block inside %} {% endblock %} diff --git a/src/tixlcontrol/templates/tixlcontrol/items/categories.html b/src/tixlcontrol/templates/tixlcontrol/items/categories.html index 99b7feb420..728a564da2 100644 --- a/src/tixlcontrol/templates/tixlcontrol/items/categories.html +++ b/src/tixlcontrol/templates/tixlcontrol/items/categories.html @@ -30,12 +30,12 @@ {% for c in categories %} - {{ c.name }} + {{ c.name }} - - + + - + {% endfor %} diff --git a/src/tixlcontrol/templates/tixlcontrol/items/category_delete.html b/src/tixlcontrol/templates/tixlcontrol/items/category_delete.html index 7cb6db8cd9..af6d03be01 100644 --- a/src/tixlcontrol/templates/tixlcontrol/items/category_delete.html +++ b/src/tixlcontrol/templates/tixlcontrol/items/category_delete.html @@ -6,12 +6,12 @@

{% trans "Delete item category" %}

{% csrf_token %} -

{% blocktrans %}Are you sure you want to the category {{ category.name }}?{% endblocktrans %}

+

{% blocktrans %}Are you sure you want to delete the category {{ category.name }}?{% endblocktrans %}

{% trans "Cancel" %} -
diff --git a/src/tixlcontrol/templates/tixlcontrol/items/index.html b/src/tixlcontrol/templates/tixlcontrol/items/index.html index 66ab70fff3..4459df7dd4 100644 --- a/src/tixlcontrol/templates/tixlcontrol/items/index.html +++ b/src/tixlcontrol/templates/tixlcontrol/items/index.html @@ -13,7 +13,8 @@ {% for i in items %} - {{ i.name }} + {{ i.name }} {{ i.category }} {% endfor %} diff --git a/src/tixlcontrol/templates/tixlcontrol/items/properties.html b/src/tixlcontrol/templates/tixlcontrol/items/properties.html index 9c92d6dbe2..c6bafd55f8 100644 --- a/src/tixlcontrol/templates/tixlcontrol/items/properties.html +++ b/src/tixlcontrol/templates/tixlcontrol/items/properties.html @@ -29,8 +29,10 @@ {% for p in properties %} - {{ p.name }} - + {{ p.name }} + {% endfor %} diff --git a/src/tixlcontrol/templates/tixlcontrol/items/property_delete.html b/src/tixlcontrol/templates/tixlcontrol/items/property_delete.html index d2b4b4ca99..c6f3ef822e 100644 --- a/src/tixlcontrol/templates/tixlcontrol/items/property_delete.html +++ b/src/tixlcontrol/templates/tixlcontrol/items/property_delete.html @@ -19,7 +19,7 @@ {% trans "Cancel" %} - diff --git a/src/tixlcontrol/templates/tixlcontrol/items/question_delete.html b/src/tixlcontrol/templates/tixlcontrol/items/question_delete.html index 16b2370bfe..762d3ae3fc 100644 --- a/src/tixlcontrol/templates/tixlcontrol/items/question_delete.html +++ b/src/tixlcontrol/templates/tixlcontrol/items/question_delete.html @@ -8,7 +8,7 @@ {% csrf_token %}

{% blocktrans %}Are you sure you want to the question {{ question }}?{% endblocktrans %}

{% if dependent|length > 0 %} -

{% blocktrans %}All answers to the question given by the buyers of the following tickets will be permanently lost.{% endblocktrans %}

+

{% blocktrans %}All answers to the question given by the buyers of the following tickets will be lost.{% endblocktrans %}

{% for item in dependent %}
  • {{ item.name }}
  • {% endfor %} @@ -17,7 +17,7 @@ {% trans "Cancel" %} - diff --git a/src/tixlcontrol/templates/tixlcontrol/items/questions.html b/src/tixlcontrol/templates/tixlcontrol/items/questions.html index 0e958d819c..3c949f5fc7 100644 --- a/src/tixlcontrol/templates/tixlcontrol/items/questions.html +++ b/src/tixlcontrol/templates/tixlcontrol/items/questions.html @@ -30,9 +30,11 @@ {% for q in questions %} - {{ q.question }} + {{ q.question }} {{ q.get_type_display }} - + {% endfor %} diff --git a/src/tixlcontrol/templates/tixlcontrol/items/quota_delete.html b/src/tixlcontrol/templates/tixlcontrol/items/quota_delete.html index 118bbec058..2e33ff06d0 100644 --- a/src/tixlcontrol/templates/tixlcontrol/items/quota_delete.html +++ b/src/tixlcontrol/templates/tixlcontrol/items/quota_delete.html @@ -17,7 +17,7 @@ {% trans "Cancel" %} - diff --git a/src/tixlcontrol/templates/tixlcontrol/items/quotas.html b/src/tixlcontrol/templates/tixlcontrol/items/quotas.html index 7d3ec2568f..e0c2d473aa 100644 --- a/src/tixlcontrol/templates/tixlcontrol/items/quotas.html +++ b/src/tixlcontrol/templates/tixlcontrol/items/quotas.html @@ -32,11 +32,11 @@ {% for q in quotas %} - {{ q.name }} + {{ q.name }} {{ q.size }} - + {% endfor %} diff --git a/src/tixlcontrol/urls.py b/src/tixlcontrol/urls.py index ce0f9cdf46..a4fd4cd10e 100644 --- a/src/tixlcontrol/urls.py +++ b/src/tixlcontrol/urls.py @@ -21,26 +21,26 @@ urlpatterns += patterns( url(r'^settings/$', event.EventUpdate.as_view(), name='event.settings'), url(r'^settings/plugins$', event.EventPlugins.as_view(), name='event.settings.plugins'), url(r'^items/$', item.ItemList.as_view(), name='event.items'), - url(r'^items/(?P\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'), - url(r'^items/(?P\d+)/variations$', item.ItemVariations.as_view(), name='event.item.variations'), - url(r'^items/(?P\d+)/restrictions$', item.ItemRestrictions.as_view(), name='event.item.restrictions'), + url(r'^items/(?P[0-9a-f-]+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'), + url(r'^items/(?P[0-9a-f-]+)/variations$', item.ItemVariations.as_view(), name='event.item.variations'), + url(r'^items/(?P[0-9a-f-]+)/restrictions$', item.ItemRestrictions.as_view(), name='event.item.restrictions'), url(r'^categories/$', item.CategoryList.as_view(), name='event.items.categories'), - url(r'^categories/(?P\d+)/delete$', item.CategoryDelete.as_view(), name='event.items.categories.delete'), - url(r'^categories/(?P\d+)/up$', item.category_move_up, name='event.items.categories.up'), - url(r'^categories/(?P\d+)/down$', item.category_move_down, name='event.items.categories.down'), - url(r'^categories/(?P\d+)/$', item.CategoryUpdate.as_view(), name='event.items.categories.edit'), + url(r'^categories/(?P[0-9a-f-]+)/delete$', item.CategoryDelete.as_view(), name='event.items.categories.delete'), + url(r'^categories/(?P[0-9a-f-]+)/up$', item.category_move_up, name='event.items.categories.up'), + url(r'^categories/(?P[0-9a-f-]+)/down$', item.category_move_down, name='event.items.categories.down'), + url(r'^categories/(?P[0-9a-f-]+)/$', item.CategoryUpdate.as_view(), name='event.items.categories.edit'), url(r'^categories/add$', item.CategoryCreate.as_view(), name='event.items.categories.add'), url(r'^questions/$', item.QuestionList.as_view(), name='event.items.questions'), - url(r'^questions/(?P\d+)/delete$', item.QuestionDelete.as_view(), name='event.items.questions.delete'), - url(r'^questions/(?P\d+)/$', item.QuestionUpdate.as_view(), name='event.items.questions.edit'), + url(r'^questions/(?P[0-9a-f-]+)/delete$', item.QuestionDelete.as_view(), name='event.items.questions.delete'), + url(r'^questions/(?P[0-9a-f-]+)/$', item.QuestionUpdate.as_view(), name='event.items.questions.edit'), url(r'^questions/add$', item.QuestionCreate.as_view(), name='event.items.questions.add'), url(r'^properties/$', item.PropertyList.as_view(), name='event.items.properties'), - url(r'^properties/(?P\d+)/$', item.PropertyUpdate.as_view(), name='event.items.properties.edit'), - url(r'^properties/(?P\d+)/delete$', item.PropertyDelete.as_view(), name='event.items.properties.delete'), + url(r'^properties/(?P[0-9a-f-]+)/$', item.PropertyUpdate.as_view(), name='event.items.properties.edit'), + url(r'^properties/(?P[0-9a-f-]+)/delete$', item.PropertyDelete.as_view(), name='event.items.properties.delete'), url(r'^properties/add$', item.PropertyCreate.as_view(), name='event.items.properties.add'), url(r'^quotas/$', item.QuotaList.as_view(), name='event.items.quotas'), - url(r'^quotas/(?P\d+)/$', item.QuotaUpdate.as_view(), name='event.items.quotas.edit'), - url(r'^quotas/(?P\d+)/delete$', item.QuotaDelete.as_view(), + url(r'^quotas/(?P[0-9a-f-]+)/$', item.QuotaUpdate.as_view(), name='event.items.quotas.edit'), + url(r'^quotas/(?P[0-9a-f-]+)/delete$', item.QuotaDelete.as_view(), name='event.items.quotas.delete'), url(r'^quotas/add$', item.QuotaCreate.as_view(), name='event.items.quotas.add'), ) diff --git a/src/tixlcontrol/views/event.py b/src/tixlcontrol/views/event.py index 03b2b8b023..63e7f16f52 100644 --- a/src/tixlcontrol/views/event.py +++ b/src/tixlcontrol/views/event.py @@ -7,12 +7,13 @@ from django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse from pytz import common_timezones +from tixlbase.forms import VersionedModelForm from tixlbase.models import Event from tixlcontrol.permissions import EventPermissionRequiredMixin -class EventUpdateForm(forms.ModelForm): +class EventUpdateForm(VersionedModelForm): timezone = forms.ChoiceField( choices=((a, a) for a in common_timezones), diff --git a/src/tixlcontrol/views/forms.py b/src/tixlcontrol/views/forms.py index 08f085ae65..dd8fb80b44 100644 --- a/src/tixlcontrol/views/forms.py +++ b/src/tixlcontrol/views/forms.py @@ -7,18 +7,19 @@ from django.utils.encoding import force_text from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.translation import ugettext as _ +from tixlbase.forms import VersionedModelForm from tixlbase.models import ItemVariation, PropertyValue, Item -class TolerantFormsetModelForm(forms.ModelForm): +class TolerantFormsetModelForm(VersionedModelForm): def has_changed(self) -> bool: """ Returns True if data differs from initial. Contrary to the default implementation, the ORDER field is being ignored. """ for name, field in self.fields.items(): - if name == 'ORDER': + if name == 'ORDER' or name == 'id': continue prefixed_name = self.add_prefix(name) data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name) @@ -37,7 +38,7 @@ class TolerantFormsetModelForm(forms.ModelForm): self._changed_data.append(name) continue # We're using a private API of Django here. This is not nice, but no problem as it seems - # like this will become a public API in Django 1.7. + # like this will become a public API in future Django. if field._has_changed(initial_value, data_value): return True return False @@ -77,7 +78,7 @@ class RestrictionInlineFormset(forms.BaseInlineFormSet): data, files, instance, save_as_new, prefix, queryset, **kwargs ) 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): form = self.form( @@ -139,7 +140,7 @@ class VariationsFieldRenderer(forms.widgets.CheckboxFieldRenderer): final_attrs['checked'] = 'checked' w = self.choice_input_class( self.name, self.value, self.attrs.copy(), - (variation['key'], variation[properties[0].pk].value), + (variation['key'], variation[properties[0].identity].value), i ) output.append(format_html('
  • {0}
  • ', force_text(w))) @@ -148,28 +149,28 @@ class VariationsFieldRenderer(forms.widgets.CheckboxFieldRenderer): elif dimension >= 2: # prop1 is the property on all the grid's y-axes 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 = properties[1] - prop2v = list(prop2.values.all()) + prop2v = list(prop2.values.current.all()) # Given an iterable of PropertyValue objects, this will return a # list of their primary keys, ordered by the primary keys of the # properties they belong to EXCEPT the value for the property prop2. # We'll see later why we need this. selector = lambda values: [ - v.pk for v in sorted(values, key=lambda v: v.prop.pk) - if v.prop.pk != prop2.pk + v.identity for v in sorted(values, key=lambda v: v.prop.identity) + if v.prop.identity != prop2.identity ] # Given a list of variations, this will sort them by their position # 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 # properties which are NOT on the axes of the grid because we # create one grid for any combination of them. - for gridrow in product(*[prop.values.all() for prop in properties[2:]]): + for gridrow in product(*[prop.values.current.all() for prop in properties[2:]]): if len(gridrow) > 0: output.append('') 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) return ( ( - v['variation'].pk if 'variation' in v else v.key(), + v['variation'].identity if 'variation' in v else v.key(), v ) for v in variations ) @@ -287,9 +288,9 @@ class VariationsField(forms.ModelMultipleChoiceField): for var in all_variations: key = [] for v in var.values.all(): - key.append((v.prop_id, v.pk)) + key.append((v.prop_id, v.identity)) key = tuple(sorted(key)) - variations_cache[key] = var.pk + variations_cache[key] = var.identity cleaned_value = [] @@ -303,7 +304,7 @@ class VariationsField(forms.ModelMultipleChoiceField): # Hash the combination in the same way as in our cache above key = [] 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)) if key in variations_cache: @@ -316,15 +317,15 @@ class VariationsField(forms.ModelMultipleChoiceField): # No ItemVariation present, create one! var = ItemVariation() - var.item = self.item + var.item_id = self.item.identity var.save() # Add the values to the ItemVariation object for pair in pk.split(","): prop, value = pair.split(":") try: var.values.add( - PropertyValue.objects.get( - pk=value, + PropertyValue.objects.current.get( + identity=value, prop_id=prop ) ) @@ -334,16 +335,16 @@ class VariationsField(forms.ModelMultipleChoiceField): code='invalid_pk_value', params={'pk': value}, ) - variations_cache[key] = var.pk - cleaned_value.append(str(var.pk)) + variations_cache[key] = var.identity + cleaned_value.append(str(var.identity)) else: # An ItemVariation id was given 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 - 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: if force_text(val) not in pks: raise ValidationError( diff --git a/src/tixlcontrol/views/item.py b/src/tixlcontrol/views/item.py index 94311edaa7..9afb314221 100644 --- a/src/tixlcontrol/views/item.py +++ b/src/tixlcontrol/views/item.py @@ -1,4 +1,5 @@ from itertools import product +from django.db import transaction from django.views.generic import ListView 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.core.urlresolvers import resolve, reverse from django.http import HttpResponseRedirect, HttpResponseForbidden -from django import forms from django.shortcuts import redirect from django.forms.models import inlineformset_factory +from tixlbase.forms import VersionedModelForm from tixlbase.models import ( Item, ItemCategory, Property, ItemVariation, PropertyValue, Question, Quota @@ -24,12 +25,12 @@ class ItemList(ListView): template_name = 'tixlcontrol/items/index.html' def get_queryset(self): - return Item.objects.filter( + return Item.objects.current.filter( event=self.request.event ).prefetch_related("category") -class CategoryForm(forms.ModelForm): +class CategoryForm(VersionedModelForm): class Meta: model = ItemCategory @@ -48,13 +49,15 @@ class CategoryDelete(EventPermissionRequiredMixin, DeleteView): def get_object(self, queryset=None): url = resolve(self.request.path_info) - return self.request.event.categories.get( - id=url.kwargs['category'] + return self.request.event.categories.current.get( + identity=url.kwargs['category'] ) def delete(self, request, *args, **kwargs): 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() self.object.delete() return HttpResponseRedirect(success_url) @@ -75,8 +78,8 @@ class CategoryUpdate(EventPermissionRequiredMixin, UpdateView): def get_object(self, queryset=None): url = resolve(self.request.path_info) - return self.request.event.categories.get( - id=url.kwargs['category'] + return self.request.event.categories.current.get( + identity=url.kwargs['category'] ) def get_success_url(self): @@ -110,14 +113,14 @@ class CategoryList(ListView): template_name = 'tixlcontrol/items/categories.html' 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): - category = request.event.categories.get( - id=category + category = request.event.categories.current.get( + identity=category ) - categories = list(request.event.categories.order_by("position")) + categories = list(request.event.categories.current.order_by("position")) index = categories.index(category) if index != 0 and up: @@ -155,12 +158,12 @@ class PropertyList(ListView): template_name = 'tixlcontrol/items/properties.html' def get_queryset(self): - return Property.objects.filter( + return Property.objects.current.filter( event=self.request.event ) -class PropertyForm(forms.ModelForm): +class PropertyForm(VersionedModelForm): class Meta: model = Property localized_fields = '__all__' @@ -187,8 +190,8 @@ class PropertyUpdate(EventPermissionRequiredMixin, UpdateView): def get_object(self, queryset=None): url = resolve(self.request.path_info) - return self.request.event.properties.get( - id=url.kwargs['property'] + return self.request.event.properties.current.get( + identity=url.kwargs['property'] ) def get_success_url(self): @@ -206,7 +209,9 @@ class PropertyUpdate(EventPermissionRequiredMixin, UpdateView): can_order=True, 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 def get_context_data(self, *args, **kwargs): @@ -215,9 +220,15 @@ class PropertyUpdate(EventPermissionRequiredMixin, UpdateView): return context def form_valid(self, form, formset): + for f in formset.deleted_forms: + f.instance.delete() + f.instance.pk = None + for i, f in enumerate(formset.ordered_forms): + if f.instance.pk is not None: + f.instance = f.instance.clone() f.instance.position = i - formset.save() + f.instance.save() return super().form_valid(form) def post(self, request, *args, **kwargs): @@ -288,18 +299,18 @@ class PropertyDelete(EventPermissionRequiredMixin, DeleteView): def get_context_data(self, *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() return context 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): if not hasattr(self, 'object') or not self.object: url = resolve(self.request.path_info) - self.object = self.request.event.properties.get( - id=url.kwargs['property'] + self.object = self.request.event.properties.current.get( + identity=url.kwargs['property'] ) return self.object @@ -324,10 +335,10 @@ class QuestionList(ListView): template_name = 'tixlcontrol/items/questions.html' 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: model = Question @@ -347,18 +358,17 @@ class QuestionDelete(EventPermissionRequiredMixin, DeleteView): def get_object(self, queryset=None): url = resolve(self.request.path_info) - return self.request.event.questions.get( - id=url.kwargs['question'] + return self.request.event.questions.current.get( + identity=url.kwargs['question'] ) def get_context_data(self, *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 def delete(self, request, *args, **kwargs): self.object = self.get_object() - self.object.items.update(category=None) success_url = self.get_success_url() self.object.delete() return HttpResponseRedirect(success_url) @@ -379,8 +389,8 @@ class QuestionUpdate(EventPermissionRequiredMixin, UpdateView): def get_object(self, queryset=None): url = resolve(self.request.path_info) - return self.request.event.questions.get( - id=url.kwargs['question'] + return self.request.event.questions.current.get( + identity=url.kwargs['question'] ) def get_success_url(self): @@ -414,12 +424,12 @@ class QuotaList(ListView): template_name = 'tixlcontrol/items/quotas.html' def get_queryset(self): - return Quota.objects.filter( + return Quota.objects.current.filter( event=self.request.event ) -class QuotaForm(forms.ModelForm): +class QuotaForm(VersionedModelForm): class Meta: model = Quota @@ -457,8 +467,8 @@ class QuotaUpdate(EventPermissionRequiredMixin, UpdateView): def get_object(self, queryset=None): url = resolve(self.request.path_info) - return self.request.event.quotas.get( - id=url.kwargs['quota'] + return self.request.event.quotas.current.get( + identity=url.kwargs['quota'] ) def get_success_url(self): @@ -476,18 +486,17 @@ class QuotaDelete(EventPermissionRequiredMixin, DeleteView): def get_object(self, queryset=None): url = resolve(self.request.path_info) - return self.request.event.quotas.get( - id=url.kwargs['quota'] + return self.request.event.quotas.current.get( + identity=url.kwargs['quota'] ) def get_context_data(self, *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 def delete(self, request, *args, **kwargs): self.object = self.get_object() - self.object.items.update(category=None) success_url = self.get_success_url() self.object.delete() return HttpResponseRedirect(success_url) @@ -506,20 +515,20 @@ class ItemDetailMixin(SingleObjectMixin): def get_object(self, queryset=None): if not hasattr(self, 'object') or not self.object: url = resolve(self.request.path_info) - self.item = self.request.event.items.get( - id=url.kwargs['item'] + self.item = self.request.event.items.current.get( + identity=url.kwargs['item'] ) self.object = self.item return self.object -class ItemUpdateFormGeneral(forms.ModelForm): +class ItemUpdateFormGeneral(VersionedModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['category'].queryset = self.instance.event.categories.all() - self.fields['properties'].queryset = self.instance.event.properties.all() - self.fields['questions'].queryset = self.instance.event.questions.all() + self.fields['category'].queryset = self.instance.event.categories.current.all() + self.fields['properties'].queryset = self.instance.event.properties.current.all() + self.fields['questions'].queryset = self.instance.event.questions.current.all() class Meta: model = Item @@ -546,11 +555,11 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie return reverse('control:event.item', kwargs={ 'organizer': self.request.event.organizer.slug, 'event': self.request.event.slug, - 'item': self.get_object().pk, + 'item': self.get_object().identity, }) + '?success=true' -class ItemVariationForm(forms.ModelForm): +class ItemVariationForm(VersionedModelForm): class Meta: model = ItemVariation @@ -580,13 +589,16 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView form = ItemVariationForm( data, instance=variation['variation'], - prefix=",".join([str(i.pk) for i in values]), + prefix=",".join([str(i.identity) for i in values]), ) else: + inst = ItemVariation(item=self.object) + inst.item_id = self.object.identity + inst.creation = True form = ItemVariationForm( data, - instance=ItemVariation(item=self.object), - prefix=",".join([str(i.pk) for i in values]), + instance=inst, + prefix=",".join([str(i.identity) for i in values]), ) form.values = values return form @@ -630,20 +642,20 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView # properties they belong to EXCEPT the value for the property prop2. # We'll see later why we need this. selector = lambda values: [ - v.pk for v in sorted(values, key=lambda v: v.prop.pk) - if v.prop.pk != prop2.pk + v.identity for v in sorted(values, key=lambda v: v.prop.identity) + if v.prop.identity != prop2.identity ] # Given a list of variations, this will sort them by their position # 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 # properties which are NOT on the axes of the grid because we # create one grid for any combination of them. - for gridrow in product(*[prop.values.all() for prop in self.properties[2:]]): + for gridrow in product(*[prop.values.current.all() for prop in self.properties[2:]]): grids = [] - for val1 in prop1.values.all(): + for val1 in prop1.values.current.all(): formrow = [] # We are now inside one of the rows of the grid and have to # select the variations to display in this row. In order to @@ -669,7 +681,7 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView def main(self, request, *args, **kwargs): 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.forms, self.forms_flat = self.get_forms() @@ -681,13 +693,16 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView def post(self, request, *args, **kwargs): self.main(request, *args, **kwargs) context = self.get_context_data(object=self.object) - for form in self.forms_flat: - if form.is_valid(): - if form.instance.pk is None: - form.save() - form.instance.values.add(*form.values) - else: + with transaction.atomic(): + for form in self.forms_flat: + if form.is_valid() and form.has_changed(): form.save() + if hasattr(form.instance, 'creation') and form.instance.creation: + # We need this special 'creation' field set to true in get_form + # for newly created items as cleanerversion does already set the + # primary key in its post_init hook + form.instance.values.add(*form.values) + # TODO: Redirect to success message return self.render_to_response(context) def get_template_names(self): @@ -762,5 +777,5 @@ class ItemRestrictions(ItemDetailMixin, EventPermissionRequiredMixin, TemplateVi return reverse('control:event.item.restrictions', kwargs={ 'organizer': self.request.event.organizer.slug, 'event': self.request.event.slug, - 'item': self.object.pk + 'item': self.object.identity }) + '?success=true' diff --git a/src/tixlcontrol/views/main.py b/src/tixlcontrol/views/main.py index c080e584f1..a53cecf5db 100644 --- a/src/tixlcontrol/views/main.py +++ b/src/tixlcontrol/views/main.py @@ -10,7 +10,7 @@ class EventList(ListView): template_name = 'tixlcontrol/events/index.html' def get_queryset(self): - return Event.objects.filter( + return Event.objects.current.filter( permitted__id__exact=self.request.user.pk ).prefetch_related( "organizer", diff --git a/src/tixlplugins/timerestriction/migrations/0001_initial.py b/src/tixlplugins/timerestriction/migrations/0001_initial.py deleted file mode 100644 index 817f266ca3..0000000000 --- a/src/tixlplugins/timerestriction/migrations/0001_initial.py +++ /dev/null @@ -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,), - ), - ] diff --git a/src/tixlplugins/timerestriction/migrations/0002_auto_20141013_1811.py b/src/tixlplugins/timerestriction/migrations/0002_auto_20141013_1811.py deleted file mode 100644 index fea843bfcd..0000000000 --- a/src/tixlplugins/timerestriction/migrations/0002_auto_20141013_1811.py +++ /dev/null @@ -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'), - ), - ] diff --git a/src/tixlplugins/timerestriction/migrations/0003_auto_20141013_1811.py b/src/tixlplugins/timerestriction/migrations/0003_auto_20141013_1811.py deleted file mode 100644 index fbdaeaad88..0000000000 --- a/src/tixlplugins/timerestriction/migrations/0003_auto_20141013_1811.py +++ /dev/null @@ -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', - ), - ] diff --git a/src/tixlplugins/timerestriction/migrations/0004_auto_20141017_2148.py b/src/tixlplugins/timerestriction/migrations/0004_auto_20141017_2148.py deleted file mode 100644 index fb386bee8c..0000000000 --- a/src/tixlplugins/timerestriction/migrations/0004_auto_20141017_2148.py +++ /dev/null @@ -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'), - ), - ] diff --git a/src/tixlplugins/timerestriction/migrations/__init__.py b/src/tixlplugins/timerestriction/migrations/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/tixlplugins/timerestriction/signals.py b/src/tixlplugins/timerestriction/signals.py index f2ac32c4f9..2c75f480e0 100644 --- a/src/tixlplugins/timerestriction/signals.py +++ b/src/tixlplugins/timerestriction/signals.py @@ -20,7 +20,7 @@ def availability_handler(sender, **kwargs): context = kwargs['context'] # NOQA # Fetch all restriction objects applied to this item - restrictions = list(TimeRestriction.objects.filter( + restrictions = list(TimeRestriction.objects.current.filter( item=item, ).prefetch_related('variations')) @@ -79,7 +79,7 @@ def availability_handler(sender, **kwargs): # Walk through all restriction objects applied to this item 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 # is directly applied to this variation OR is applied to all diff --git a/src/tixlpresale/migrations/__init__.py b/src/tixlpresale/migrations/__init__.py deleted file mode 100644 index e69de29bb2..0000000000