diff --git a/src/make_testdata.py b/src/make_testdata.py index 9e2e59f619..93f2c1dc81 100644 --- a/src/make_testdata.py +++ b/src/make_testdata.py @@ -39,18 +39,6 @@ cat_tickets = ItemCategory.objects.create( cat_merch = ItemCategory.objects.create( event=event, name='Merchandise' ) -size_prop = Property.objects.create( - event=event, name='Size' -) -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' -) question = Question.objects.create( event=event, question='Age', type=Question.TYPE_NUMBER, required=False @@ -64,7 +52,18 @@ item_shirt = Item.objects.create( event=event, category=cat_merch, name='T-Shirt', default_price=15, tax_rate=19 ) -item_shirt.properties.add(size_prop) +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) diff --git a/src/pretix/base/migrations/0001_initial.py b/src/pretix/base/migrations/0001_initial.py index ef4253f7eb..79df631421 100644 --- a/src/pretix/base/migrations/0001_initial.py +++ b/src/pretix/base/migrations/0001_initial.py @@ -23,41 +23,41 @@ class Migration(migrations.Migration): migrations.CreateModel( name='User', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)), ('password', models.CharField(verbose_name='password', max_length=128)), - ('last_login', models.DateTimeField(verbose_name='last login', blank=True, null=True)), - ('is_superuser', models.BooleanField(verbose_name='superuser status', help_text='Designates that this user has all permissions without explicitly assigning them.', default=False)), - ('email', models.EmailField(verbose_name='E-mail', null=True, unique=True, db_index=True, max_length=254, blank=True)), - ('givenname', models.CharField(verbose_name='Given name', blank=True, null=True, max_length=255)), - ('familyname', models.CharField(verbose_name='Family name', blank=True, null=True, max_length=255)), - ('is_active', models.BooleanField(verbose_name='Is active', default=True)), - ('is_staff', models.BooleanField(verbose_name='Is site admin', default=False)), + ('last_login', models.DateTimeField(verbose_name='last login', null=True, blank=True)), + ('is_superuser', models.BooleanField(default=False, verbose_name='superuser status', help_text='Designates that this user has all permissions without explicitly assigning them.')), + ('email', models.EmailField(unique=True, verbose_name='E-mail', blank=True, db_index=True, max_length=254, null=True)), + ('givenname', models.CharField(verbose_name='Given name', null=True, max_length=255, blank=True)), + ('familyname', models.CharField(verbose_name='Family name', null=True, max_length=255, blank=True)), + ('is_active', models.BooleanField(default=True, verbose_name='Is active')), + ('is_staff', models.BooleanField(default=False, verbose_name='Is site admin')), ('date_joined', models.DateTimeField(verbose_name='Date joined', auto_now_add=True)), - ('locale', models.CharField(verbose_name='Language', choices=[('en', 'English'), ('de', 'German'), ('de-informal', 'German (informal)')], default='en', max_length=50)), - ('timezone', models.CharField(verbose_name='Timezone', default='UTC', max_length=100)), - ('groups', models.ManyToManyField(verbose_name='groups', related_query_name='user', help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', to='auth.Group', blank=True)), - ('user_permissions', models.ManyToManyField(verbose_name='user permissions', related_query_name='user', help_text='Specific permissions for this user.', related_name='user_set', to='auth.Permission', blank=True)), + ('locale', models.CharField(default='en', verbose_name='Language', choices=[('en', 'English'), ('de', 'German'), ('de-informal', 'German (informal)')], max_length=50)), + ('timezone', models.CharField(default='UTC', verbose_name='Timezone', max_length=100)), + ('groups', models.ManyToManyField(related_query_name='user', verbose_name='groups', help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', blank=True, related_name='user_set', to='auth.Group')), + ('user_permissions', models.ManyToManyField(related_query_name='user', verbose_name='user permissions', help_text='Specific permissions for this user.', blank=True, related_name='user_set', to='auth.Permission')), ], options={ - 'verbose_name_plural': 'Users', 'verbose_name': 'User', + 'verbose_name_plural': 'Users', }, ), migrations.CreateModel( name='CachedFile', fields=[ - ('id', models.UUIDField(primary_key=True, default=uuid.uuid4, serialize=False)), - ('expires', models.DateTimeField(blank=True, null=True)), - ('date', models.DateTimeField(blank=True, null=True)), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('expires', models.DateTimeField(null=True, blank=True)), + ('date', models.DateTimeField(null=True, blank=True)), ('filename', models.CharField(max_length=255)), ('type', models.CharField(max_length=255)), - ('file', models.FileField(upload_to=pretix.base.models.cachedfile_name, blank=True, null=True)), + ('file', models.FileField(upload_to=pretix.base.models.cachedfile_name, null=True, blank=True)), ], ), migrations.CreateModel( name='CachedTicket', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)), ('provider', models.CharField(max_length=255)), ('cachedfile', models.ForeignKey(to='pretixbase.CachedFile')), ], @@ -65,50 +65,50 @@ class Migration(migrations.Migration): migrations.CreateModel( name='CartPosition', fields=[ - ('id', models.CharField(primary_key=True, serialize=False, max_length=36)), + ('id', models.CharField(serialize=False, max_length=36, primary_key=True)), ('identity', models.CharField(max_length=36)), ('version_start_date', models.DateTimeField()), - ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_end_date', models.DateTimeField(default=None, null=True, blank=True)), ('version_birth_date', models.DateTimeField()), - ('session', models.CharField(verbose_name='Session', blank=True, null=True, max_length=255)), - ('price', models.DecimalField(verbose_name='Price', max_digits=10, decimal_places=2)), + ('session', models.CharField(verbose_name='Session', null=True, max_length=255, blank=True)), + ('price', models.DecimalField(decimal_places=2, verbose_name='Price', max_digits=10)), ('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', blank=True, help_text='Empty, if this product is not an admission ticket', null=True, max_length=255)), + ('attendee_name', models.CharField(verbose_name='Attendee name', null=True, help_text='Empty, if this product is not an admission ticket', max_length=255, blank=True)), ], options={ - 'verbose_name_plural': 'Cart positions', 'verbose_name': 'Cart position', + 'verbose_name_plural': 'Cart positions', }, bases=(pretix.base.models.ObjectWithAnswers, models.Model), ), migrations.CreateModel( name='Event', fields=[ - ('id', models.CharField(primary_key=True, serialize=False, max_length=36)), + ('id', models.CharField(serialize=False, max_length=36, primary_key=True)), ('identity', models.CharField(max_length=36)), ('version_start_date', models.DateTimeField()), - ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_end_date', models.DateTimeField(default=None, null=True, blank=True)), ('version_birth_date', models.DateTimeField()), ('name', pretix.base.i18n.I18nCharField(verbose_name='Name', max_length=200)), - ('slug', models.SlugField(verbose_name='Slug', help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')])), - ('currency', models.CharField(verbose_name='Default currency', default='EUR', max_length=10)), + ('slug', models.SlugField(verbose_name='Slug', help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(regex='^[a-zA-Z0-9.-]+$', message='The slug may only contain letters, numbers, dots and dashes.')])), + ('currency', models.CharField(default='EUR', verbose_name='Default currency', 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)), - ('presale_end', models.DateTimeField(verbose_name='End of presale', blank=True, help_text='No products will be sold after this date.', null=True)), - ('presale_start', models.DateTimeField(verbose_name='Start of presale', blank=True, help_text='No products will be sold before this date.', null=True)), - ('plugins', models.TextField(verbose_name='Plugins', blank=True, null=True)), + ('date_to', models.DateTimeField(verbose_name='Event end time', null=True, blank=True)), + ('presale_end', models.DateTimeField(verbose_name='End of presale', help_text='No products will be sold after this date.', null=True, blank=True)), + ('presale_start', models.DateTimeField(verbose_name='Start of presale', help_text='No products will be sold before this date.', null=True, blank=True)), + ('plugins', models.TextField(verbose_name='Plugins', null=True, blank=True)), ], options={ - 'verbose_name_plural': 'Events', 'verbose_name': 'Event', + 'verbose_name_plural': 'Events', 'ordering': ('date_from', 'name'), }, ), migrations.CreateModel( name='EventLock', fields=[ - ('event', models.CharField(primary_key=True, serialize=False, max_length=36)), + ('event', models.CharField(serialize=False, max_length=36, primary_key=True)), ('date', models.DateTimeField(auto_now=True)), ('token', models.UUIDField(default=uuid.uuid4)), ], @@ -116,31 +116,31 @@ class Migration(migrations.Migration): migrations.CreateModel( name='EventPermission', fields=[ - ('id', models.CharField(primary_key=True, serialize=False, max_length=36)), + ('id', models.CharField(serialize=False, max_length=36, primary_key=True)), ('identity', models.CharField(max_length=36)), ('version_start_date', models.DateTimeField()), - ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_end_date', models.DateTimeField(default=None, null=True, blank=True)), ('version_birth_date', models.DateTimeField()), - ('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)), + ('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', versions.models.VersionedForeignKey(to='pretixbase.Event')), ('user', models.ForeignKey(related_name='event_perms', to=settings.AUTH_USER_MODEL)), ], options={ - 'verbose_name_plural': 'Event permissions', 'verbose_name': 'Event permission', + 'verbose_name_plural': 'Event permissions', }, ), migrations.CreateModel( name='EventSetting', fields=[ - ('id', models.CharField(primary_key=True, serialize=False, max_length=36)), + ('id', models.CharField(serialize=False, max_length=36, primary_key=True)), ('identity', models.CharField(max_length=36)), ('version_start_date', models.DateTimeField()), - ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_end_date', models.DateTimeField(default=None, null=True, blank=True)), ('version_birth_date', models.DateTimeField()), ('key', models.CharField(max_length=255)), ('value', models.TextField()), @@ -153,151 +153,151 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Item', fields=[ - ('id', models.CharField(primary_key=True, serialize=False, max_length=36)), + ('id', models.CharField(serialize=False, max_length=36, primary_key=True)), ('identity', models.CharField(max_length=36)), ('version_start_date', models.DateTimeField()), - ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_end_date', models.DateTimeField(default=None, null=True, blank=True)), ('version_birth_date', models.DateTimeField()), ('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', blank=True, help_text='This is shown below the product name in lists.', null=True)), - ('default_price', models.DecimalField(verbose_name='Default price', max_digits=7, null=True, decimal_places=2)), - ('tax_rate', models.DecimalField(verbose_name='Taxes included in percent', blank=True, max_digits=7, null=True, decimal_places=2)), - ('admission', models.BooleanField(verbose_name='Is an admission ticket', help_text='Whether or not buying this product allows a person to enter your event', default=False)), + ('active', models.BooleanField(default=True, verbose_name='Active')), + ('description', pretix.base.i18n.I18nTextField(verbose_name='Description', help_text='This is shown below the product name in lists.', null=True, blank=True)), + ('default_price', models.DecimalField(decimal_places=2, verbose_name='Default price', max_digits=7, null=True)), + ('tax_rate', models.DecimalField(decimal_places=2, verbose_name='Taxes included in percent', max_digits=7, null=True, blank=True)), + ('admission', models.BooleanField(default=False, verbose_name='Is an admission ticket', help_text='Whether or not buying this product allows a person to enter your event')), ('position', models.IntegerField(default=0)), - ('picture', models.ImageField(upload_to=pretix.base.models.itempicture_upload_to, verbose_name='Product picture', blank=True, null=True)), + ('picture', models.ImageField(upload_to=pretix.base.models.itempicture_upload_to, verbose_name='Product picture', null=True, blank=True)), ], options={ - 'verbose_name_plural': 'Products', 'verbose_name': 'Product', + 'verbose_name_plural': 'Products', 'ordering': ('category__position', 'category', 'position'), }, ), migrations.CreateModel( name='ItemCategory', fields=[ - ('id', models.CharField(primary_key=True, serialize=False, max_length=36)), + ('id', models.CharField(serialize=False, max_length=36, primary_key=True)), ('identity', models.CharField(max_length=36)), ('version_start_date', models.DateTimeField()), - ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_end_date', models.DateTimeField(default=None, null=True, blank=True)), ('version_birth_date', models.DateTimeField()), ('name', pretix.base.i18n.I18nCharField(verbose_name='Category name', max_length=255)), ('position', models.IntegerField(default=0)), ('event', versions.models.VersionedForeignKey(related_name='categories', to='pretixbase.Event')), ], options={ - 'verbose_name_plural': 'Product categories', 'verbose_name': 'Product category', + 'verbose_name_plural': 'Product categories', 'ordering': ('position', 'version_birth_date'), }, ), migrations.CreateModel( name='ItemVariation', fields=[ - ('id', models.CharField(primary_key=True, serialize=False, max_length=36)), + ('id', models.CharField(serialize=False, max_length=36, primary_key=True)), ('identity', models.CharField(max_length=36)), ('version_start_date', models.DateTimeField()), - ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_end_date', models.DateTimeField(default=None, null=True, blank=True)), ('version_birth_date', models.DateTimeField()), - ('active', models.BooleanField(verbose_name='Active', default=True)), - ('default_price', models.DecimalField(verbose_name='Default price', blank=True, max_digits=7, null=True, decimal_places=2)), + ('active', models.BooleanField(default=True, verbose_name='Active')), + ('default_price', models.DecimalField(decimal_places=2, verbose_name='Default price', max_digits=7, null=True, blank=True)), ('item', versions.models.VersionedForeignKey(related_name='variations', to='pretixbase.Item')), ], options={ - 'verbose_name_plural': 'Product variations', 'verbose_name': 'Product variation', + 'verbose_name_plural': 'Product variations', }, ), migrations.CreateModel( name='Order', fields=[ - ('id', models.CharField(primary_key=True, serialize=False, max_length=36)), + ('id', models.CharField(serialize=False, max_length=36, primary_key=True)), ('identity', models.CharField(max_length=36)), ('version_start_date', models.DateTimeField()), - ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_end_date', models.DateTimeField(default=None, null=True, blank=True)), ('version_birth_date', models.DateTimeField()), ('code', models.CharField(verbose_name='Order code', max_length=16)), ('status', models.CharField(verbose_name='Status', choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3)), - ('email', models.EmailField(verbose_name='E-mail', blank=True, null=True, max_length=254)), - ('locale', models.CharField(verbose_name='Locale', blank=True, null=True, max_length=32)), - ('secret', models.CharField(max_length=32, default=pretix.base.models.generate_secret)), + ('email', models.EmailField(verbose_name='E-mail', null=True, max_length=254, blank=True)), + ('locale', models.CharField(verbose_name='Locale', null=True, max_length=32, blank=True)), + ('secret', models.CharField(default=pretix.base.models.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', blank=True, null=True, max_length=255)), - ('payment_fee', models.DecimalField(verbose_name='Payment method fee', max_digits=10, default=0, 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)), + ('payment_date', models.DateTimeField(verbose_name='Payment date', null=True, blank=True)), + ('payment_provider', models.CharField(verbose_name='Payment provider', null=True, max_length=255, blank=True)), + ('payment_fee', models.DecimalField(decimal_places=2, default=0, verbose_name='Payment method fee', max_digits=10)), + ('payment_info', models.TextField(verbose_name='Payment information', null=True, blank=True)), + ('payment_manual', models.BooleanField(default=False, verbose_name='Payment state was manually modified')), + ('total', models.DecimalField(decimal_places=2, verbose_name='Total amount', max_digits=10)), ('event', versions.models.VersionedForeignKey(verbose_name='Event', related_name='orders', to='pretixbase.Event')), ], options={ - 'verbose_name_plural': 'Orders', 'verbose_name': 'Order', + 'verbose_name_plural': 'Orders', 'ordering': ('-datetime',), }, ), migrations.CreateModel( name='OrderPosition', fields=[ - ('id', models.CharField(primary_key=True, serialize=False, max_length=36)), + ('id', models.CharField(serialize=False, max_length=36, primary_key=True)), ('identity', models.CharField(max_length=36)), ('version_start_date', models.DateTimeField()), - ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_end_date', models.DateTimeField(default=None, null=True, blank=True)), ('version_birth_date', models.DateTimeField()), - ('price', models.DecimalField(verbose_name='Price', max_digits=10, decimal_places=2)), - ('attendee_name', models.CharField(verbose_name='Attendee name', blank=True, help_text='Empty, if this product is not an admission ticket', null=True, max_length=255)), + ('price', models.DecimalField(decimal_places=2, verbose_name='Price', max_digits=10)), + ('attendee_name', models.CharField(verbose_name='Attendee name', null=True, help_text='Empty, if this product is not an admission ticket', max_length=255, blank=True)), ('item', versions.models.VersionedForeignKey(verbose_name='Item', related_name='positions', to='pretixbase.Item')), ('order', versions.models.VersionedForeignKey(verbose_name='Order', related_name='positions', to='pretixbase.Order')), - ('variation', versions.models.VersionedForeignKey(verbose_name='Variation', to='pretixbase.ItemVariation', null=True, blank=True)), + ('variation', versions.models.VersionedForeignKey(verbose_name='Variation', blank=True, null=True, to='pretixbase.ItemVariation')), ], options={ - 'verbose_name_plural': 'Order positions', 'verbose_name': 'Order position', + 'verbose_name_plural': 'Order positions', }, bases=(pretix.base.models.ObjectWithAnswers, models.Model), ), migrations.CreateModel( name='Organizer', fields=[ - ('id', models.CharField(primary_key=True, serialize=False, max_length=36)), + ('id', models.CharField(serialize=False, max_length=36, primary_key=True)), ('identity', models.CharField(max_length=36)), ('version_start_date', models.DateTimeField()), - ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_end_date', models.DateTimeField(default=None, null=True, blank=True)), ('version_birth_date', models.DateTimeField()), ('name', models.CharField(verbose_name='Name', max_length=200)), - ('slug', models.SlugField(verbose_name='Slug', help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')])), + ('slug', models.SlugField(verbose_name='Slug', help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(regex='^[a-zA-Z0-9.-]+$', message='The slug may only contain letters, numbers, dots and dashes.')])), ], options={ - 'verbose_name_plural': 'Organizers', 'verbose_name': 'Organizer', + 'verbose_name_plural': 'Organizers', 'ordering': ('name',), }, ), migrations.CreateModel( name='OrganizerPermission', fields=[ - ('id', models.CharField(primary_key=True, serialize=False, max_length=36)), + ('id', models.CharField(serialize=False, max_length=36, primary_key=True)), ('identity', models.CharField(max_length=36)), ('version_start_date', models.DateTimeField()), - ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_end_date', models.DateTimeField(default=None, null=True, blank=True)), ('version_birth_date', models.DateTimeField()), - ('can_create_events', models.BooleanField(verbose_name='Can create events', default=True)), + ('can_create_events', models.BooleanField(default=True, verbose_name='Can create events')), ('organizer', versions.models.VersionedForeignKey(to='pretixbase.Organizer')), ('user', models.ForeignKey(related_name='organizer_perms', to=settings.AUTH_USER_MODEL)), ], options={ - 'verbose_name_plural': 'Organizer permissions', 'verbose_name': 'Organizer permission', + 'verbose_name_plural': 'Organizer permissions', }, ), migrations.CreateModel( name='OrganizerSetting', fields=[ - ('id', models.CharField(primary_key=True, serialize=False, max_length=36)), + ('id', models.CharField(serialize=False, max_length=36, primary_key=True)), ('identity', models.CharField(max_length=36)), ('version_start_date', models.DateTimeField()), - ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_end_date', models.DateTimeField(default=None, null=True, blank=True)), ('version_birth_date', models.DateTimeField()), ('key', models.CharField(max_length=255)), ('value', models.TextField()), @@ -310,67 +310,68 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Property', fields=[ - ('id', models.CharField(primary_key=True, serialize=False, max_length=36)), + ('id', models.CharField(serialize=False, max_length=36, primary_key=True)), ('identity', models.CharField(max_length=36)), ('version_start_date', models.DateTimeField()), - ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_end_date', models.DateTimeField(default=None, null=True, blank=True)), ('version_birth_date', models.DateTimeField()), ('name', pretix.base.i18n.I18nCharField(verbose_name='Property name', max_length=250)), ('event', versions.models.VersionedForeignKey(related_name='properties', to='pretixbase.Event')), + ('item', versions.models.VersionedForeignKey(blank=True, related_name='properties', null=True, to='pretixbase.Item')), ], options={ - 'verbose_name_plural': 'Product properties', 'verbose_name': 'Product property', + 'verbose_name_plural': 'Product properties', }, ), migrations.CreateModel( name='PropertyValue', fields=[ - ('id', models.CharField(primary_key=True, serialize=False, max_length=36)), + ('id', models.CharField(serialize=False, max_length=36, primary_key=True)), ('identity', models.CharField(max_length=36)), ('version_start_date', models.DateTimeField()), - ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_end_date', models.DateTimeField(default=None, null=True, blank=True)), ('version_birth_date', models.DateTimeField()), ('value', pretix.base.i18n.I18nCharField(verbose_name='Value', max_length=250)), ('position', models.IntegerField(default=0)), ('prop', versions.models.VersionedForeignKey(related_name='values', to='pretixbase.Property')), ], options={ - 'verbose_name_plural': 'Property values', 'verbose_name': 'Property value', + 'verbose_name_plural': 'Property values', 'ordering': ('position', 'version_birth_date'), }, ), migrations.CreateModel( name='Question', fields=[ - ('id', models.CharField(primary_key=True, serialize=False, max_length=36)), + ('id', models.CharField(serialize=False, max_length=36, primary_key=True)), ('identity', models.CharField(max_length=36)), ('version_start_date', models.DateTimeField()), - ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_end_date', models.DateTimeField(default=None, null=True, blank=True)), ('version_birth_date', models.DateTimeField()), ('question', pretix.base.i18n.I18nTextField(verbose_name='Question')), ('type', models.CharField(verbose_name='Question type', choices=[('N', 'Number'), ('S', 'Text (one line)'), ('T', 'Multiline text'), ('B', 'Yes/No')], max_length=5)), - ('required', models.BooleanField(verbose_name='Required question', default=False)), + ('required', models.BooleanField(default=False, verbose_name='Required question')), ('event', versions.models.VersionedForeignKey(related_name='questions', to='pretixbase.Event')), - ('items', versions.models.VersionedManyToManyField(verbose_name='Products', blank=True, help_text='This question will be asked to buyers of the selected products', related_name='questions', to='pretixbase.Item')), + ('items', versions.models.VersionedManyToManyField(to='pretixbase.Item', verbose_name='Products', help_text='This question will be asked to buyers of the selected products', related_name='questions', blank=True)), ], options={ - 'verbose_name_plural': 'Questions', 'verbose_name': 'Question', + 'verbose_name_plural': 'Questions', }, ), migrations.CreateModel( name='QuestionAnswer', fields=[ - ('id', models.CharField(primary_key=True, serialize=False, max_length=36)), + ('id', models.CharField(serialize=False, max_length=36, primary_key=True)), ('identity', models.CharField(max_length=36)), ('version_start_date', models.DateTimeField()), - ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_end_date', models.DateTimeField(default=None, null=True, blank=True)), ('version_birth_date', models.DateTimeField()), ('answer', models.TextField()), - ('cartposition', models.ForeignKey(related_name='answers', to='pretixbase.CartPosition', null=True, blank=True)), - ('orderposition', models.ForeignKey(related_name='answers', to='pretixbase.OrderPosition', null=True, blank=True)), + ('cartposition', models.ForeignKey(blank=True, related_name='answers', null=True, to='pretixbase.CartPosition')), + ('orderposition', models.ForeignKey(blank=True, related_name='answers', null=True, to='pretixbase.OrderPosition')), ('question', versions.models.VersionedForeignKey(related_name='answers', to='pretixbase.Question')), ], options={ @@ -380,26 +381,26 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Quota', fields=[ - ('id', models.CharField(primary_key=True, serialize=False, max_length=36)), + ('id', models.CharField(serialize=False, max_length=36, primary_key=True)), ('identity', models.CharField(max_length=36)), ('version_start_date', models.DateTimeField()), - ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_end_date', models.DateTimeField(default=None, null=True, blank=True)), ('version_birth_date', models.DateTimeField()), ('name', models.CharField(verbose_name='Name', max_length=200)), ('size', models.PositiveIntegerField(verbose_name='Total capacity')), ('event', versions.models.VersionedForeignKey(verbose_name='Event', related_name='quotas', to='pretixbase.Event')), - ('items', versions.models.VersionedManyToManyField(verbose_name='Item', blank=True, related_name='quotas', to='pretixbase.Item')), - ('variations', pretix.base.models.VariationsField(verbose_name='Variations', blank=True, related_name='quotas', to='pretixbase.ItemVariation')), + ('items', versions.models.VersionedManyToManyField(to='pretixbase.Item', verbose_name='Item', related_name='quotas', blank=True)), + ('variations', pretix.base.models.VariationsField(to='pretixbase.ItemVariation', verbose_name='Variations', related_name='quotas', blank=True)), ], options={ - 'verbose_name_plural': 'Quotas', 'verbose_name': 'Quota', + 'verbose_name_plural': 'Quotas', }, ), migrations.AddField( model_name='organizer', name='permitted', - field=models.ManyToManyField(through='pretixbase.OrganizerPermission', related_name='organizers', to=settings.AUTH_USER_MODEL), + field=models.ManyToManyField(related_name='organizers', to=settings.AUTH_USER_MODEL, through='pretixbase.OrganizerPermission'), ), migrations.AddField( model_name='itemvariation', @@ -409,18 +410,13 @@ class Migration(migrations.Migration): migrations.AddField( model_name='item', name='category', - field=versions.models.VersionedForeignKey(verbose_name='Category', related_name='items', to='pretixbase.ItemCategory', null=True, blank=True, on_delete=django.db.models.deletion.PROTECT), + field=versions.models.VersionedForeignKey(verbose_name='Category', blank=True, to='pretixbase.ItemCategory', related_name='items', null=True, on_delete=django.db.models.deletion.PROTECT), ), migrations.AddField( model_name='item', name='event', field=versions.models.VersionedForeignKey(verbose_name='Event', related_name='items', to='pretixbase.Event', on_delete=django.db.models.deletion.PROTECT), ), - migrations.AddField( - model_name='item', - name='properties', - field=versions.models.VersionedManyToManyField(verbose_name='Properties', blank=True, 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.", related_name='items', to='pretixbase.Property'), - ), migrations.AddField( model_name='event', name='organizer', @@ -429,7 +425,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='event', name='permitted', - field=models.ManyToManyField(through='pretixbase.EventPermission', related_name='events', to=settings.AUTH_USER_MODEL), + field=models.ManyToManyField(related_name='events', to=settings.AUTH_USER_MODEL, through='pretixbase.EventPermission'), ), migrations.AddField( model_name='cartposition', @@ -444,7 +440,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='cartposition', name='variation', - field=versions.models.VersionedForeignKey(verbose_name='Variation', to='pretixbase.ItemVariation', null=True, blank=True), + field=versions.models.VersionedForeignKey(verbose_name='Variation', blank=True, null=True, to='pretixbase.ItemVariation'), ), migrations.AddField( model_name='cachedticket', diff --git a/src/pretix/base/models.py b/src/pretix/base/models.py index 7396d45ed4..8b01a307d9 100644 --- a/src/pretix/base/models.py +++ b/src/pretix/base/models.py @@ -557,96 +557,6 @@ class ItemCategory(Versionable): return self.sortkey < other.sortkey -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'. - - :param event: The event this belongs to - :type event: Event - :param name: The name of this property. - :type name: str - """ - - event = VersionedForeignKey( - Event, - related_name="properties", - ) - 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(Versionable): - """ - 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 = VersionedForeignKey( - 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", "version_birth_date") - - 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): - return self.position, self.version_birth_date - - def __lt__(self, other): - return self.sortkey < other.sortkey - - def itempicture_upload_to(instance, filename): return '%s/%s/item-%s.%s' % ( instance.event.organizer.slug, instance.event.slug, instance.identity, @@ -675,7 +585,6 @@ class Item(Versionable): :type default_price: decimal.Decimal :param tax_rate: The VAT tax that is included in this item's price (in %) :type tax_rate: decimal.Decimal - :param properties: A set of ``Property`` objects that should be applied to this item :param admission: ``True``, if this item allows persons to enter the event (as opposed to e.g. merchandise) :type admission: bool :param picture: A product picture to be shown next to the product description. @@ -718,17 +627,6 @@ class Item(Versionable): verbose_name=_("Taxes included in percent"), max_digits=7, decimal_places=2 ) - properties = VersionedManyToManyField( - Property, - related_name='items', - verbose_name=_("Properties"), - blank=True, - 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.' - ) - ) admission = models.BooleanField( verbose_name=_("Is an admission ticket"), help_text=_( @@ -945,6 +843,99 @@ class Item(Versionable): return price +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'. + + :param event: The event this belongs to + :type event: Event + :param name: The name of this property. + :type name: str + """ + + event = VersionedForeignKey( + Event, + related_name="properties" + ) + item = VersionedForeignKey( + 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(Versionable): + """ + 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 = VersionedForeignKey( + 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", "version_birth_date") + + 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): + return self.position, self.version_birth_date + + def __lt__(self, other): + return self.sortkey < other.sortkey + + class ItemVariation(Versionable): """ A variation is an item combined with values for all properties diff --git a/src/pretix/control/forms/__init__.py b/src/pretix/control/forms/__init__.py index 06cbfb44a9..e4209c1e66 100644 --- a/src/pretix/control/forms/__init__.py +++ b/src/pretix/control/forms/__init__.py @@ -5,7 +5,9 @@ from itertools import product from django import forms from django.core.exceptions import ValidationError from django.db import transaction -from django.forms import BaseInlineFormSet +from django.forms import ( + BaseInlineFormSet, BaseModelFormSet, ModelForm, modelformset_factory, +) from django.forms.widgets import flatatt from django.utils.encoding import force_text from django.utils.html import format_html @@ -21,6 +23,7 @@ class I18nInlineFormSet(BaseInlineFormSet): This is equivalent to a normal BaseInlineFormset, 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) @@ -30,6 +33,32 @@ class I18nInlineFormSet(BaseInlineFormSet): return super()._construct_form(i, **kwargs) +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 + + class TolerantFormsetModelForm(VersionedModelForm): """ This is equivalent to a normal VersionedModelForm, but works around a problem that @@ -407,7 +436,6 @@ class VariationsField(forms.ModelMultipleChoiceField): class ExtFileField(forms.FileField): - def __init__(self, *args, **kwargs): ext_whitelist = kwargs.pop("ext_whitelist") self.ext_whitelist = [i.lower() for i in ext_whitelist] @@ -422,3 +450,108 @@ class ExtFileField(forms.FileField): if ext not in self.ext_whitelist: raise forms.ValidationError(_("Filetype not allowed!")) return data + + +class BaseNestedFormset(I18nFormSet): + + def add_fields(self, form, index): + # allow the super class to create the fields as usual + super().add_fields(form, index) + + form.nested = [] + for f in self.nested_formset_class: + inner_formset = f( + instance=form.instance, + data=form.data if form.is_bound else None, + prefix='%s-%s' % (form.prefix, f.get_default_prefix()), + queryset=form.instance.values.all(), + event=self.event + ) + form.nested.append(inner_formset) + + def is_valid(self): + result = super(BaseNestedFormset, self).is_valid() + + if self.is_bound: + # look at any nested formsets, as well + for form in self.forms: + if not self._should_delete_form(form): + for n in form.nested: + result = result and n.is_valid() + + return result + + def save(self, commit=True): + result = super(BaseNestedFormset, self).save(commit=commit) + + for form in self.forms: + if not self._should_delete_form(form): + for n in form.nested: + n.save(commit=commit) + + return result + + +def nestedformset_factory(model, nested_formset, form=ModelForm, + formset=BaseNestedFormset, fk_name=None, fields=None, + exclude=None, extra=3, can_order=False, + can_delete=True, max_num=None, + formfield_callback=None, widgets=None, + validate_max=False, localized_fields=None, + labels=None, help_texts=None, error_messages=None): + kwargs = { + 'form': form, + 'formfield_callback': formfield_callback, + 'formset': formset, + 'extra': extra, + 'can_delete': can_delete, + 'can_order': can_order, + 'fields': fields, + 'exclude': exclude, + 'max_num': max_num, + 'widgets': widgets, + 'validate_max': validate_max, + 'localized_fields': localized_fields, + 'labels': labels, + 'help_texts': help_texts, + 'error_messages': error_messages, + } + + nfs_class = modelformset_factory(model, **kwargs) + nfs_class.nested_formset_class = [] + for f in nested_formset: + nfs_class.nested_formset_class.append(f) + return nfs_class + + +class NestedInnerI18nInlineFormSet(I18nFormSet): + """A formset for child objects related to a parent.""" + + def __init__(self, data=None, files=None, instance=None, + save_as_new=False, prefix=None, queryset=None, **kwargs): + if instance is None: + self.instance = self.fk.rel.to() + else: + self.instance = instance + self.save_as_new = save_as_new + if queryset is None: + if self.instance is not None: + queryset = getattr(self.instance, self.fk.related_query_name()).all() + else: + queryset = self.model._default_manager + if self.instance.pk is not None: + qs = queryset + else: + qs = self.model._default_manager.none() + super().__init__(data, files, prefix=prefix, queryset=qs, **kwargs) + + @property + def empty_form(self): + form = self.form( + auto_id=self.auto_id, + prefix=self.add_prefix('__inner_prefix__'), + empty_permitted=True, + event=self.event + ) + self.add_fields(form, None) + return form diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py index a2e898edc1..f991c7f52d 100644 --- a/src/pretix/control/forms/item.py +++ b/src/pretix/control/forms/item.py @@ -124,7 +124,6 @@ class ItemFormGeneral(VersionedModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['category'].queryset = self.instance.event.categories.current.all() - self.fields['properties'].queryset = self.instance.event.properties.current.all() class Meta: model = Item @@ -138,7 +137,6 @@ class ItemFormGeneral(VersionedModelForm): 'picture', 'default_price', 'tax_rate', - 'properties', ] diff --git a/src/pretix/control/templates/pretixcontrol/event/base.html b/src/pretix/control/templates/pretixcontrol/event/base.html index 9b17bbf435..87331d955a 100644 --- a/src/pretix/control/templates/pretixcontrol/event/base.html +++ b/src/pretix/control/templates/pretixcontrol/event/base.html @@ -79,12 +79,6 @@ {% trans "Categories" %} -
+ +
+ +- {% trans "Create new property" %} -
-| {% trans "Product properties" %} | -- |
|---|---|
| {{ p.name }} | -- |
{% blocktrans %}You can not delete the property {{ property }} as long as the following products use it:{% endblocktrans %}
-