Removed multi-dimensional item variations [backwards-incompatible]

This commit is contained in:
Raphael Michel
2015-12-13 15:03:56 +01:00
parent bc13ba9517
commit f748752391
36 changed files with 616 additions and 2019 deletions

View File

@@ -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,
)

View File

@@ -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': [
{

View File

@@ -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

View File

@@ -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),
]

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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)),
],
),
]

View File

@@ -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'
]

View File

@@ -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,

View File

@@ -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:

View File

@@ -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])

View File

@@ -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

View File

@@ -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('<div class="variations" id="{0}">', id_) if id_ else '<div class="variations">'
output = [start_tag]
# TODO: This is very duplicate to pretixcontrol.views.item.ItemVariations.get_forms()
# Find a common abstraction to avoid the repetition.
if dimension == 0:
output.append(format_html('<em>{0}</em>', _("not applicable")))
elif dimension == 1:
output = self.render_1d(output, variations, properties)
else:
output = self.render_nd(output, variations, properties)
output.append(
('<div class="help-block"><a href="#" class="variations-select-all">{0}</a> · '
'<a href="#" class="variations-select-none">{1}</a></div></div>').format(
_("Select all"),
_("Deselect all")
)
)
return mark_safe('\n'.join(output))
def render_1d(self, output, variations, properties):
output.append('<ul>')
for i, variation in enumerate(variations):
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'
w = self.choice_input_class(
self.name, self.value, self.attrs.copy(),
(variation['key'], variation[properties[0].id].value),
i
)
output.append(format_html('<li>{0}</li>', force_text(w)))
output.append('</ul>')
return output
def render_nd(self, output, variations, properties):
# prop1 is the property on all the grid's y-axes
prop1 = properties[0]
prop1v = list(prop1.values.all())
# prop2 is the property on all the grid's x-axes
prop2 = properties[1]
prop2v = list(prop2.values.all())
# We now iterate over the cartesian product of all the other
# properties which are NOT on the axes of the grid because we
# create one grid for any combination of them.
for gridrow in product(*[prop.values.all() for prop in properties[2:]]):
if len(gridrow) > 0:
output.append('<strong>')
output.append(", ".join([str(value.value) for value in gridrow]))
output.append('</strong>')
output.append('<table class="table"><thead><tr><th></th>')
for val2 in prop2v:
output.append(format_html('<th>{0}</th>', val2.value))
output.append('</thead><tbody>')
for val1 in prop1v:
output.append(format_html('<tr><th>{0}</th>', 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('<td><label><input{0} /></label></td>', flatatt(final_attrs)))
output.append('</td>')
output.append('</tbody></table>')
return output
class VariationsCheckboxRenderer(VariationsFieldRenderer):
"""
This is the same as VariationsFieldRenderer but with the choice input class
forced to checkboxes
"""
choice_input_class = forms.widgets.CheckboxChoiceInput
class VariationsSelectMultiple(forms.CheckboxSelectMultiple):
"""
This is the default widget for a VariationsField
"""
renderer = VariationsCheckboxRenderer
_empty_value = []
class VariationsField(forms.ModelMultipleChoiceField):
"""
This form field is intended to be used to let the user select a
variation of a certain item, for example in a restriction plugin.
As this field expects the non-standard keyword parameter ``item``
at initialization time, this is field is normally named ``variations``
and lives inside a ``pretixcontrol.views.forms.RestrictionForm``, which
does some magic to provide this parameter.
"""
def __init__(self, *args, item=None, **kwargs):
self.item = item
if 'widget' not in args or kwargs['widget'] is None:
kwargs['widget'] = VariationsSelectMultiple
super().__init__(*args, **kwargs)
def set_item(self, item: Item):
assert isinstance(item, Item)
self.item = item
self._set_choices(self._get_choices())
def _get_choices(self) -> "list[(str, VariationDict)]":
"""
We can't use a normal QuerySet as there theoretically might be
two types of variations: Some who already have a ItemVariation
object associated with them and some who don't. We therefore use
the item's ``get_all_variations`` method. In the first case, we
use the ItemVariation objects primary key as our choice, key,
in the latter case we use a string constructed from the values
(see VariationDict.key() for implementation details).
"""
if self.item is None:
return ()
variations = self.item.get_all_variations(use_cache=True)
return (
(
v['variation'].id if 'variation' in v else v.key(),
v
) for v in variations
)
def clean(self, value: "list[int]"):
"""
At cleaning time, we have to clean up the mess we produced with our
_get_choices implementation. In the case of ItemVariation object ids
we don't to anything to them, but if one of the selected items is a
list of PropertyValue objects (see _get_choices), we need to create
a new ItemVariation object for this combination and then add this to
our list of selected items.
"""
if self.item is None:
raise ValueError(
"VariationsField object was not properly initialized. Please"
"use a pretixcontrol.views.forms.RestrictionForm form instead of"
"a plain Django ModelForm"
)
# Standard validation foo
if self.required and not value:
raise ValidationError(self.error_messages['required'], code='required')
elif not self.required and not value:
return []
if not isinstance(value, (list, tuple)):
raise ValidationError(self.error_messages['list'], code='list')
cleaned_value = self._clean_value(value)
qs = self.item.variations.filter(id__in=cleaned_value)
# Re-check for consistency
pks = set(force_text(getattr(o, "id")) for o in qs)
for val in cleaned_value:
if force_text(val) not in pks:
raise ValidationError(
self.error_messages['invalid_choice'],
code='invalid_choice',
params={'value': val},
)
# Since this overrides the inherited ModelChoiceField.clean
# we run custom validators here
self.run_validators(cleaned_value)
return qs
def _clean_value(self, value):
# Build up a cache of variations having an ItemVariation object
# For implementation details, see ItemVariation.get_all_variations()
# which uses a very similar method
all_variations = self.item.variations.all().prefetch_related("values")
variations_cache = {
var.to_variation_dict().identify(): var.id for var in all_variations
}
cleaned_value = []
# Wrap this in a transaction to prevent strange database state if we
# get a ValidationError half-way through
with transaction.atomic():
for pk in value:
if ":" in pk:
# A combination of PropertyValues was given
# Hash the combination in the same way as in our cache above
key = ",".join([pair.split(":")[1] for pair in sorted(pk.split(","))])
if key in variations_cache:
# An ItemVariation object already exists for this variation,
# so use this. (This might occur if the variation object was
# created _after_ the user loaded the form but _before_ he
# submitted it.)
cleaned_value.append(str(variations_cache[key]))
continue
# No ItemVariation present, create one!
var = ItemVariation()
var.item_id = self.item.id
var.save()
# Add the values to the ItemVariation object
try:
var.add_values_from_string(pk)
except:
raise ValidationError(
self.error_messages['invalid_pk_value'],
code='invalid_pk_value',
params={'pk': value},
)
variations_cache[key] = var.id
cleaned_value.append(str(var.id))
else:
# An ItemVariation id was given
cleaned_value.append(pk)
return cleaned_value
choices = property(_get_choices, forms.ChoiceField._set_choices)
class ExtFileField(forms.FileField):
def __init__(self, *args, **kwargs):
ext_whitelist = kwargs.pop("ext_whitelist")
@@ -396,108 +125,3 @@ 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

View File

@@ -1,15 +1,13 @@
import copy
from django import forms
from django.forms import BooleanField
from django.forms import BooleanField, ModelMultipleChoiceField
from django.utils.translation import ugettext_lazy as _
from pretix.base.forms import I18nModelForm
from pretix.base.models import (
Item, ItemCategory, ItemVariation, Property, PropertyValue, Question,
Quota,
Item, ItemCategory, ItemVariation, Question, Quota,
)
from pretix.control.forms import TolerantFormsetModelForm, VariationsField
class CategoryForm(I18nModelForm):
@@ -21,24 +19,6 @@ class CategoryForm(I18nModelForm):
]
class PropertyForm(I18nModelForm):
class Meta:
model = Property
localized_fields = '__all__'
fields = [
'name',
]
class PropertyValueForm(TolerantFormsetModelForm):
class Meta:
model = PropertyValue
localized_fields = '__all__'
fields = [
'value',
]
class QuestionForm(I18nModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -75,13 +55,14 @@ class QuotaForm(I18nModelForm):
active_variations = set()
for item in items:
if len(item.properties.all()) > 0:
self.fields['item_%s' % item.id] = VariationsField(
item, label=_("Activate for"),
if len(item.variations.all()) > 0:
self.fields['item_%s' % item.id] = ModelMultipleChoiceField(
label=_("Activate for"),
required=False,
initial=active_variations
initial=active_variations,
queryset=item.variations.all(),
widget=forms.CheckboxSelectMultiple
)
self.fields['item_%s' % item.id].set_item(item)
else:
self.fields['item_%s' % item.id] = BooleanField(
label=_("Activate"),
@@ -125,6 +106,7 @@ class ItemVariationForm(I18nModelForm):
model = ItemVariation
localized_fields = '__all__'
fields = [
'value',
'active',
'default_price',
]

View File

@@ -6,7 +6,6 @@
<h1>{% trans "Modify product:" %} {{ object.name }}</h1>
<ul class="nav nav-pills">
<li {% if "event.item" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.item' organizer=request.event.organizer.slug event=request.event.slug item=object.id %}">{% trans "General information" %}</a></li>
<li {% if "event.item.properties" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.item.properties' organizer=request.event.organizer.slug event=request.event.slug item=object.id %}">{% trans "Properties" %}</a></li>
<li {% if "event.item.variations" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.item.variations' organizer=request.event.organizer.slug event=request.event.slug item=object.id %}">{% trans "Variations" %}</a></li>
</ul>
{% else %}

View File

@@ -1,149 +0,0 @@
{% extends "pretixcontrol/item/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load formset_tags %}
{% block inside %}
<form class="form-horizontal branches" method="post" action="">
{% csrf_token %}
{% if state %}
<div class="alert alert-{{ state.class }}">{{ state.message|safe }}</div>
{% endif %}
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
<div data-formset-body>
{% for form in formset %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
{% bootstrap_field form.name layout='inline' form_group_class="" %}
</h4>
</div>
<div class="panel-body">
{% bootstrap_form_errors form %}
<div class="nested-formset" data-formset-prefix="{{ form.nested.0.prefix }}">
<div data-nested-formset-body>
{{ form.nested.0.management_form }}
{% for f in form.nested.0 %}
<div class="form-group" data-nested-formset-form>
{{ f.id }}
<div class="col-sm-9">
{% bootstrap_field f.value form_group_class="" layout="inline" %}
</div>
<div class="sr-only">
{% bootstrap_field f.ORDER form_group_class="" layout="inline" %}
{% bootstrap_field f.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-sm-3 text-right">
<button type="button" class="btn btn-default"
data-nested-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-default"
data-nested-formset-move-down-button>
<i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-danger" data-nested-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-nested-formset-empty-form>
{% escapescript %}
<div class="form-group" data-nested-formset-form>
{{ form.nested.0.empty_form.id }}
<div class="col-sm-9">
{% bootstrap_field form.nested.0.empty_form.value form_group_class="" layout="inline" %}
</div>
<div class="sr-only">
{% bootstrap_field form.nested.0.empty_form.ORDER form_group_class="" layout="inline" %}
{% bootstrap_field form.nested.0.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-sm-3 text-right">
<button type="button" class="btn btn-default"
data-nested-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-default"
data-nested-formset-move-down-button>
<i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-danger" data-nested-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endescapescript %}
</script>
<p style="margin-top: 10px">
<button type="button" class="btn btn-default" data-nested-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new value" %}</button>
</p>
</div>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ formset.empty_form.id }}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
{% bootstrap_field formset.empty_form.name layout='inline' form_group_class="" %}
</h4>
</div>
<div class="panel-body">
<div class="nested-formset" data-formset-prefix="{{ formset.empty_form.nested.0.prefix }}">
<div data-nested-formset-body>
{{ formset.empty_form.nested.0.management_form }}
</div>
{{ '<script type="form-template" data-nested-formset-empty-form>' }}
<div class="form-group" data-nested-formset-form>
{{ formset.empty_form.nested.0.empty_form.id }}
<div class="col-sm-9">
{% bootstrap_field formset.empty_form.nested.0.empty_form.value form_group_class="" layout="inline" %}
</div>
<div class="sr-only">
{% bootstrap_field formset.empty_form.nested.0.empty_form.ORDER form_group_class="" layout="inline" %}
{% bootstrap_field formset.empty_form.nested.0.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-sm-3 text-right">
<button type="button" class="btn btn-default"
data-nested-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-default"
data-nested-formset-move-down-button>
<i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-danger" data-nested-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{{ '</script>' }}
<p style="margin-top: 10px">
<button type="button" class="btn btn-default" data-nested-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new value" %}</button>
</p>
</div>
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new property" %}</button>
</p>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,87 @@
{% extends "pretixcontrol/item/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load formset_tags %}
{% block inside %}
<form class="form-horizontal branches" method="post" action="">
{% csrf_token %}
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
<div data-formset-body>
{% for form in formset %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
{% bootstrap_field form.ORDER form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
<div class="row">
<div class="col-md-9">
{% bootstrap_field form.value layout='inline' form_group_class="" %}
</div>
<div class="col-md-3 text-right">
<button type="button" class="btn btn-default" data-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-default" data-formset-move-down-button>
<i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</h4>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_form_errors form %}
{% bootstrap_field form.active layout='horizontal' %}
{% bootstrap_field form.default_price layout='horizontal' %}
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ formset.empty_form.id }}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
{% bootstrap_field formset.empty_form.ORDER form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
<div class="row">
<div class="col-md-9">
{% bootstrap_field formset.empty_form.value layout='inline' form_group_class="" %}
</div>
<div class="col-md-3 text-right">
<button type="button" class="btn btn-default" data-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-default" data-formset-move-down-button>
<i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</h4>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_field formset.empty_form.active layout='horizontal' %}
{% bootstrap_field formset.empty_form.default_price layout='horizontal' %}
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new variation" %}</button>
</p>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -1,6 +0,0 @@
{% extends "pretixcontrol/item/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inside %}
<p><em>{% trans "You have to define and select propreties to be able to configure variations." %}</em></p>
{% endblock %}

View File

@@ -1,35 +0,0 @@
{% extends "pretixcontrol/item/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inside %}
<form action="" method="post">
{% csrf_token %}
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>{{ properties.0 }}</th>
<th>{% trans "Active" %}</th>
<th>{% trans "Price" %}</th>
</tr>
</thead>
<tbody>
{% for form in forms %}
{% bootstrap_form_errors form type='all' layout='inline' %}
<tr>
<td>{{ form.values.0 }}</td>
<td>{% bootstrap_field form.active layout='inline' %}</td>
<td>{% bootstrap_field form.default_price layout='inline' %} {{ form.default_price.errors }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -1,51 +0,0 @@
{% extends "pretixcontrol/item/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inside %}
<form action="" method="post">
{% csrf_token %}
{% for major in forms %}
{% if major.row %}
<h3>{{ major.row }}</h3>
{% endif %}
<div class="table-responsive">
<table class="table variation-matrix">
<thead>
<tr>
<th></th>
{% for val in properties.1.values.all %}
<th>{{ val.value }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for sub in major.forms %}
<tr>
<td>{{ sub.row.value }}</td>
{% for form in sub.forms %}
<td>
<div class="row">
<div class="col-sm-5">
{% bootstrap_field form.active layout='inline' %}
</div>
<div class="col-sm-7">
{% bootstrap_field form.default_price layout='inline' %}
</div>
</div>
{{ form.default_price.errors }}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endfor %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -30,8 +30,6 @@ urlpatterns = [
url(r'^items/(?P<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
url(r'^items/(?P<item>\d+)/variations$', item.ItemVariations.as_view(),
name='event.item.variations'),
url(r'^items/(?P<item>\d+)/properties$', item.ItemProperties.as_view(),
name='event.item.properties'),
url(r'^items/(?P<item>\d+)/up$', item.item_move_up, name='event.items.up'),
url(r'^items/(?P<item>\d+)/down$', item.item_move_down, name='event.items.down'),
url(r'^items/(?P<item>\d+)/delete$', item.ItemDelete.as_view(), name='event.items.delete'),

View File

@@ -1,9 +1,7 @@
from itertools import product
from django.contrib import messages
from django.core.urlresolvers import resolve, reverse
from django.db import transaction
from django.forms.models import inlineformset_factory
from django.forms.models import ModelMultipleChoiceField, inlineformset_factory
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import redirect
from django.utils.functional import cached_property
@@ -13,16 +11,12 @@ from django.views.generic.base import TemplateView
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import DeleteView
from pretix.base.i18n import I18nFormSet
from pretix.base.models import (
Item, ItemCategory, ItemVariation, Property, PropertyValue, Question,
Quota,
)
from pretix.control.forms import (
NestedInnerI18nInlineFormSet, VariationsField, nestedformset_factory,
Item, ItemCategory, ItemVariation, Question, Quota,
)
from pretix.control.forms.item import (
CategoryForm, ItemFormGeneral, ItemVariationForm, PropertyForm,
PropertyValueForm, QuestionForm, QuotaForm,
CategoryForm, ItemFormGeneral, ItemVariationForm, QuestionForm, QuotaForm,
)
from pretix.control.permissions import (
EventPermissionRequiredMixin, event_permission_required,
@@ -350,7 +344,7 @@ class QuotaList(ListView):
class QuotaEditorMixin:
@cached_property
def items(self) -> "List[Item]":
return list(self.request.event.items.all().prefetch_related("properties", "variations"))
return list(self.request.event.items.all().prefetch_related("variations"))
def get_form(self, form_class=QuotaForm):
if not hasattr(self, '_form'):
@@ -376,7 +370,7 @@ class QuotaEditorMixin:
for item in self.items:
field = form.fields['item_%s' % item.id]
data = form.cleaned_data['item_%s' % item.id]
if isinstance(field, VariationsField):
if isinstance(field, ModelMultipleChoiceField):
for v in data:
selected_variations.append(v)
if data and item not in items:
@@ -548,264 +542,59 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie
return super().form_valid(form)
class ItemProperties(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
permission = 'can_change_items'
template_name = 'pretixcontrol/item/properties.html'
def get_success_url(self) -> str:
return reverse('control:event.item.properties', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
'item': self.get_object().id,
})
def get_inner_formset_class(self):
formsetclass = inlineformset_factory(
Property, PropertyValue,
form=PropertyValueForm,
formset=NestedInnerI18nInlineFormSet,
can_order=True, extra=0
)
return formsetclass
def get_outer_formset(self):
formsetclass = nestedformset_factory(
Property, [self.get_inner_formset_class()],
form=PropertyForm, can_order=False, can_delete=True, extra=0
)
formset = formsetclass(self.request.POST if self.request.method == "POST" else None,
queryset=Property.objects.filter(item=self.object).prefetch_related('values'),
event=self.request.event)
return formset
def get_context_data(self, **kwargs):
self.object = self.get_object()
ctx = super().get_context_data(**kwargs)
ctx['formset'] = self.get_outer_formset()
return ctx
@transaction.atomic()
def form_valid(self, formset):
for f in formset:
f.instance.event = self.request.event
f.instance.item = self.get_object()
is_created = not f.instance.pk
f.instance.save()
if f.has_changed() and not is_created:
change_data = {
k: f.cleaned_data.get(k) for k in f.changed_data
}
change_data['id'] = f.instance.pk
f.instance.item.log_action(
'pretix.event.item.property.changed', user=self.request.user, data=change_data
)
elif is_created:
change_data = dict(f.cleaned_data)
change_data['id'] = f.instance.pk
f.instance.item.log_action(
'pretix.event.item.property.added', user=self.request.user, data=change_data
)
for n in f.nested:
for fn in n.deleted_forms:
f.instance.item.log_action(
'pretix.event.item.property.value.deleted', user=self.request.user, data={
'id': fn.instance.pk
}
)
fn.instance.delete()
fn.instance.pk = None
for i, fn in enumerate(n.ordered_forms + [ef for ef in n.extra_forms if ef not in n.ordered_forms]):
fn.instance.position = i
fn.instance.prop = f.instance
fn.save()
if f.has_changed():
change_data = {k: f.cleaned_data.get(k) for k in f.changed_data}
change_data['id'] = f.instance.pk
f.instance.item.log_action(
'pretix.event.item.property.value.changed', user=self.request.user, data=change_data
)
for form in n.extra_forms:
if not form.has_changed():
continue
if n.can_delete and n._should_delete_form(form):
continue
change_data = dict(f.cleaned_data)
n.save_new(form)
change_data['id'] = form.instance.pk
f.instance.item.log_action(
'pretix.event.item.property.value.added', user=self.request.user, data=change_data
)
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
def post(self, request, *args, **kwargs):
self.object = self.get_object()
formset = self.get_outer_formset()
if formset.is_valid():
return self.form_valid(formset)
else:
return self.get(request, *args, **kwargs)
class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
permission = 'can_change_items'
template_name = 'pretixcontrol/item/variations.html'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.item = None
def get_form(self, variation, data=None) -> ItemVariationForm:
"""
Return the dict for one given variation. Variations are expected to be
dictionaries in the format of Item.get_all_variations()
"""
# Values are all dictionary ite
values = [i[1] for i in sorted([it for it in variation.items() if it[0] != 'variation'])]
if 'variation' in variation:
form = ItemVariationForm(
data,
instance=variation['variation'],
prefix=",".join([str(i.id) for i in values]),
)
else:
inst = ItemVariation(item=self.object)
inst.item_id = self.object.id
inst.creation = True
form = ItemVariationForm(
data,
instance=inst,
prefix=",".join([str(i.id) for i in values]),
)
form.values = values
return form
def get_forms(self) -> tuple:
"""
Returns one form per possible item variation. The forms are returned
twice: The first entry in the returned tuple contains a 1-, 2- or
3-dimensional list, depending on the number of properties associated
with this item (this is being used for form display), the second
contains all forms in one single list (this is used for processing).
The first, hierarchical list, is a list of dicts on all levels but the
last one, where the dict contains the two entries 'row' containing a
string describing this layer and 'forms' which contains the forms or
the next list of dicts.
"""
forms = []
forms_flat = []
variations = self.object.get_all_variations()
data = self.request.POST if self.request.method == 'POST' else None
if self.dimension == 1:
# For one-dimensional structures we just have a list of forms
for variation in variations:
form = self.get_form(variation, data)
forms.append(form)
forms_flat = forms
elif self.dimension >= 2:
# For 2 or more dimensional structures we display a list of grids
# of forms
# prop1 is the property on all the grid's y-axes
prop1 = self.properties[0]
# prop2 is the property on all the grid's x-axes
prop2 = self.properties[1]
def selector(values):
# Given an iterable of PropertyValue objects, this will return a
# list of their primary keys, ordered by the primary keys of the
# properties they belong to EXCEPT the value for the property prop2.
# We'll see later why we need this.
return [
v.id for v in sorted(values, key=lambda v: v.prop.id)
if v.prop.id != prop2.id
]
def sort(v):
# Given a list of variations, this will sort them by their position
# on the x-axis
return v[prop2.id].sortkey
# We now iterate over the cartesian product of all the other
# properties which are NOT on the axes of the grid because we
# create one grid for any combination of them.
for gridrow in product(*[prop.values.all() for prop in self.properties[2:]]):
grids = []
for val1 in prop1.values.all():
formrow = []
# We are now inside one of the rows of the grid and have to
# select the variations to display in this row. In order to
# 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,))
# We now iterate over all variations who generate the same
# selector as 'selection'.
filtered = [v for v in variations if selector(v.relevant_values()) == selection]
for variation in sorted(filtered, key=sort):
form = self.get_form(variation, data)
formrow.append(form)
forms_flat.append(form)
grids.append({'row': val1, 'forms': formrow})
forms.append({'row': ", ".join([str(value.value) for value in gridrow]), 'forms': grids})
return forms, forms_flat
def main(self, request, *args, **kwargs):
self.object = self.get_object()
self.properties = list(self.object.properties.all().prefetch_related("values"))
self.dimension = len(self.properties)
self.forms, self.forms_flat = self.get_forms()
def get(self, request, *args, **kwargs):
self.main(request, *args, **kwargs)
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
@cached_property
def formset(self):
formsetclass = inlineformset_factory(
Item, ItemVariation,
form=ItemVariationForm, formset=I18nFormSet,
can_order=True, can_delete=True, extra=0
)
return formsetclass(self.request.POST if self.request.method == "POST" else None,
queryset=ItemVariation.objects.filter(item=self.get_object()),
event=self.request.event)
def post(self, request, *args, **kwargs):
self.main(request, *args, **kwargs)
context = self.get_context_data(object=self.object)
valid = True
with transaction.atomic():
for form in self.forms_flat:
if form.is_valid() and form.has_changed():
form.save()
change_data = {
k: form.cleaned_data.get(k) for k in form.changed_data
}
change_data['id'] = form.instance.pk
self.object.log_action(
'pretix.event.item.variation.changed', user=self.request.user, data=change_data
if self.formset.is_valid():
for form in self.formset.deleted_forms:
if not form.instance.pk:
continue
self.get_object().log_action(
'pretix.event.item.variation.deleted', user=self.request.user, data={
'id': form.instance.pk
}
)
if hasattr(form.instance, 'creation') and form.instance.creation:
# We need this special 'creation' field set to true in get_form
# for newly created items as cleanerversion does already set the
# primary key in its post_init hook
form.instance.values.add(*form.values)
elif not form.is_valid and form.has_changed():
valid = False
if valid:
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
return self.render_to_response(context)
form.instance.delete()
form.instance.pk = None
def get_template_names(self) -> "List[str]":
if self.dimension == 0:
return ['pretixcontrol/item/variations_0d.html']
elif self.dimension == 1:
return ['pretixcontrol/item/variations_1d.html']
elif self.dimension >= 2:
return ['pretixcontrol/item/variations_nd.html']
for i, form in enumerate(self.formset.ordered_forms + [
ef for ef in self.formset.extra_forms if (ef not in self.formset.ordered_forms and ef not in
self.formset.deleted_forms)
]):
form.instance.position = i
form.instance.item = self.get_object()
created = not form.instance.pk
form.save()
if form.has_changed():
change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
change_data['id'] = form.instance.pk
self.get_object().log_action(
'pretix.event.item.variation.changed' if not created else
'pretix.event.item.variation.added',
user=self.request.user, data=change_data
)
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
return self.get(request, *args, **kwargs)
def get_success_url(self) -> str:
return reverse('control:event.item.variations', kwargs={
@@ -815,9 +604,9 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
})
def get_context_data(self, **kwargs) -> dict:
self.object = self.get_object()
context = super().get_context_data(**kwargs)
context['forms'] = self.forms
context['properties'] = self.properties
context['formset'] = self.formset
return context
@@ -835,13 +624,13 @@ class ItemDelete(EventPermissionRequiredMixin, DeleteView):
def is_allowed(self) -> bool:
return not self.get_object().positions.exists()
def get_object(self, queryset=None) -> Property:
def get_object(self, queryset=None) -> Item:
if not hasattr(self, 'object') or not self.object:
try:
self.object = self.request.event.items.get(
id=self.kwargs['item']
)
except Property.DoesNotExist:
except Item.DoesNotExist:
raise Http404(_("The requested product does not exist."))
return self.object

View File

@@ -126,8 +126,7 @@ class OrderDetail(OrderView):
).select_related(
'item', 'variation'
).prefetch_related(
'variation__values', 'variation__values__prop', 'item__questions',
'answers'
'item__questions', 'answers'
)
# Group items of the same variation

View File

@@ -1,21 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-13 11:44
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('pretixbase', '0001_initial'),
('pretixbase', '0002_auto_20151213_1144'),
]
operations = [
migrations.CreateModel(
name='KnownDomain',
fields=[
('domainname', models.CharField(primary_key=True, max_length=255, serialize=False)),
('organizer', models.ForeignKey(to='pretixbase.Organizer', null=True, blank=True, related_name='domains')),
('domainname', models.CharField(max_length=255, primary_key=True, serialize=False)),
('organizer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='domains', to='pretixbase.Organizer')),
],
options={
'verbose_name_plural': 'Known domains',

View File

@@ -109,7 +109,7 @@
<div class="col-md-2 col-xs-6 availability-box available">
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
max="{{ var.cached_availability.1 }}"
name="variation_{{ item.id }}_{{ var.variation.id }}">
name="variation_{{ item.id }}_{{ var.id }}">
</div>
{% else %}
{% include "pretixpresale/event/fragment_availability.html" with avail=var.cached_availability.0 %}

View File

@@ -21,7 +21,6 @@ class CartMixin:
).select_related(
'item', 'variation'
).prefetch_related(
'variation__values', 'variation__values__prop',
'item__questions', 'answers'
))
@@ -30,7 +29,7 @@ class CartMixin:
cart_id=self.request.session.session_key, event=self.request.event
)
prefetch = ['variation__values', 'variation__values__prop']
prefetch = []
if answers:
prefetch.append('item__questions')
prefetch.append('answers')

View File

@@ -20,35 +20,32 @@ class EventIndex(EventViewMixin, CartMixin, TemplateView):
).select_related(
'category', # for re-grouping
).prefetch_related(
'properties', # for .get_all_available_variations()
'quotas', 'variations__quotas', 'quotas__event' # for .availability()
).annotate(quotac=Count('quotas')).filter(
quotac__gt=0
).order_by('category__position', 'category_id', 'position', 'name')
for item in items:
item.available_variations = sorted(item.get_all_available_variations(),
key=lambda vd: vd.ordered_values())
item.has_variations = (len(item.available_variations) != 1
or not item.available_variations[0].empty())
item.available_variations = list(item.variations.filter(active=True, quotas__isnull=False).distinct())
item.has_variations = item.variations.exists()
if not item.has_variations:
item.cached_availability = list(item.check_quotas())
item.cached_availability[1] = min((item.cached_availability[1]
if item.cached_availability[1] is not None else sys.maxsize),
int(self.request.event.settings.max_items_per_order))
item.price = item.available_variations[0]['price']
item.price = item.default_price
else:
for var in item.available_variations:
var.cached_availability = list(var['variation'].check_quotas())
var.cached_availability = list(var.check_quotas())
var.cached_availability[1] = min(var.cached_availability[1]
if var.cached_availability[1] is not None else sys.maxsize,
int(self.request.event.settings.max_items_per_order))
var.price = var.get('price', item.default_price)
var.price = var.default_price if var.default_price is not None else item.default_price
if len(item.available_variations) > 0:
item.min_price = min([v.price for v in item.available_variations])
item.max_price = max([v.price for v in item.available_variations])
items = [item for item in items if len(item.available_variations) > 0]
items = [item for item in items if len(item.available_variations) > 0 or not item.has_variations]
# Regroup those by category
context['items_by_category'] = sorted(

View File

@@ -3,27 +3,12 @@
$(function () {
"use strict";
var nested_formset_config = {
form: '[data-nested-formset-form]',
emptyForm: 'script[type=form-template][data-nested-formset-empty-form]',
body: '[data-nested-formset-body]',
add: '[data-nested-formset-add]',
deleteButton: '[data-nested-formset-delete-button]',
moveUpButton: '[data-nested-formset-move-up-button]',
moveDownButton: '[data-nested-formset-move-down-button]',
animateForms: true,
reorderMode: 'animate',
empty_prefix: '__inner_prefix__'
};
$("[data-formset]").formset(
{
animateForms: true,
reorderMode: 'animate'
}
).on("formAdded", "[data-formset-form]", function () {
$(this).find(".nested-formset").formset(nested_formset_config);
});
$(".nested-formset").formset(nested_formset_config);
);
$(document).on("click", ".variations .variations-select-all", function (e) {
$(this).parent().parent().find("input[type=checkbox]").prop("checked", true).change();
e.stopPropagation();

View File

@@ -7,143 +7,9 @@ from django.utils.timezone import now
from pretix.base.models import (
CachedFile, CartPosition, Event, Item, ItemCategory, ItemVariation, Order,
OrderPosition, Organizer, Property, PropertyValue, Question, Quota, User,
OrderPosition, Organizer, Question, Quota, User,
)
from pretix.base.services.orders import mark_order_paid
from pretix.base.types import VariationDict
class ItemVariationsTest(TestCase):
"""
This test case tests various methods around the properties /
variations concept.
"""
@classmethod
def setUpTestData(cls):
o = Organizer.objects.create(name='Dummy', slug='dummy')
cls.event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
date_from=now(),
)
cls.p_size = Property.objects.create(event=cls.event, name='Size')
cls.pv_size_s = PropertyValue.objects.create(prop=cls.p_size, value='S')
cls.pv_size_m = PropertyValue.objects.create(prop=cls.p_size, value='M')
PropertyValue.objects.create(prop=cls.p_size, value='L')
cls.p_color = Property.objects.create(event=cls.event, name='Color')
cls.pv_color_black = PropertyValue.objects.create(prop=cls.p_color, value='black')
PropertyValue.objects.create(prop=cls.p_color, value='blue')
def test_variationdict(self):
i = Item.objects.create(event=self.event, name='Dummy', default_price=0)
self.p_size.item = i
self.p_size.save()
iv = ItemVariation.objects.create(item=i)
iv.values.add(self.pv_size_s)
variations = i.get_all_variations()
for vd in variations:
for i, v in vd.relevant_items():
self.assertIs(type(v), PropertyValue)
for v in vd.relevant_values():
self.assertIs(type(v), PropertyValue)
if vd[self.p_size.pk] == self.pv_size_s:
vd1 = vd
vd2 = VariationDict()
vd2[self.p_size.pk] = self.pv_size_s
self.assertEqual(vd2.identify(), vd1.identify())
self.assertEqual(vd2, vd1)
vd2[self.p_size.pk] = self.pv_size_m
self.assertNotEqual(vd2.identify(), vd.identify())
self.assertNotEqual(vd2, vd1)
vd3 = vd2.copy()
self.assertEqual(vd3, vd2)
vd2[self.p_size.pk] = self.pv_size_s
self.assertNotEqual(vd3, vd2)
vd4 = VariationDict()
vd4[4] = 'b'
vd4[2] = 'a'
self.assertEqual(vd4.ordered_values(), ['a', 'b'])
def test_get_all_variations(self):
i = Item.objects.create(event=self.event, name='Dummy', default_price=0)
# No properties available
v = i.get_all_variations()
self.assertEqual(len(v), 1)
self.assertEqual(v[0], {})
# One property, no variations
self.p_size.item = i
self.p_size.save()
v = i.get_all_variations()
self.assertIs(type(v), list)
self.assertEqual(len(v), 3)
values = []
for var in v:
self.assertIs(type(var), VariationDict)
self.assertIn(self.p_size.pk, var)
self.assertIs(type(var[self.p_size.pk]), PropertyValue)
values.append(var[self.p_size.pk].value)
self.assertEqual(sorted([str(V) for V in values]), sorted(['S', 'M', 'L']))
# One property, one variation
iv = ItemVariation.objects.create(item=i, default_price=0)
iv.values.add(self.pv_size_s)
v = i.get_all_variations()
self.assertIs(type(v), list)
self.assertEqual(len(v), 3)
values = []
num_variations = 0
for var in v:
self.assertIs(type(var), VariationDict)
if 'variation' in var and type(var['variation']) is ItemVariation:
self.assertEqual(iv.pk, var['variation'].pk)
values.append(var['variation'].values.all()[0].value)
num_variations += 1
elif self.p_size.pk in var:
self.assertIs(type(var[self.p_size.pk]), PropertyValue)
values.append(var[self.p_size.pk].value)
self.assertEqual(sorted([str(V) for V in values]), sorted(['S', 'M', 'L']))
self.assertEqual(num_variations, 1)
# Two properties, one variation
self.p_color.item = i
self.p_color.save()
iv.values.add(self.pv_color_black)
v = i.get_all_variations()
self.assertIs(type(v), list)
self.assertEqual(len(v), 6)
values = []
num_variations = 0
for var in v:
self.assertIs(type(var), VariationDict)
if 'variation' in var:
self.assertEqual(iv.pk, var['variation'].pk)
values.append(sorted([str(ivv.value) for ivv in iv.values.all()]))
self.assertEqual(sorted([str(ivv.value) for ivv in iv.values.all()]), sorted(['S', 'black']))
num_variations += 1
else:
values.append(sorted([str(pv.value) for pv in var.values()]))
self.assertEqual(sorted(values), sorted([
['S', 'black'],
['S', 'blue'],
['M', 'black'],
['M', 'blue'],
['L', 'black'],
['L', 'blue'],
]))
self.assertEqual(num_variations, 1)
class UserTestCase(TestCase):
@@ -184,12 +50,7 @@ class BaseQuotaTestCase(TestCase):
self.item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
admission=True)
self.item2 = Item.objects.create(event=self.event, name="T-Shirt", default_price=23)
p = Property.objects.create(event=self.event, name='Size', item=self.item2)
pv1 = PropertyValue.objects.create(prop=p, value='S')
PropertyValue.objects.create(prop=p, value='M')
PropertyValue.objects.create(prop=p, value='L')
self.var1 = ItemVariation.objects.create(item=self.item2)
self.var1.values.add(pv1)
self.var1 = ItemVariation.objects.create(item=self.item2, value='S')
class QuotaTestCase(BaseQuotaTestCase):

View File

@@ -3,13 +3,12 @@ import os
import time
import unittest
from django.utils.timezone import now
from selenium.webdriver.support.select import Select
from tests.base import BrowserTest
from pretix.base.models import (
Event, EventPermission, Item, ItemCategory, Organizer, OrganizerPermission,
Property, PropertyValue, Question, Quota, User,
Event, EventPermission, Item, ItemCategory, ItemVariation, Organizer,
OrganizerPermission, Question, Quota, User,
)
@@ -105,62 +104,6 @@ class CategoriesTest(ItemFormTest):
assert not ItemCategory.objects.filter(id=c.id).exists()
class PropertiesTest(ItemFormTest):
"""
Properties have moved from their original place, skip this for now
"""
@unittest.skip
def test_create(self):
self.driver.get('%s/control/event/%s/%s/properties/add' % (
self.live_server_url, self.orga1.slug, self.event1.slug
))
self.driver.find_element_by_css_selector("#id_name_0").send_keys('Size')
self.driver.find_element_by_name("values-0-value_0").send_keys('S')
self.driver.find_element_by_name("values-1-value_0").send_keys('M')
self.scroll_and_click(self.driver.find_element_by_class_name("btn-save"))
self.driver.find_element_by_class_name("alert-success")
self.assertIn("Size", self.driver.find_element_by_css_selector("#page-wrapper table").text)
self.driver.find_element_by_partial_link_text("Size").click()
self.assertEqual("S", self.driver.find_element_by_name("values-0-value_0").get_attribute("value"))
self.assertEqual("M", self.driver.find_element_by_name("values-1-value_0").get_attribute("value"))
@unittest.skip
def test_update(self):
c = Property.objects.create(event=self.event1, name="Size")
p1 = PropertyValue.objects.create(prop=c, position=0, value="S")
p2 = PropertyValue.objects.create(prop=c, position=1, value="M")
self.driver.get('%s/control/event/%s/%s/properties/%s/' % (
self.live_server_url, self.orga1.slug, self.event1.slug, c.id
))
self.driver.find_element_by_css_selector("#id_name_0").clear()
self.driver.find_element_by_css_selector("#id_name_0").send_keys('Color')
self.driver.find_elements_by_css_selector("div.form-group button.btn-danger")[0].click()
self.scroll_into_view(self.driver.find_element_by_name("values-1-value_0"))
self.driver.find_element_by_name("values-1-value_0").clear()
self.driver.find_element_by_name("values-1-value_0").send_keys('red')
self.driver.find_element_by_css_selector("button[data-formset-add]").click()
self.driver.find_element_by_name("values-2-value_0").send_keys('blue')
self.driver.find_element_by_class_name("btn-save").click()
self.driver.find_element_by_class_name("alert-success")
self.assertEqual("red", self.driver.find_element_by_name("values-0-value_0").get_attribute("value"))
self.assertEqual("blue", self.driver.find_element_by_name("values-1-value_0").get_attribute("value"))
assert str(Property.objects.get(id=c.id).name) == 'Color'
assert str(PropertyValue.objects.get(id=p2.id).value) == 'red'
assert not PropertyValue.objects.filter(id=p1.id).exists()
@unittest.skip
def test_delete(self):
c = Property.objects.create(event=self.event1, name="Size")
self.driver.get('%s/control/event/%s/%s/properties/%s/delete' % (
self.live_server_url, self.orga1.slug, self.event1.slug, c.id
))
self.driver.find_element_by_class_name("btn-danger").click()
self.driver.find_element_by_class_name("alert-success")
self.assertNotIn("Size", self.driver.find_element_by_css_selector("#page-wrapper table").text)
assert not Property.objects.filter(id=c.id).exists()
class QuestionsTest(ItemFormTest):
def test_create(self):
@@ -215,9 +158,8 @@ class QuotaTest(ItemFormTest):
c = Quota.objects.create(event=self.event1, name="Full house", size=500)
item1 = Item.objects.create(event=self.event1, name="Standard", default_price=0)
item2 = Item.objects.create(event=self.event1, name="Business", default_price=0)
prop1 = Property.objects.create(event=self.event1, name="Level", item=item2)
PropertyValue.objects.create(prop=prop1, value="Silver")
PropertyValue.objects.create(prop=prop1, value="Gold")
ItemVariation.objects.create(item=item2, value="Silver")
ItemVariation.objects.create(item=item2, value="Gold")
self.driver.get('%s/control/event/%s/%s/quotas/%s/' % (
self.live_server_url, self.orga1.slug, self.event1.slug, c.id
))

View File

@@ -35,7 +35,6 @@ event_urls = [
"items/add",
"items/1/",
"items/1/variations",
"items/1/properties",
"categories/",
"categories/add",
"categories/2/",

View File

@@ -6,7 +6,7 @@ from django.utils.timezone import now
from pretix.base.models import (
Event, EventPermission, Item, ItemVariation, Order, OrderPosition,
Organizer, Property, PropertyValue, User,
Organizer, User,
)
@@ -20,13 +20,8 @@ def env():
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
EventPermission.objects.create(user=user, event=event)
shirt = Item.objects.create(event=event, name='T-Shirt', default_price=12)
prop1 = Property.objects.create(event=event, name="Color", item=shirt)
val1 = PropertyValue.objects.create(prop=prop1, value="Red", position=0)
val2 = PropertyValue.objects.create(prop=prop1, value="Black", position=1)
shirt_red = ItemVariation.objects.create(item=shirt, default_price=14)
shirt_red.values.add(val1)
var2 = ItemVariation.objects.create(item=shirt)
var2.values.add(val2)
shirt_red = ItemVariation.objects.create(item=shirt, default_price=14, value="Red")
ItemVariation.objects.create(item=shirt, value="Blue")
ticket = Item.objects.create(event=event, name='Ticket', default_price=23)
o1 = Order.objects.create(
code='FOO', event=event, status=Order.STATUS_PENDING,

View File

@@ -8,7 +8,7 @@ from django.utils.timezone import now
from pretix.base.models import (
CartPosition, Event, Item, ItemCategory, ItemVariation, Organizer,
Property, PropertyValue, Question, QuestionAnswer, Quota, User,
Question, QuestionAnswer, Quota,
)
@@ -23,14 +23,9 @@ class CartTestMixin:
self.category = ItemCategory.objects.create(event=self.event, name="Everything", position=0)
self.quota_shirts = Quota.objects.create(event=self.event, name='Shirts', size=2)
self.shirt = Item.objects.create(event=self.event, name='T-Shirt', category=self.category, default_price=12)
prop1 = Property.objects.create(event=self.event, name="Color", item=self.shirt)
val1 = PropertyValue.objects.create(prop=prop1, value="Red", position=0)
val2 = PropertyValue.objects.create(prop=prop1, value="Black", position=1)
self.quota_shirts.items.add(self.shirt)
self.shirt_red = ItemVariation.objects.create(item=self.shirt, default_price=14)
self.shirt_red.values.add(val1)
var2 = ItemVariation.objects.create(item=self.shirt)
var2.values.add(val2)
self.shirt_red = ItemVariation.objects.create(item=self.shirt, default_price=14, value='Red')
var2 = ItemVariation.objects.create(item=self.shirt, value='Blue')
self.quota_shirts.variations.add(self.shirt_red)
self.quota_shirts.variations.add(var2)
self.quota_tickets = Quota.objects.create(event=self.event, name='Tickets', size=5)

View File

@@ -6,8 +6,7 @@ from django.utils.timezone import now
from tests.base import BrowserTest
from pretix.base.models import (
Event, Item, ItemCategory, ItemVariation, Organizer, Property,
PropertyValue, Quota, User,
Event, Item, ItemCategory, ItemVariation, Organizer, Quota,
)
@@ -99,9 +98,7 @@ class ItemDisplayTest(EventTestMixin, BrowserTest):
c = ItemCategory.objects.create(event=self.event, name="Entry tickets", position=0)
q = Quota.objects.create(event=self.event, name='Quota', size=2)
item = Item.objects.create(event=self.event, name='Early-bird ticket', category=c, default_price=0)
prop1 = Property.objects.create(event=self.event, name="Color", item=item)
PropertyValue.objects.create(prop=prop1, value="Red")
PropertyValue.objects.create(prop=prop1, value="Black")
ItemVariation.objects.create(item=item, value='Blue')
q.items.add(item)
self.driver.get('%s/%s/%s/' % (self.live_server_url, self.orga.slug, self.event.slug))
resp = self.client.get('%s/%s/%s/' % (self.live_server_url, self.orga.slug, self.event.slug))
@@ -111,12 +108,9 @@ class ItemDisplayTest(EventTestMixin, BrowserTest):
c = ItemCategory.objects.create(event=self.event, name="Entry tickets", position=0)
q = Quota.objects.create(event=self.event, name='Quota', size=2)
item = Item.objects.create(event=self.event, name='Early-bird ticket', category=c, default_price=0)
prop1 = Property.objects.create(event=self.event, name="Color", item=item)
val1 = PropertyValue.objects.create(prop=prop1, value="Red")
PropertyValue.objects.create(prop=prop1, value="Black")
var1 = ItemVariation.objects.create(item=item, value='Red')
ItemVariation.objects.create(item=item, value='Blue')
q.items.add(item)
var1 = ItemVariation.objects.create(item=item)
var1.values.add(val1)
q.variations.add(var1)
self._assert_variation_found()
@@ -124,12 +118,9 @@ class ItemDisplayTest(EventTestMixin, BrowserTest):
c = ItemCategory.objects.create(event=self.event, name="Entry tickets", position=0)
q = Quota.objects.create(event=self.event, name='Quota', size=None)
item = Item.objects.create(event=self.event, name='Early-bird ticket', category=c, default_price=0)
prop1 = Property.objects.create(event=self.event, name="Color", item=item)
val1 = PropertyValue.objects.create(prop=prop1, value="Red")
PropertyValue.objects.create(prop=prop1, value="Black")
var1 = ItemVariation.objects.create(item=item, value='Red')
ItemVariation.objects.create(item=item, value='Blue')
q.items.add(item)
var1 = ItemVariation.objects.create(item=item)
var1.values.add(val1)
q.variations.add(var1)
self._assert_variation_found()
@@ -149,16 +140,11 @@ class ItemDisplayTest(EventTestMixin, BrowserTest):
c = ItemCategory.objects.create(event=self.event, name="Entry tickets", position=0)
q = Quota.objects.create(event=self.event, name='Quota', size=2)
item = Item.objects.create(event=self.event, name='Early-bird ticket', category=c, default_price=12)
prop1 = Property.objects.create(event=self.event, name="Color", item=item)
val1 = PropertyValue.objects.create(prop=prop1, value="Red", position=0)
val2 = PropertyValue.objects.create(prop=prop1, value="Black", position=1)
q.items.add(item)
var1 = ItemVariation.objects.create(item=item, default_price=14)
var1.values.add(val1)
var2 = ItemVariation.objects.create(item=item)
var2.values.add(val2)
var1 = ItemVariation.objects.create(item=item, value='Red', default_price=14, position=1)
var2 = ItemVariation.objects.create(item=item, value='Black', position=2)
q.variations.add(var1)
q.variations.add(var2)
q.items.add(item)
self.driver.get('%s/%s/%s/' % (self.live_server_url, self.orga.slug, self.event.slug))
self.assertIn("Early-bird",
self.driver.find_element_by_css_selector("section:nth-of-type(1) div:nth-of-type(1)").text)

View File

@@ -7,7 +7,7 @@ from django.utils.timezone import now
from pretix.base.models import (
Event, Item, ItemCategory, ItemVariation, Order, OrderPosition, Organizer,
Property, PropertyValue, Question, Quota,
Question, Quota,
)
@@ -26,15 +26,9 @@ class OrdersTest(TestCase):
self.category = ItemCategory.objects.create(event=self.event, name="Everything", position=0)
self.quota_shirts = Quota.objects.create(event=self.event, name='Shirts', size=2)
self.shirt = Item.objects.create(event=self.event, name='T-Shirt', category=self.category, default_price=12)
prop1 = Property.objects.create(event=self.event, name="Color", item=self.shirt)
self.shirt.properties.add(prop1)
val1 = PropertyValue.objects.create(prop=prop1, value="Red", position=0)
val2 = PropertyValue.objects.create(prop=prop1, value="Black", position=1)
self.quota_shirts.items.add(self.shirt)
self.shirt_red = ItemVariation.objects.create(item=self.shirt, default_price=14)
self.shirt_red.values.add(val1)
var2 = ItemVariation.objects.create(item=self.shirt)
var2.values.add(val2)
self.shirt_red = ItemVariation.objects.create(item=self.shirt, default_price=14, value="Red")
var2 = ItemVariation.objects.create(item=self.shirt, value="Blue")
self.quota_shirts.variations.add(self.shirt_red)
self.quota_shirts.variations.add(var2)
self.quota_tickets = Quota.objects.create(event=self.event, name='Tickets', size=5)