diff --git a/doc/development/models.rst b/doc/development/models.rst index d581bf5da8..0bbf533607 100644 --- a/doc/development/models.rst +++ b/doc/development/models.rst @@ -39,19 +39,12 @@ Items .. autoclass:: pretix.base.models.ItemCategory :members: -.. autoclass:: pretix.base.models.Property - :members: - -.. autoclass:: pretix.base.models.PropertyValue +.. autoclass:: pretix.base.models.ItemVariation :members: .. autoclass:: pretix.base.models.Question :members: -.. autoclass:: pretix.base.models.ItemVariation - :members: - :exclude-members: add_values_from_string - .. autoclass:: pretix.base.models.Quota :members: diff --git a/src/make_testdata.py b/src/make_testdata.py index 93f2c1dc81..b78251098b 100644 --- a/src/make_testdata.py +++ b/src/make_testdata.py @@ -52,24 +52,9 @@ item_shirt = Item.objects.create( event=event, category=cat_merch, name='T-Shirt', default_price=15, tax_rate=19 ) -size_prop = Property.objects.create( - event=event, name='Size', item=item_shirt -) -size_s = PropertyValue.objects.create( - prop=size_prop, value='S' -) -size_l = PropertyValue.objects.create( - prop=size_prop, value='L' -) -size_m = PropertyValue.objects.create( - prop=size_prop, value='M' -) -var_s = ItemVariation.objects.create(item=item_shirt) -var_s.values.add(size_s) -var_m = ItemVariation.objects.create(item=item_shirt) -var_m.values.add(size_m) -var_l = ItemVariation.objects.create(item=item_shirt) -var_l.values.add(size_l) +var_s = ItemVariation.objects.create(item=item_shirt, value='S') +var_m = ItemVariation.objects.create(item=item_shirt, value='M') +var_l = ItemVariation.objects.create(item=item_shirt, value='L') ticket_quota = Quota.objects.create( event=event, name='Ticket quota', size=400, ) diff --git a/src/pretix/base/exporter.py b/src/pretix/base/exporter.py index b66a447b24..0654d7f08f 100644 --- a/src/pretix/base/exporter.py +++ b/src/pretix/base/exporter.py @@ -120,7 +120,7 @@ class JSONExporter(BaseExporter): { 'code': order.code, 'status': order.status, - 'user': order.user.email, + 'user': order.email, 'datetime': order.datetime, 'payment_fee': order.payment_fee, 'total': order.total, @@ -140,8 +140,7 @@ class JSONExporter(BaseExporter): } for position in order.positions.all() ] } for order in - self.event.orders.all().prefetch_related('positions', 'positions__answers').select_related( - 'user') + self.event.orders.all().prefetch_related('positions', 'positions__answers') ], 'quotas': [ { diff --git a/src/pretix/base/i18n.py b/src/pretix/base/i18n.py index b6d86cc7cc..7a0532a651 100644 --- a/src/pretix/base/i18n.py +++ b/src/pretix/base/i18n.py @@ -5,6 +5,7 @@ from django import forms from django.conf import settings from django.core.serializers.json import DjangoJSONEncoder from django.db.models import Model, QuerySet, TextField +from django.forms import BaseModelFormSet from django.utils import translation from django.utils.safestring import mark_safe from typing import Dict, List @@ -251,3 +252,29 @@ class I18nJSONEncoder(DjangoJSONEncoder): return {'type': obj.__class__.__name__, 'id': obj.id} else: return super().default(obj) + + +class I18nFormSet(BaseModelFormSet): + """ + This is equivalent to a normal BaseModelFormset, but cares for the special needs + of I18nForms (see there for more information). + """ + + def __init__(self, *args, **kwargs): + self.event = kwargs.pop('event', None) + super().__init__(*args, **kwargs) + + def _construct_form(self, i, **kwargs): + kwargs['event'] = self.event + return super()._construct_form(i, **kwargs) + + @property + def empty_form(self): + form = self.form( + auto_id=self.auto_id, + prefix=self.add_prefix('__prefix__'), + empty_permitted=True, + event=self.event + ) + self.add_fields(form, None) + return form diff --git a/src/pretix/base/migrations/0001_initial.py b/src/pretix/base/migrations/0001_initial.py index 91e65ce6c5..17aae79ca1 100644 --- a/src/pretix/base/migrations/0001_initial.py +++ b/src/pretix/base/migrations/0001_initial.py @@ -1,19 +1,9 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -import uuid - -import django.core.validators -import django.db.models.deletion -from django.conf import settings from django.contrib.auth.hashers import make_password from django.db import migrations, models -import pretix.base.i18n -import pretix.base.models.base -import pretix.base.models.items -import pretix.base.models.orders - def initial_user(apps, schema_editor): User = apps.get_model("pretixbase", "User") @@ -54,335 +44,5 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'Users', }, ), - migrations.CreateModel( - name='CachedFile', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('expires', models.DateTimeField(blank=True, null=True)), - ('date', models.DateTimeField(blank=True, null=True)), - ('filename', models.CharField(max_length=255)), - ('type', models.CharField(max_length=255)), - ('file', models.FileField(blank=True, null=True, upload_to=pretix.base.models.base.cachedfile_name)), - ], - ), - migrations.CreateModel( - name='CachedTicket', - fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), - ('provider', models.CharField(max_length=255)), - ('cachedfile', models.ForeignKey(to='pretixbase.CachedFile')), - ], - ), - migrations.CreateModel( - name='CartPosition', - fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), - ('cart_id', models.CharField(verbose_name='Cart ID (e.g. session key)', max_length=255, blank=True, null=True)), - ('price', models.DecimalField(verbose_name='Price', max_digits=10, decimal_places=2)), - ('datetime', models.DateTimeField(verbose_name='Date', auto_now_add=True)), - ('expires', models.DateTimeField(verbose_name='Expiration date')), - ('attendee_name', models.CharField(verbose_name='Attendee name', max_length=255, blank=True, null=True, help_text='Empty, if this product is not an admission ticket')), - ], - options={ - 'verbose_name': 'Cart position', - 'verbose_name_plural': 'Cart positions', - }, - bases=(pretix.base.models.orders.ObjectWithAnswers, models.Model), - ), - migrations.CreateModel( - name='Event', - fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), - ('name', pretix.base.i18n.I18nCharField(verbose_name='Name', max_length=200)), - ('slug', models.SlugField(verbose_name='Slug', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], 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.')), - ('currency', models.CharField(verbose_name='Default currency', default='EUR', max_length=10)), - ('date_from', models.DateTimeField(verbose_name='Event start time')), - ('date_to', models.DateTimeField(verbose_name='Event end time', blank=True, null=True)), - ('is_public', models.BooleanField(verbose_name='Visible in public lists', default=False, help_text="If selected, this event may show up on the ticket system's start page or an organization profile.")), - ('presale_end', models.DateTimeField(verbose_name='End of presale', help_text='No products will be sold after this date.', blank=True, null=True)), - ('presale_start', models.DateTimeField(verbose_name='Start of presale', help_text='No products will be sold before this date.', blank=True, null=True)), - ('plugins', models.TextField(verbose_name='Plugins', blank=True, null=True)), - ], - options={ - 'verbose_name': 'Event', - 'verbose_name_plural': 'Events', - 'ordering': ('date_from', 'name'), - }, - ), - migrations.CreateModel( - name='EventLock', - fields=[ - ('event', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('date', models.DateTimeField(auto_now=True)), - ('token', models.UUIDField(default=uuid.uuid4)), - ], - ), - migrations.CreateModel( - name='EventPermission', - fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), - ('can_change_settings', models.BooleanField(verbose_name='Can change event settings', default=True)), - ('can_change_items', models.BooleanField(verbose_name='Can change product settings', default=True)), - ('can_view_orders', models.BooleanField(verbose_name='Can view orders', default=True)), - ('can_change_permissions', models.BooleanField(verbose_name='Can change permissions', default=True)), - ('can_change_orders', models.BooleanField(verbose_name='Can change orders', default=True)), - ('event', models.ForeignKey(to='pretixbase.Event', related_name='user_perms')), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_perms')), - ], - options={ - 'verbose_name': 'Event permission', - 'verbose_name_plural': 'Event permissions', - }, - ), - migrations.CreateModel( - name='EventSetting', - fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), - ('key', models.CharField(max_length=255)), - ('value', models.TextField()), - ('object', models.ForeignKey(to='pretixbase.Event', related_name='setting_objects')), - ], - ), - migrations.CreateModel( - name='Item', - fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), - ('name', pretix.base.i18n.I18nCharField(verbose_name='Item name', max_length=255)), - ('active', models.BooleanField(verbose_name='Active', default=True)), - ('description', pretix.base.i18n.I18nTextField(verbose_name='Description', help_text='This is shown below the product name in lists.', blank=True, null=True)), - ('default_price', models.DecimalField(verbose_name='Default price', max_digits=7, decimal_places=2, null=True)), - ('tax_rate', models.DecimalField(verbose_name='Taxes included in percent', max_digits=7, blank=True, null=True, decimal_places=2)), - ('admission', models.BooleanField(verbose_name='Is an admission ticket', default=False, help_text='Whether or not buying this product allows a person to enter your event')), - ('position', models.IntegerField(default=0)), - ('picture', models.ImageField(verbose_name='Product picture', blank=True, null=True, upload_to=pretix.base.models.items.itempicture_upload_to)), - ('available_from', models.DateTimeField(verbose_name='Available from', help_text='This product will not be sold before the given date.', blank=True, null=True)), - ('available_until', models.DateTimeField(verbose_name='Available until', help_text='This product will not be sold after the given date.', blank=True, null=True)), - ], - options={ - 'verbose_name': 'Product', - 'verbose_name_plural': 'Products', - 'ordering': ('category__position', 'category', 'position'), - }, - ), - migrations.CreateModel( - name='ItemCategory', - fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), - ('name', pretix.base.i18n.I18nCharField(verbose_name='Category name', max_length=255)), - ('position', models.IntegerField(default=0)), - ('event', models.ForeignKey(to='pretixbase.Event', related_name='categories')), - ], - options={ - 'verbose_name': 'Product category', - 'verbose_name_plural': 'Product categories', - 'ordering': ('position', 'id'), - }, - ), - migrations.CreateModel( - name='ItemVariation', - fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), - ('active', models.BooleanField(verbose_name='Active', default=True)), - ('default_price', models.DecimalField(verbose_name='Default price', max_digits=7, blank=True, null=True, decimal_places=2)), - ('item', models.ForeignKey(to='pretixbase.Item', related_name='variations')), - ], - options={ - 'verbose_name': 'Product variation', - 'verbose_name_plural': 'Product variations', - }, - ), - migrations.CreateModel( - name='Order', - fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), - ('code', models.CharField(verbose_name='Order code', max_length=16)), - ('status', models.CharField(verbose_name='Status', max_length=3, choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')])), - ('email', models.EmailField(verbose_name='E-mail', max_length=254, blank=True, null=True)), - ('locale', models.CharField(verbose_name='Locale', max_length=32, blank=True, null=True)), - ('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)), - ('datetime', models.DateTimeField(verbose_name='Date')), - ('expires', models.DateTimeField(verbose_name='Expiration date')), - ('payment_date', models.DateTimeField(verbose_name='Payment date', blank=True, null=True)), - ('payment_provider', models.CharField(verbose_name='Payment provider', max_length=255, blank=True, null=True)), - ('payment_fee', models.DecimalField(verbose_name='Payment method fee', default=0, max_digits=10, decimal_places=2)), - ('payment_info', models.TextField(verbose_name='Payment information', blank=True, null=True)), - ('payment_manual', models.BooleanField(verbose_name='Payment state was manually modified', default=False)), - ('total', models.DecimalField(verbose_name='Total amount', max_digits=10, decimal_places=2)), - ('event', models.ForeignKey(to='pretixbase.Event', verbose_name='Event', related_name='orders')), - ], - options={ - 'verbose_name': 'Order', - 'verbose_name_plural': 'Orders', - 'ordering': ('-datetime',), - }, - ), - migrations.CreateModel( - name='OrderPosition', - fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), - ('price', models.DecimalField(verbose_name='Price', max_digits=10, decimal_places=2)), - ('attendee_name', models.CharField(verbose_name='Attendee name', max_length=255, blank=True, null=True, help_text='Empty, if this product is not an admission ticket')), - ('item', models.ForeignKey(to='pretixbase.Item', on_delete=django.db.models.deletion.PROTECT, verbose_name='Item', related_name='positions')), - ('order', models.ForeignKey(to='pretixbase.Order', on_delete=django.db.models.deletion.PROTECT, verbose_name='Order', related_name='positions')), - ('variation', models.ForeignKey(blank=True, to='pretixbase.ItemVariation', on_delete=django.db.models.deletion.PROTECT, verbose_name='Variation', null=True)), - ], - options={ - 'verbose_name': 'Order position', - 'verbose_name_plural': 'Order positions', - }, - bases=(pretix.base.models.orders.ObjectWithAnswers, models.Model), - ), - migrations.CreateModel( - name='Organizer', - fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), - ('name', models.CharField(verbose_name='Name', max_length=200)), - ('slug', models.SlugField(verbose_name='Slug', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], 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.')), - ], - options={ - 'verbose_name': 'Organizer', - 'verbose_name_plural': 'Organizers', - 'ordering': ('name',), - }, - ), - migrations.CreateModel( - name='OrganizerPermission', - fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), - ('can_create_events', models.BooleanField(verbose_name='Can create events', default=True)), - ('organizer', models.ForeignKey(to='pretixbase.Organizer', related_name='user_perms')), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='organizer_perms')), - ], - options={ - 'verbose_name': 'Organizer permission', - 'verbose_name_plural': 'Organizer permissions', - }, - ), - migrations.CreateModel( - name='OrganizerSetting', - fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), - ('key', models.CharField(max_length=255)), - ('value', models.TextField()), - ('object', models.ForeignKey(to='pretixbase.Organizer', related_name='setting_objects')), - ], - ), - migrations.CreateModel( - name='Property', - fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), - ('name', pretix.base.i18n.I18nCharField(verbose_name='Property name', max_length=250)), - ('event', models.ForeignKey(to='pretixbase.Event', related_name='properties')), - ('item', models.ForeignKey(blank=True, to='pretixbase.Item', null=True, related_name='properties')), - ], - options={ - 'verbose_name': 'Product property', - 'verbose_name_plural': 'Product properties', - }, - ), - migrations.CreateModel( - name='PropertyValue', - fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), - ('value', pretix.base.i18n.I18nCharField(verbose_name='Value', max_length=250)), - ('position', models.IntegerField(default=0)), - ('prop', models.ForeignKey(to='pretixbase.Property', related_name='values')), - ], - options={ - 'verbose_name': 'Property value', - 'verbose_name_plural': 'Property values', - 'ordering': ('position', 'id'), - }, - ), - migrations.CreateModel( - name='Question', - fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), - ('question', pretix.base.i18n.I18nTextField(verbose_name='Question')), - ('type', models.CharField(verbose_name='Question type', max_length=5, choices=[('N', 'Number'), ('S', 'Text (one line)'), ('T', 'Multiline text'), ('B', 'Yes/No')])), - ('required', models.BooleanField(verbose_name='Required question', default=False)), - ('event', models.ForeignKey(to='pretixbase.Event', related_name='questions')), - ('items', models.ManyToManyField(verbose_name='Products', help_text='This question will be asked to buyers of the selected products', blank=True, to='pretixbase.Item', related_name='questions')), - ], - options={ - 'verbose_name': 'Question', - 'verbose_name_plural': 'Questions', - }, - ), - migrations.CreateModel( - name='QuestionAnswer', - fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), - ('answer', models.TextField()), - ('cartposition', models.ForeignKey(blank=True, to='pretixbase.CartPosition', null=True, related_name='answers')), - ('orderposition', models.ForeignKey(blank=True, to='pretixbase.OrderPosition', null=True, related_name='answers')), - ('question', models.ForeignKey(to='pretixbase.Question', related_name='answers')), - ], - ), - migrations.CreateModel( - name='Quota', - fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), - ('name', models.CharField(verbose_name='Name', max_length=200)), - ('size', models.PositiveIntegerField(verbose_name='Total capacity', help_text='Leave empty for an unlimited number of tickets.', blank=True, null=True)), - ('event', models.ForeignKey(to='pretixbase.Event', verbose_name='Event', related_name='quotas')), - ('items', models.ManyToManyField(verbose_name='Item', to='pretixbase.Item', blank=True, related_name='quotas')), - ('variations', pretix.base.models.items.VariationsField(verbose_name='Variations', to='pretixbase.ItemVariation', blank=True, related_name='quotas')), - ], - options={ - 'verbose_name': 'Quota', - 'verbose_name_plural': 'Quotas', - }, - ), - migrations.AddField( - model_name='organizer', - name='permitted', - field=models.ManyToManyField(to=settings.AUTH_USER_MODEL, through='pretixbase.OrganizerPermission', related_name='organizers'), - ), - migrations.AddField( - model_name='itemvariation', - name='values', - field=models.ForeignKey(to='pretixbase.PropertyValue', related_name='variations'), - ), - migrations.AddField( - model_name='item', - name='category', - field=models.ForeignKey(blank=True, to='pretixbase.ItemCategory', on_delete=django.db.models.deletion.PROTECT, verbose_name='Category', null=True, related_name='items'), - ), - migrations.AddField( - model_name='item', - name='event', - field=models.ForeignKey(to='pretixbase.Event', on_delete=django.db.models.deletion.PROTECT, verbose_name='Event', related_name='items'), - ), - migrations.AddField( - model_name='event', - name='organizer', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Organizer', related_name='events'), - ), - migrations.AddField( - model_name='event', - name='permitted', - field=models.ManyToManyField(to=settings.AUTH_USER_MODEL, through='pretixbase.EventPermission', related_name='events'), - ), - migrations.AddField( - model_name='cartposition', - name='event', - field=models.ForeignKey(verbose_name='Event', to='pretixbase.Event'), - ), - migrations.AddField( - model_name='cartposition', - name='item', - field=models.ForeignKey(verbose_name='Item', to='pretixbase.Item'), - ), - migrations.AddField( - model_name='cartposition', - name='variation', - field=models.ForeignKey(blank=True, to='pretixbase.ItemVariation', verbose_name='Variation', null=True), - ), - migrations.AddField( - model_name='cachedticket', - name='order', - field=models.ForeignKey(to='pretixbase.Order'), - ), migrations.RunPython(initial_user), ] diff --git a/src/pretix/base/migrations/0002_auto_20151212_1123.py b/src/pretix/base/migrations/0002_auto_20151212_1123.py deleted file mode 100644 index 45708bd5cf..0000000000 --- a/src/pretix/base/migrations/0002_auto_20151212_1123.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('pretixbase', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='itemvariation', - name='values', - ), - migrations.AddField( - model_name='itemvariation', - name='values', - field=models.ManyToManyField(related_name='variations', to='pretixbase.PropertyValue'), - ), - ] diff --git a/src/pretix/base/migrations/0002_auto_20151213_1144.py b/src/pretix/base/migrations/0002_auto_20151213_1144.py new file mode 100644 index 0000000000..cc6e400834 --- /dev/null +++ b/src/pretix/base/migrations/0002_auto_20151213_1144.py @@ -0,0 +1,347 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2015-12-13 11:44 +from __future__ import unicode_literals + +import uuid + +import django.core.validators +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + +import pretix.base.i18n +import pretix.base.models.base +import pretix.base.models.items +import pretix.base.models.orders + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('pretixbase', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='CachedFile', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('expires', models.DateTimeField(blank=True, null=True)), + ('date', models.DateTimeField(blank=True, null=True)), + ('filename', models.CharField(max_length=255)), + ('type', models.CharField(max_length=255)), + ('file', models.FileField(blank=True, null=True, upload_to=pretix.base.models.base.cachedfile_name)), + ], + ), + migrations.CreateModel( + name='CachedTicket', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('provider', models.CharField(max_length=255)), + ('cachedfile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.CachedFile')), + ], + ), + migrations.CreateModel( + name='CartPosition', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('cart_id', models.CharField(blank=True, max_length=255, null=True, verbose_name='Cart ID (e.g. session key)')), + ('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Price')), + ('datetime', models.DateTimeField(auto_now_add=True, verbose_name='Date')), + ('expires', models.DateTimeField(verbose_name='Expiration date')), + ('attendee_name', models.CharField(blank=True, help_text='Empty, if this product is not an admission ticket', max_length=255, null=True, verbose_name='Attendee name')), + ], + options={ + 'verbose_name_plural': 'Cart positions', + 'verbose_name': 'Cart position', + }, + bases=(pretix.base.models.orders.ObjectWithAnswers, models.Model), + ), + migrations.CreateModel( + name='Event', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', pretix.base.i18n.I18nCharField(max_length=200, verbose_name='Name')), + ('slug', models.SlugField(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')), + ('currency', models.CharField(default='EUR', max_length=10, verbose_name='Default currency')), + ('date_from', models.DateTimeField(verbose_name='Event start time')), + ('date_to', models.DateTimeField(blank=True, null=True, verbose_name='Event end time')), + ('is_public', models.BooleanField(default=False, help_text="If selected, this event may show up on the ticket system's start page or an organization profile.", verbose_name='Visible in public lists')), + ('presale_end', models.DateTimeField(blank=True, help_text='No products will be sold after this date.', null=True, verbose_name='End of presale')), + ('presale_start', models.DateTimeField(blank=True, help_text='No products will be sold before this date.', null=True, verbose_name='Start of presale')), + ('plugins', models.TextField(blank=True, null=True, verbose_name='Plugins')), + ], + options={ + 'ordering': ('date_from', 'name'), + 'verbose_name_plural': 'Events', + 'verbose_name': 'Event', + }, + bases=(models.Model, pretix.base.models.base.LoggingMixin), + ), + migrations.CreateModel( + name='EventLock', + fields=[ + ('event', models.CharField(max_length=36, primary_key=True, serialize=False)), + ('date', models.DateTimeField(auto_now=True)), + ('token', models.UUIDField(default=uuid.uuid4)), + ], + ), + migrations.CreateModel( + name='EventPermission', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('can_change_settings', models.BooleanField(default=True, verbose_name='Can change event settings')), + ('can_change_items', models.BooleanField(default=True, verbose_name='Can change product settings')), + ('can_view_orders', models.BooleanField(default=True, verbose_name='Can view orders')), + ('can_change_permissions', models.BooleanField(default=True, verbose_name='Can change permissions')), + ('can_change_orders', models.BooleanField(default=True, verbose_name='Can change orders')), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_perms', to='pretixbase.Event')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_perms', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name_plural': 'Event permissions', + 'verbose_name': 'Event permission', + }, + ), + migrations.CreateModel( + name='EventSetting', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(max_length=255)), + ('value', models.TextField()), + ('object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='setting_objects', to='pretixbase.Event')), + ], + ), + migrations.CreateModel( + name='Item', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', pretix.base.i18n.I18nCharField(max_length=255, verbose_name='Item name')), + ('active', models.BooleanField(default=True, verbose_name='Active')), + ('description', pretix.base.i18n.I18nTextField(blank=True, help_text='This is shown below the product name in lists.', null=True, verbose_name='Description')), + ('default_price', models.DecimalField(decimal_places=2, max_digits=7, null=True, verbose_name='Default price')), + ('tax_rate', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True, verbose_name='Taxes included in percent')), + ('admission', models.BooleanField(default=False, help_text='Whether or not buying this product allows a person to enter your event', verbose_name='Is an admission ticket')), + ('position', models.IntegerField(default=0)), + ('picture', models.ImageField(blank=True, null=True, upload_to=pretix.base.models.items.itempicture_upload_to, verbose_name='Product picture')), + ('available_from', models.DateTimeField(blank=True, help_text='This product will not be sold before the given date.', null=True, verbose_name='Available from')), + ('available_until', models.DateTimeField(blank=True, help_text='This product will not be sold after the given date.', null=True, verbose_name='Available until')), + ], + options={ + 'ordering': ('category__position', 'category', 'position'), + 'verbose_name_plural': 'Products', + 'verbose_name': 'Product', + }, + bases=(models.Model, pretix.base.models.base.LoggingMixin), + ), + migrations.CreateModel( + name='ItemCategory', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', pretix.base.i18n.I18nCharField(max_length=255, verbose_name='Category name')), + ('position', models.IntegerField(default=0)), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='categories', to='pretixbase.Event')), + ], + options={ + 'ordering': ('position', 'id'), + 'verbose_name_plural': 'Product categories', + 'verbose_name': 'Product category', + }, + bases=(models.Model, pretix.base.models.base.LoggingMixin), + ), + migrations.CreateModel( + name='ItemVariation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('value', pretix.base.i18n.I18nCharField(max_length=255, verbose_name='Description')), + ('active', models.BooleanField(default=True, verbose_name='Active')), + ('position', models.PositiveIntegerField(default=0, verbose_name='Position')), + ('default_price', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True, verbose_name='Default price')), + ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='variations', to='pretixbase.Item')), + ], + options={ + 'ordering': ('position',), + 'verbose_name_plural': 'Product variations', + 'verbose_name': 'Product variation', + }, + ), + migrations.CreateModel( + name='LogEntry', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField()), + ('datetime', models.DateTimeField(auto_now_add=True)), + ('action_type', models.CharField(max_length=255)), + ('data', models.TextField(default='{}')), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=16, verbose_name='Order code')), + ('status', models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3, verbose_name='Status')), + ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-mail')), + ('locale', models.CharField(blank=True, max_length=32, null=True, verbose_name='Locale')), + ('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)), + ('datetime', models.DateTimeField(verbose_name='Date')), + ('expires', models.DateTimeField(verbose_name='Expiration date')), + ('payment_date', models.DateTimeField(blank=True, null=True, verbose_name='Payment date')), + ('payment_provider', models.CharField(blank=True, max_length=255, null=True, verbose_name='Payment provider')), + ('payment_fee', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Payment method fee')), + ('payment_info', models.TextField(blank=True, null=True, verbose_name='Payment information')), + ('payment_manual', models.BooleanField(default=False, verbose_name='Payment state was manually modified')), + ('total', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Total amount')), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to='pretixbase.Event', verbose_name='Event')), + ], + options={ + 'ordering': ('-datetime',), + 'verbose_name_plural': 'Orders', + 'verbose_name': 'Order', + }, + bases=(models.Model, pretix.base.models.base.LoggingMixin), + ), + migrations.CreateModel( + name='OrderPosition', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Price')), + ('attendee_name', models.CharField(blank=True, help_text='Empty, if this product is not an admission ticket', max_length=255, null=True, verbose_name='Attendee name')), + ('item', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='positions', to='pretixbase.Item', verbose_name='Item')), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='positions', to='pretixbase.Order', verbose_name='Order')), + ('variation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.ItemVariation', verbose_name='Variation')), + ], + options={ + 'verbose_name_plural': 'Order positions', + 'verbose_name': 'Order position', + }, + bases=(pretix.base.models.orders.ObjectWithAnswers, models.Model), + ), + migrations.CreateModel( + name='Organizer', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, verbose_name='Name')), + ('slug', models.SlugField(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')), + ], + options={ + 'ordering': ('name',), + 'verbose_name_plural': 'Organizers', + 'verbose_name': 'Organizer', + }, + bases=(models.Model, pretix.base.models.base.LoggingMixin), + ), + migrations.CreateModel( + name='OrganizerPermission', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('can_create_events', models.BooleanField(default=True, verbose_name='Can create events')), + ('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_perms', to='pretixbase.Organizer')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='organizer_perms', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name_plural': 'Organizer permissions', + 'verbose_name': 'Organizer permission', + }, + ), + migrations.CreateModel( + name='OrganizerSetting', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(max_length=255)), + ('value', models.TextField()), + ('object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='setting_objects', to='pretixbase.Organizer')), + ], + ), + migrations.CreateModel( + name='Question', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('question', pretix.base.i18n.I18nTextField(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(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='pretixbase.Event')), + ('items', models.ManyToManyField(blank=True, help_text='This question will be asked to buyers of the selected products', related_name='questions', to='pretixbase.Item', verbose_name='Products')), + ], + options={ + 'verbose_name_plural': 'Questions', + 'verbose_name': 'Question', + }, + bases=(models.Model, pretix.base.models.base.LoggingMixin), + ), + migrations.CreateModel( + name='QuestionAnswer', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('answer', models.TextField()), + ('cartposition', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='pretixbase.CartPosition')), + ('orderposition', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='pretixbase.OrderPosition')), + ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='pretixbase.Question')), + ], + ), + migrations.CreateModel( + name='Quota', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, verbose_name='Name')), + ('size', models.PositiveIntegerField(blank=True, help_text='Leave empty for an unlimited number of tickets.', null=True, verbose_name='Total capacity')), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quotas', to='pretixbase.Event', verbose_name='Event')), + ('items', models.ManyToManyField(blank=True, related_name='quotas', to='pretixbase.Item', verbose_name='Item')), + ('variations', models.ManyToManyField(blank=True, related_name='quotas', to='pretixbase.ItemVariation', verbose_name='Variations')), + ], + options={ + 'verbose_name_plural': 'Quotas', + 'verbose_name': 'Quota', + }, + bases=(models.Model, pretix.base.models.base.LoggingMixin), + ), + migrations.AddField( + model_name='organizer', + name='permitted', + field=models.ManyToManyField(related_name='organizers', through='pretixbase.OrganizerPermission', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='item', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='items', to='pretixbase.ItemCategory', verbose_name='Category'), + ), + migrations.AddField( + model_name='item', + name='event', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='items', to='pretixbase.Event', verbose_name='Event'), + ), + migrations.AddField( + model_name='event', + name='organizer', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='events', to='pretixbase.Organizer'), + ), + migrations.AddField( + model_name='event', + name='permitted', + field=models.ManyToManyField(related_name='events', through='pretixbase.EventPermission', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='cartposition', + name='event', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event', verbose_name='Event'), + ), + migrations.AddField( + model_name='cartposition', + name='item', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Item', verbose_name='Item'), + ), + migrations.AddField( + model_name='cartposition', + name='variation', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.ItemVariation', verbose_name='Variation'), + ), + migrations.AddField( + model_name='cachedticket', + name='order', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Order'), + ), + ] diff --git a/src/pretix/base/migrations/0003_logentry.py b/src/pretix/base/migrations/0003_logentry.py deleted file mode 100644 index 2bbd029d29..0000000000 --- a/src/pretix/base/migrations/0003_logentry.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9 on 2015-12-12 13:32 -from __future__ import unicode_literals - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('pretixbase', '0002_auto_20151212_1123'), - ] - - operations = [ - migrations.CreateModel( - name='LogEntry', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('object_id', models.PositiveIntegerField()), - ('datetime', models.DateTimeField(auto_now_add=True)), - ('action_type', models.CharField(max_length=255)), - ('data', models.TextField(default='{}')), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), - ('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event')), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/src/pretix/base/models/__init__.py b/src/pretix/base/models/__init__.py index dd203ea60d..1d19acbd9c 100644 --- a/src/pretix/base/models/__init__.py +++ b/src/pretix/base/models/__init__.py @@ -2,8 +2,7 @@ from .auth import User from .base import CachedFile, cachedfile_name from .event import Event, EventLock, EventPermission, EventSetting from .items import ( - Item, ItemCategory, ItemVariation, Property, PropertyValue, Question, - Quota, VariationsField, itempicture_upload_to, + Item, ItemCategory, ItemVariation, Question, Quota, itempicture_upload_to, ) from .log import LogEntry from .orders import ( @@ -14,8 +13,7 @@ from .organizer import Organizer, OrganizerPermission, OrganizerSetting __all__ = [ 'User', 'CachedFile', 'Organizer', 'OrganizerPermission', 'Event', 'EventPermission', - 'ItemCategory', 'Item', 'Property', 'PropertyValue', 'ItemVariation', 'VariationsField', 'Question', - 'Quota', 'Order', 'CachedTicket', 'QuestionAnswer', 'ObjectWithAnswers', 'OrderPosition', - 'CartPosition', 'EventSetting', 'OrganizerSetting', 'EventLock', 'cachedfile_name', 'itempicture_upload_to', - 'generate_secret', 'LogEntry' + 'ItemCategory', 'Item', 'ItemVariation', 'Question', 'Quota', 'Order', 'CachedTicket', 'QuestionAnswer', + 'ObjectWithAnswers', 'OrderPosition', 'CartPosition', 'EventSetting', 'OrganizerSetting', 'EventLock', + 'cachedfile_name', 'itempicture_upload_to', 'generate_secret', 'LogEntry' ] diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index 6f6975f8e6..ed6928f644 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -1,18 +1,16 @@ import sys from datetime import datetime -from itertools import product from django.db import models from django.db.models import Q, Case, Count, Sum, When from django.utils.functional import cached_property from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ -from typing import List, Tuple +from typing import Tuple from pretix.base.i18n import I18nCharField, I18nTextField from pretix.base.models.base import LoggedModel -from ..types import VariationDict from .event import Event @@ -196,117 +194,6 @@ class Item(LoggedModel): return False return True - def get_all_variations(self, use_cache: bool=False) -> List[VariationDict]: - """ - This method returns a list containing all variations of this - item. The list contains one VariationDict per variation, where - the Proprty IDs are keys and the PropertyValue objects are - values. If an ItemVariation object exists, it is available in - the dictionary via the special key 'variation'. - - VariationDicts differ from dicts only by specifying some extra - methods. - - :param use_cache: If this parameter is set to ``True``, a second call to this method - on the same model instance won't query the database again but return - the previous result again. - :type use_cache: bool - """ - 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") - variations_cache = {} - for var in all_variations: - key = [] - for v in var.values.all(): - key.append((v.prop_id, v.id)) - key = tuple(sorted(key)) - variations_cache[key] = var - - result = [] - for comb in product(*[prop.values.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.id, v.id)) - var[v.prop.id] = v - key = tuple(sorted(key)) - if key in variations_cache: - var['variation'] = variations_cache[key] - result.append(var) - - self._get_all_variations_cache = result - return result - - def _get_all_generated_variations(self): - propids = set([p.id for p in self.properties.all()]) - if len(propids) == 0: - variations = [VariationDict()] - else: - all_variations = list( - self.variations.annotate( - qc=Count('quotas') - ).filter(qc__gt=0).prefetch_related( - "values", "values__prop", "quotas__event" - ) - ) - variations = [] - for var in all_variations: - values = list(var.values.all()) - # Make sure we don't expose stale ItemVariation objects which are - # still around altough they have an old set of properties - if set([v.prop.id for v in values]) != propids: - continue - vardict = VariationDict() - for v in values: - vardict[v.prop.id] = v - vardict['variation'] = var - variations.append(vardict) - return variations - - def get_all_available_variations(self, use_cache: bool=False): - """ - This method returns a list of all variations which are theoretically - possible for sale. It DOES only return variations which DO have an ItemVariation - object, as all variations without one CAN NOT be part of a Quota and therefore can - never be available for sale. The only exception is the empty variation - for items without properties, which never has an ItemVariation object. - - This DOES NOT take into account quotas itself. Use ``is_available`` on the - ItemVariation objects (or the Item it self, if it does not have variations) to - determine availability by the terms of quotas. - - It is recommended to call:: - - .prefetch_related('properties', 'variations__values__prop') - - when retrieving Item objects you are going to use this method on. - """ - if use_cache and hasattr(self, '_get_all_available_variations_cache'): - return self._get_all_available_variations_cache - - variations = self._get_all_generated_variations() - - for i, var in enumerate(variations): - var['available'] = var['variation'].active if 'variation' in var else True - if 'variation' in var: - if var['variation'].default_price is not None: - var['price'] = var['variation'].default_price - else: - var['price'] = self.default_price - else: - var['price'] = self.default_price - - variations = [var for var in variations if var['available']] - - self._get_all_available_variations_cache = variations - return variations - def check_quotas(self): """ This method is used to determine whether this Item is currently available @@ -314,130 +201,24 @@ class Item(LoggedModel): :returns: any of the return codes of :py:meth:`Quota.availability()`. - :raises ValueError: if you call this on an item which has properties associated with it. + :raises ValueError: if you call this on an item which has variations associated with it. Please use the method on the ItemVariation object you are interested in. """ - if self.properties.count() > 0: # NOQA - raise ValueError('Do not call this directly on items which have properties ' + if self.variations.count() > 0: # NOQA + raise ValueError('Do not call this directly on items which have variations ' 'but call this on their ItemVariation objects') return min([q.availability() for q in self.quotas.all()], key=lambda s: (s[0], s[1] if s[1] is not None else sys.maxsize)) -class Property(models.Model): - """ - 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'. - - :param event: The event this belongs to - :type event: Event - :param name: The name of this property. - :type name: str - """ - - event = models.ForeignKey( - Event, - related_name="properties" - ) - item = models.ForeignKey( - Item, related_name='properties', null=True, blank=True - ) - name = I18nCharField( - max_length=250, - verbose_name=_("Property name") - ) - - class Meta: - verbose_name = _("Product property") - verbose_name_plural = _("Product properties") - - def __str__(self): - return str(self.name) - - 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() - - -class PropertyValue(models.Model): - """ - A value of a property. If the property would be 'T-Shirt size', - this could be 'M' or 'L'. - - :param prop: The property this value is a valid option for. - :type prop: Property - :param value: The value, as a human-readable string - :type value: str - :param position: An integer, used for sorting - :type position: int - """ - - prop = models.ForeignKey( - Property, - on_delete=models.CASCADE, - related_name="values" - ) - value = I18nCharField( - max_length=250, - verbose_name=_("Value"), - ) - position = models.IntegerField( - default=0 - ) - - class Meta: - verbose_name = _("Property value") - verbose_name_plural = _("Property values") - ordering = ("position", "id") - - def __str__(self): - return "%s: %s" % (self.prop.name, self.value) - - 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() - - @property - def sortkey(self) -> Tuple[int, datetime]: - return self.position, self.id - - def __lt__(self, other) -> bool: - return self.sortkey < other.sortkey - - class ItemVariation(models.Model): """ - A variation is an item combined with values for all properties - associated with the item. For example, if your item is 'T-Shirt' - and your properties are 'Size' and 'Color', then an example for an - variation would be 'T-Shirt XL read'. - - Attention: _ALL_ combinations of PropertyValues _ALWAYS_ exist, - even if there is no ItemVariation object for them! ItemVariation objects - do NOT prove existance, they are only available to make it possible - to override default values (like the price) for certain combinations - of property values. However, appropriate ItemVariation objects will be - created as soon as you add your variations to a quota. - - They also allow to explicitly EXCLUDE certain combinations of property - values by creating an ItemVariation object for them with active set to - False. + A variation of a product. For example, if your item is 'T-Shirt' + then an example for a variation would be 'T-Shirt XL'. :param item: The item this variation belongs to :type item: Item - :param values: A set of ``PropertyValue`` objects defining this variation + :param value: A string defining this variation :param active: Whether this value is to be sold. :type active: bool :param default_price: This variation's default price @@ -447,14 +228,18 @@ class ItemVariation(models.Model): Item, related_name='variations' ) - values = models.ManyToManyField( - PropertyValue, - related_name='variations', + value = I18nCharField( + max_length=255, + verbose_name=_('Description') ) active = models.BooleanField( default=True, verbose_name=_("Active"), ) + position = models.PositiveIntegerField( + default=0, + verbose_name=_("Position") + ) default_price = models.DecimalField( decimal_places=2, max_digits=7, null=True, blank=True, @@ -464,9 +249,10 @@ class ItemVariation(models.Model): class Meta: verbose_name = _("Product variation") verbose_name_plural = _("Product variations") + ordering = ("position", "id") def __str__(self): - return str(self.to_variation_dict()) + return str(self.value) def delete(self, *args, **kwargs): super().delete(*args, **kwargs) @@ -488,56 +274,10 @@ class ItemVariation(models.Model): return min([q.availability() for q in self.quotas.all()], key=lambda s: (s[0], s[1] if s[1] is not None else sys.maxsize)) - def to_variation_dict(self) -> VariationDict: - """ - :return: a :py:class:`VariationDict` representing this variation. - """ - vd = VariationDict() - for v in self.values.all(): - vd[v.prop.id] = v - vd['variation'] = self - return vd - - def add_values_from_string(self, pk): - """ - Add values to this ItemVariation using a serialized string of the form - ``property-id:value-id,ṗroperty-id:value-id`` - """ - for pair in pk.split(","): - prop, value = pair.split(":") - self.values.add( - PropertyValue.objects.get( - id=value, - prop_id=prop - ) - ) - - -class VariationsField(models.ManyToManyField): - """ - This is a ManyToManyField using the pretixcontrol.views.forms.VariationsField - form field by default. - """ - - def formfield(self, **kwargs): - from pretix.control.forms import VariationsField as FVariationsField - from django.db.models.fields.related import RelatedField - - defaults = { - 'form_class': FVariationsField, - # We don't need a queryset - 'queryset': ItemVariation.objects.none(), - } - defaults.update(kwargs) - # If initial is passed in, it's a list of related objects, but the - # MultipleChoiceField takes a list of IDs. - if defaults.get('initial') is not None: - initial = defaults['initial'] - if callable(initial): - initial = initial() - defaults['initial'] = [i.id for i in initial] - # Skip ManyToManyField in dependency chain - return super(RelatedField, self).formfield(**defaults) + def __lt__(self, other): + if self.position == other.position: + return self.id < other.id + return self.position < other.position class Question(LoggedModel): @@ -686,7 +426,7 @@ class Quota(LoggedModel): related_name="quotas", blank=True ) - variations = VariationsField( + variations = models.ManyToManyField( ItemVariation, related_name="quotas", blank=True, diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py index 4908597ea1..6a2a8ef915 100644 --- a/src/pretix/base/services/cart.py +++ b/src/pretix/base/services/cart.py @@ -77,7 +77,7 @@ def _add_new_items(event: Event, items: List[Tuple[int, Optional[int], int]], variations_query = ItemVariation.objects.filter( item__event=event, id__in=[i[1] for i in items if i[1] is not None] - ).select_related("item", "item__event").prefetch_related("quotas", "values", "values__prop") + ).select_related("item", "item__event").prefetch_related("quotas") variations_cache = {v.id: v for v in variations_query} for i in items: diff --git a/src/pretix/base/services/stats.py b/src/pretix/base/services/stats.py index b042481e8a..f9152020a9 100644 --- a/src/pretix/base/services/stats.py +++ b/src/pretix/base/services/stats.py @@ -31,8 +31,6 @@ def tuplesum(tuples: Iterable[Tuple]) -> Tuple: def order_overview(event: Event) -> Tuple[List[Tuple[ItemCategory, List[Item]]], Dict[str, Tuple[Decimal, Decimal]]]: items = event.items.all().select_related( 'category', # for re-grouping - ).prefetch_related( - 'properties', # for .get_all_available_variations() ).order_by('category__position', 'category_id', 'name') num_total = { @@ -73,22 +71,27 @@ def order_overview(event: Event) -> Tuple[List[Tuple[ItemCategory, List[Item]]], } for item in items: - item.all_variations = sorted(item.get_all_variations(), - key=lambda vd: vd.ordered_values()) - for var in item.all_variations: - variid = var['variation'].id if 'variation' in var else None - var.num_total = num_total.get((item.id, variid), (0, 0)) - var.num_pending = num_pending.get((item.id, variid), (0, 0)) - var.num_cancelled = num_cancelled.get((item.id, variid), (0, 0)) - var.num_refunded = num_refunded.get((item.id, variid), (0, 0)) - var.num_paid = num_paid.get((item.id, variid), (0, 0)) - item.has_variations = (len(item.all_variations) != 1 - or not item.all_variations[0].empty()) - item.num_total = tuplesum(var.num_total for var in item.all_variations) - item.num_pending = tuplesum(var.num_pending for var in item.all_variations) - item.num_cancelled = tuplesum(var.num_cancelled for var in item.all_variations) - item.num_refunded = tuplesum(var.num_refunded for var in item.all_variations) - item.num_paid = tuplesum(var.num_paid for var in item.all_variations) + item.all_variations = list(item.variations.all()) + item.has_variations = (len(item.all_variations) > 0) + if item.has_variations: + for var in item.all_variations: + variid = var.id + var.num_total = num_total.get((item.id, variid), (0, 0)) + var.num_pending = num_pending.get((item.id, variid), (0, 0)) + var.num_cancelled = num_cancelled.get((item.id, variid), (0, 0)) + var.num_refunded = num_refunded.get((item.id, variid), (0, 0)) + var.num_paid = num_paid.get((item.id, variid), (0, 0)) + item.num_total = tuplesum(var.num_total for var in item.all_variations) + item.num_pending = tuplesum(var.num_pending for var in item.all_variations) + item.num_cancelled = tuplesum(var.num_cancelled for var in item.all_variations) + item.num_refunded = tuplesum(var.num_refunded for var in item.all_variations) + item.num_paid = tuplesum(var.num_paid for var in item.all_variations) + else: + item.num_total = num_total.get((item.id, None), (0, 0)) + item.num_pending = num_pending.get((item.id, None), (0, 0)) + item.num_cancelled = num_cancelled.get((item.id, None), (0, 0)) + item.num_refunded = num_refunded.get((item.id, None), (0, 0)) + item.num_paid = num_paid.get((item.id, None), (0, 0)) nonecat = ItemCategory(name=_('Uncategorized')) # Regroup those by category @@ -106,6 +109,7 @@ def order_overview(event: Event) -> Tuple[List[Tuple[ItemCategory, List[Item]]], for c in items_by_category: c[0].num_total = tuplesum(item.num_total for item in c[1]) + print(c[1], c[0].num_total, [item.num_total for item in c[1]]) c[0].num_pending = tuplesum(item.num_pending for item in c[1]) c[0].num_cancelled = tuplesum(item.num_cancelled for item in c[1]) c[0].num_refunded = tuplesum(item.num_refunded for item in c[1]) diff --git a/src/pretix/base/types.py b/src/pretix/base/types.py deleted file mode 100644 index 0913b88c3a..0000000000 --- a/src/pretix/base/types.py +++ /dev/null @@ -1,96 +0,0 @@ -from typing import Iterable, List, Tuple - - -class VariationDict(dict): - """ - A VariationDict object behaves exactle the same as the Python built-in - ``dict`` does, but adds some special methods. It is used for the dicts - returned by ``Item.get_all_variations()`` to avoid duplicate code in the - code calling this method. - """ - IGNORE_KEYS = ('variation', 'key', 'available', 'price') - - def relevant_items(self) -> Iterable[Tuple]: - """ - Iterate over all items with numeric keys. - - This is in use because the variation dictionaries use property ids - as key and have some special keys like 'variation'. - """ - return (i for i in self.items() if i[0] not in self.IGNORE_KEYS) - - def relevant_values(self) -> Iterable["PropertyValue"]: - """ - Iterate over all values with numeric keys. - - This is in use because the variation dictionaries use property ids - as key and have some special keys like 'variation'. - """ - return (i[1] for i in self.items() if i[0] not in self.IGNORE_KEYS) - - def identify(self) -> str: - """ - Build a simple and unique identifier for this dict. This can be any string - used to compare one VariationDict to others. - - In the current implementation, it is a string containing a list of - the PropertyValue id's, sorted by the Property id's and is therefore - unique among one item. - """ - def order_key(i): - return i[0] - return ",".join(( - str(v[1].pk) for v in sorted(self.relevant_items(), key=order_key) - )) - - def key(self) -> str: - """ - Build an identifier for this dict which exactly specifies the combination - for this variation without any doubt. This can be used to "talk" about a - variation in network communication. - - In the current implementation, it is a string containing a list of - the propertyid:valueid tuples without any specific order and is therefore - not useful to compare two VariationDicts for equality. - """ - return ",".join(( - str(v[0]) + ":" + str(v[1].pk) for v in self.relevant_items() - )) - - def __eq__(self, other): - if type(other) is type(self): - return self.identify() == other.identify() - else: - return super().__eq__(other) - - def empty(self) -> bool: - """ - Returns true, if this VariationDict does not contain any "real" data like - references to PropertyValues, but only "metadata". - """ - return not next(self.relevant_items(), False) - - def ordered_values(self) -> List["ItemVariation"]: - """ - Returns a list of values ordered by their keys - """ - return [ - i[1] for i - in sorted( - [it for it in self.relevant_items()], - key=lambda i: i[0] - ) - ] - - def __str__(self) -> str: - return " – ".join([str(v.value) for v in self.ordered_values()]) - - def copy(self) -> "VariationDict": - """ - Return a one-level deep copy of this object (create a new - VariationDict but make a shallow copy of the dict inside it). - """ - new = VariationDict() - for k, v in self.items(): - new[k] = v - return new diff --git a/src/pretix/control/forms/__init__.py b/src/pretix/control/forms/__init__.py index b4e63396e2..6855cb8d73 100644 --- a/src/pretix/control/forms/__init__.py +++ b/src/pretix/control/forms/__init__.py @@ -110,277 +110,6 @@ def selector(values, prop): ] -def sort(v, prop): - # Given a list of variations, this will sort them by their position - # on the x-axis - return v[prop.id].sortkey - - -class VariationsFieldRenderer(forms.widgets.CheckboxFieldRenderer): - """ - This is the default renderer for a VariationsField. Based on the choice input class - this renders a list or a matrix of checkboxes/radio buttons/... - """ - - def __init__(self, name, value, attrs, choices): - self.name = name - self.value = value - self.attrs = attrs - self.choices = choices - - def render(self): - """ - Outputs a grid for this set of choice fields. - """ - if len(self.choices) == 0: - raise ValueError("Can't handle empty lists") - - variations = [] - for key, value in self.choices: - value['key'] = key - variations.append(value) - - properties = [v.prop for v in variations[0].relevant_values()] - dimension = len(properties) - - id_ = self.attrs.get('id', None) - start_tag = format_html('
| ') - for val2 in prop2v: - output.append(format_html(' | {0} | ', val2.value)) - output.append('
|---|---|
| {0} | ', val1.value)) - # We are now inside one of the rows of the grid and have to - # select the variations to display in this row. In order to - # achieve this, we use the 'selector' lambda defined above. - # It gives us a normalized, comparable version of a set of - # PropertyValue objects. In this case, we compute the - # selector of our row as the selector of the sum of the - # values defining our grind and the value defining our row. - selection = selector(gridrow + (val1,), prop2) - # We now iterate over all variations who generate the same - # selector as 'selection'. - filtered = [v for v in variations if selector(v.relevant_values(), prop2) == selection] - for variation in sorted(filtered, key=partial(sort, prop=prop2)): - final_attrs = dict( - self.attrs.copy(), type=self.choice_input_class.input_type, - name=self.name, value=variation['key'] - ) - if variation['key'] in self.value: - final_attrs['checked'] = 'checked' - output.append(format_html('', flatatt(final_attrs))) - output.append('') - output.append(' |
- -
- -{% trans "You have to define and select propreties to be able to configure variations." %}
-{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/item/variations_1d.html b/src/pretix/control/templates/pretixcontrol/item/variations_1d.html deleted file mode 100644 index 534ec1189e..0000000000 --- a/src/pretix/control/templates/pretixcontrol/item/variations_1d.html +++ /dev/null @@ -1,35 +0,0 @@ -{% extends "pretixcontrol/item/base.html" %} -{% load i18n %} -{% load bootstrap3 %} -{% block inside %} - - -{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/item/variations_nd.html b/src/pretix/control/templates/pretixcontrol/item/variations_nd.html deleted file mode 100644 index 094312c8e3..0000000000 --- a/src/pretix/control/templates/pretixcontrol/item/variations_nd.html +++ /dev/null @@ -1,51 +0,0 @@ -{% extends "pretixcontrol/item/base.html" %} -{% load i18n %} -{% load bootstrap3 %} -{% block inside %} - - -{% endblock %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index ce31610447..ca7dc1cb2f 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -30,8 +30,6 @@ urlpatterns = [ url(r'^items/(?P