mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
Removed multi-dimensional item variations [backwards-incompatible]
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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': [
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
347
src/pretix/base/migrations/0002_auto_20151213_1144.py
Normal file
347
src/pretix/base/migrations/0002_auto_20151213_1144.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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'
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
))
|
||||
|
||||
@@ -35,7 +35,6 @@ event_urls = [
|
||||
"items/add",
|
||||
"items/1/",
|
||||
"items/1/variations",
|
||||
"items/1/properties",
|
||||
"categories/",
|
||||
"categories/add",
|
||||
"categories/2/",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user