mirror of
https://github.com/pretix/pretix.git
synced 2026-09-13 16:14:40 +00:00
Removed multi-dimensional item variations [backwards-incompatible]
This commit is contained in:
@@ -39,19 +39,12 @@ Items
|
|||||||
.. autoclass:: pretix.base.models.ItemCategory
|
.. autoclass:: pretix.base.models.ItemCategory
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. autoclass:: pretix.base.models.Property
|
.. autoclass:: pretix.base.models.ItemVariation
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: pretix.base.models.PropertyValue
|
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. autoclass:: pretix.base.models.Question
|
.. autoclass:: pretix.base.models.Question
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. autoclass:: pretix.base.models.ItemVariation
|
|
||||||
:members:
|
|
||||||
:exclude-members: add_values_from_string
|
|
||||||
|
|
||||||
.. autoclass:: pretix.base.models.Quota
|
.. autoclass:: pretix.base.models.Quota
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|||||||
+3
-18
@@ -52,24 +52,9 @@ item_shirt = Item.objects.create(
|
|||||||
event=event, category=cat_merch, name='T-Shirt',
|
event=event, category=cat_merch, name='T-Shirt',
|
||||||
default_price=15, tax_rate=19
|
default_price=15, tax_rate=19
|
||||||
)
|
)
|
||||||
size_prop = Property.objects.create(
|
var_s = ItemVariation.objects.create(item=item_shirt, value='S')
|
||||||
event=event, name='Size', item=item_shirt
|
var_m = ItemVariation.objects.create(item=item_shirt, value='M')
|
||||||
)
|
var_l = ItemVariation.objects.create(item=item_shirt, value='L')
|
||||||
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)
|
|
||||||
ticket_quota = Quota.objects.create(
|
ticket_quota = Quota.objects.create(
|
||||||
event=event, name='Ticket quota', size=400,
|
event=event, name='Ticket quota', size=400,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ class JSONExporter(BaseExporter):
|
|||||||
{
|
{
|
||||||
'code': order.code,
|
'code': order.code,
|
||||||
'status': order.status,
|
'status': order.status,
|
||||||
'user': order.user.email,
|
'user': order.email,
|
||||||
'datetime': order.datetime,
|
'datetime': order.datetime,
|
||||||
'payment_fee': order.payment_fee,
|
'payment_fee': order.payment_fee,
|
||||||
'total': order.total,
|
'total': order.total,
|
||||||
@@ -140,8 +140,7 @@ class JSONExporter(BaseExporter):
|
|||||||
} for position in order.positions.all()
|
} for position in order.positions.all()
|
||||||
]
|
]
|
||||||
} for order in
|
} for order in
|
||||||
self.event.orders.all().prefetch_related('positions', 'positions__answers').select_related(
|
self.event.orders.all().prefetch_related('positions', 'positions__answers')
|
||||||
'user')
|
|
||||||
],
|
],
|
||||||
'quotas': [
|
'quotas': [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from django import forms
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.db.models import Model, QuerySet, TextField
|
from django.db.models import Model, QuerySet, TextField
|
||||||
|
from django.forms import BaseModelFormSet
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
@@ -251,3 +252,29 @@ class I18nJSONEncoder(DjangoJSONEncoder):
|
|||||||
return {'type': obj.__class__.__name__, 'id': obj.id}
|
return {'type': obj.__class__.__name__, 'id': obj.id}
|
||||||
else:
|
else:
|
||||||
return super().default(obj)
|
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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
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.contrib.auth.hashers import make_password
|
||||||
from django.db import migrations, models
|
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):
|
def initial_user(apps, schema_editor):
|
||||||
User = apps.get_model("pretixbase", "User")
|
User = apps.get_model("pretixbase", "User")
|
||||||
@@ -54,335 +44,5 @@ class Migration(migrations.Migration):
|
|||||||
'verbose_name_plural': 'Users',
|
'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),
|
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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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 .base import CachedFile, cachedfile_name
|
||||||
from .event import Event, EventLock, EventPermission, EventSetting
|
from .event import Event, EventLock, EventPermission, EventSetting
|
||||||
from .items import (
|
from .items import (
|
||||||
Item, ItemCategory, ItemVariation, Property, PropertyValue, Question,
|
Item, ItemCategory, ItemVariation, Question, Quota, itempicture_upload_to,
|
||||||
Quota, VariationsField, itempicture_upload_to,
|
|
||||||
)
|
)
|
||||||
from .log import LogEntry
|
from .log import LogEntry
|
||||||
from .orders import (
|
from .orders import (
|
||||||
@@ -14,8 +13,7 @@ from .organizer import Organizer, OrganizerPermission, OrganizerSetting
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'User', 'CachedFile', 'Organizer', 'OrganizerPermission', 'Event', 'EventPermission',
|
'User', 'CachedFile', 'Organizer', 'OrganizerPermission', 'Event', 'EventPermission',
|
||||||
'ItemCategory', 'Item', 'Property', 'PropertyValue', 'ItemVariation', 'VariationsField', 'Question',
|
'ItemCategory', 'Item', 'ItemVariation', 'Question', 'Quota', 'Order', 'CachedTicket', 'QuestionAnswer',
|
||||||
'Quota', 'Order', 'CachedTicket', 'QuestionAnswer', 'ObjectWithAnswers', 'OrderPosition',
|
'ObjectWithAnswers', 'OrderPosition', 'CartPosition', 'EventSetting', 'OrganizerSetting', 'EventLock',
|
||||||
'CartPosition', 'EventSetting', 'OrganizerSetting', 'EventLock', 'cachedfile_name', 'itempicture_upload_to',
|
'cachedfile_name', 'itempicture_upload_to', 'generate_secret', 'LogEntry'
|
||||||
'generate_secret', 'LogEntry'
|
|
||||||
]
|
]
|
||||||
|
|||||||
+21
-281
@@ -1,18 +1,16 @@
|
|||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from itertools import product
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q, Case, Count, Sum, When
|
from django.db.models import Q, Case, Count, Sum, When
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
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.i18n import I18nCharField, I18nTextField
|
||||||
from pretix.base.models.base import LoggedModel
|
from pretix.base.models.base import LoggedModel
|
||||||
|
|
||||||
from ..types import VariationDict
|
|
||||||
from .event import Event
|
from .event import Event
|
||||||
|
|
||||||
|
|
||||||
@@ -196,117 +194,6 @@ class Item(LoggedModel):
|
|||||||
return False
|
return False
|
||||||
return True
|
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):
|
def check_quotas(self):
|
||||||
"""
|
"""
|
||||||
This method is used to determine whether this Item is currently available
|
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()`.
|
: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.
|
Please use the method on the ItemVariation object you are interested in.
|
||||||
"""
|
"""
|
||||||
if self.properties.count() > 0: # NOQA
|
if self.variations.count() > 0: # NOQA
|
||||||
raise ValueError('Do not call this directly on items which have properties '
|
raise ValueError('Do not call this directly on items which have variations '
|
||||||
'but call this on their ItemVariation objects')
|
'but call this on their ItemVariation objects')
|
||||||
return min([q.availability() for q in self.quotas.all()],
|
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))
|
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):
|
class ItemVariation(models.Model):
|
||||||
"""
|
"""
|
||||||
A variation is an item combined with values for all properties
|
A variation of a product. For example, if your item is 'T-Shirt'
|
||||||
associated with the item. For example, if your item is 'T-Shirt'
|
then an example for a variation would be 'T-Shirt XL'.
|
||||||
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.
|
|
||||||
|
|
||||||
:param item: The item this variation belongs to
|
:param item: The item this variation belongs to
|
||||||
:type item: Item
|
: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.
|
:param active: Whether this value is to be sold.
|
||||||
:type active: bool
|
:type active: bool
|
||||||
:param default_price: This variation's default price
|
:param default_price: This variation's default price
|
||||||
@@ -447,14 +228,18 @@ class ItemVariation(models.Model):
|
|||||||
Item,
|
Item,
|
||||||
related_name='variations'
|
related_name='variations'
|
||||||
)
|
)
|
||||||
values = models.ManyToManyField(
|
value = I18nCharField(
|
||||||
PropertyValue,
|
max_length=255,
|
||||||
related_name='variations',
|
verbose_name=_('Description')
|
||||||
)
|
)
|
||||||
active = models.BooleanField(
|
active = models.BooleanField(
|
||||||
default=True,
|
default=True,
|
||||||
verbose_name=_("Active"),
|
verbose_name=_("Active"),
|
||||||
)
|
)
|
||||||
|
position = models.PositiveIntegerField(
|
||||||
|
default=0,
|
||||||
|
verbose_name=_("Position")
|
||||||
|
)
|
||||||
default_price = models.DecimalField(
|
default_price = models.DecimalField(
|
||||||
decimal_places=2, max_digits=7,
|
decimal_places=2, max_digits=7,
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
@@ -464,9 +249,10 @@ class ItemVariation(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Product variation")
|
verbose_name = _("Product variation")
|
||||||
verbose_name_plural = _("Product variations")
|
verbose_name_plural = _("Product variations")
|
||||||
|
ordering = ("position", "id")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.to_variation_dict())
|
return str(self.value)
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
super().delete(*args, **kwargs)
|
super().delete(*args, **kwargs)
|
||||||
@@ -488,56 +274,10 @@ class ItemVariation(models.Model):
|
|||||||
return min([q.availability() for q in self.quotas.all()],
|
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))
|
key=lambda s: (s[0], s[1] if s[1] is not None else sys.maxsize))
|
||||||
|
|
||||||
def to_variation_dict(self) -> VariationDict:
|
def __lt__(self, other):
|
||||||
"""
|
if self.position == other.position:
|
||||||
:return: a :py:class:`VariationDict` representing this variation.
|
return self.id < other.id
|
||||||
"""
|
return self.position < other.position
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class Question(LoggedModel):
|
class Question(LoggedModel):
|
||||||
@@ -686,7 +426,7 @@ class Quota(LoggedModel):
|
|||||||
related_name="quotas",
|
related_name="quotas",
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
variations = VariationsField(
|
variations = models.ManyToManyField(
|
||||||
ItemVariation,
|
ItemVariation,
|
||||||
related_name="quotas",
|
related_name="quotas",
|
||||||
blank=True,
|
blank=True,
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ def _add_new_items(event: Event, items: List[Tuple[int, Optional[int], int]],
|
|||||||
variations_query = ItemVariation.objects.filter(
|
variations_query = ItemVariation.objects.filter(
|
||||||
item__event=event,
|
item__event=event,
|
||||||
id__in=[i[1] for i in items if i[1] is not None]
|
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}
|
variations_cache = {v.id: v for v in variations_query}
|
||||||
|
|
||||||
for i in items:
|
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]]]:
|
def order_overview(event: Event) -> Tuple[List[Tuple[ItemCategory, List[Item]]], Dict[str, Tuple[Decimal, Decimal]]]:
|
||||||
items = event.items.all().select_related(
|
items = event.items.all().select_related(
|
||||||
'category', # for re-grouping
|
'category', # for re-grouping
|
||||||
).prefetch_related(
|
|
||||||
'properties', # for .get_all_available_variations()
|
|
||||||
).order_by('category__position', 'category_id', 'name')
|
).order_by('category__position', 'category_id', 'name')
|
||||||
|
|
||||||
num_total = {
|
num_total = {
|
||||||
@@ -73,22 +71,27 @@ def order_overview(event: Event) -> Tuple[List[Tuple[ItemCategory, List[Item]]],
|
|||||||
}
|
}
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
item.all_variations = sorted(item.get_all_variations(),
|
item.all_variations = list(item.variations.all())
|
||||||
key=lambda vd: vd.ordered_values())
|
item.has_variations = (len(item.all_variations) > 0)
|
||||||
for var in item.all_variations:
|
if item.has_variations:
|
||||||
variid = var['variation'].id if 'variation' in var else None
|
for var in item.all_variations:
|
||||||
var.num_total = num_total.get((item.id, variid), (0, 0))
|
variid = var.id
|
||||||
var.num_pending = num_pending.get((item.id, variid), (0, 0))
|
var.num_total = num_total.get((item.id, variid), (0, 0))
|
||||||
var.num_cancelled = num_cancelled.get((item.id, variid), (0, 0))
|
var.num_pending = num_pending.get((item.id, variid), (0, 0))
|
||||||
var.num_refunded = num_refunded.get((item.id, variid), (0, 0))
|
var.num_cancelled = num_cancelled.get((item.id, variid), (0, 0))
|
||||||
var.num_paid = num_paid.get((item.id, variid), (0, 0))
|
var.num_refunded = num_refunded.get((item.id, variid), (0, 0))
|
||||||
item.has_variations = (len(item.all_variations) != 1
|
var.num_paid = num_paid.get((item.id, variid), (0, 0))
|
||||||
or not item.all_variations[0].empty())
|
item.num_total = tuplesum(var.num_total for var in item.all_variations)
|
||||||
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_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_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_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.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'))
|
nonecat = ItemCategory(name=_('Uncategorized'))
|
||||||
# Regroup those by category
|
# 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:
|
for c in items_by_category:
|
||||||
c[0].num_total = tuplesum(item.num_total for item in c[1])
|
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_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_cancelled = tuplesum(item.num_cancelled for item in c[1])
|
||||||
c[0].num_refunded = tuplesum(item.num_refunded 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):
|
class ExtFileField(forms.FileField):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
ext_whitelist = kwargs.pop("ext_whitelist")
|
ext_whitelist = kwargs.pop("ext_whitelist")
|
||||||
@@ -396,108 +125,3 @@ class ExtFileField(forms.FileField):
|
|||||||
if ext not in self.ext_whitelist:
|
if ext not in self.ext_whitelist:
|
||||||
raise forms.ValidationError(_("Filetype not allowed!"))
|
raise forms.ValidationError(_("Filetype not allowed!"))
|
||||||
return data
|
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
|
import copy
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.forms import BooleanField
|
from django.forms import BooleanField, ModelMultipleChoiceField
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.forms import I18nModelForm
|
from pretix.base.forms import I18nModelForm
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Item, ItemCategory, ItemVariation, Property, PropertyValue, Question,
|
Item, ItemCategory, ItemVariation, Question, Quota,
|
||||||
Quota,
|
|
||||||
)
|
)
|
||||||
from pretix.control.forms import TolerantFormsetModelForm, VariationsField
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryForm(I18nModelForm):
|
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):
|
class QuestionForm(I18nModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -75,13 +55,14 @@ class QuotaForm(I18nModelForm):
|
|||||||
active_variations = set()
|
active_variations = set()
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
if len(item.properties.all()) > 0:
|
if len(item.variations.all()) > 0:
|
||||||
self.fields['item_%s' % item.id] = VariationsField(
|
self.fields['item_%s' % item.id] = ModelMultipleChoiceField(
|
||||||
item, label=_("Activate for"),
|
label=_("Activate for"),
|
||||||
required=False,
|
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:
|
else:
|
||||||
self.fields['item_%s' % item.id] = BooleanField(
|
self.fields['item_%s' % item.id] = BooleanField(
|
||||||
label=_("Activate"),
|
label=_("Activate"),
|
||||||
@@ -125,6 +106,7 @@ class ItemVariationForm(I18nModelForm):
|
|||||||
model = ItemVariation
|
model = ItemVariation
|
||||||
localized_fields = '__all__'
|
localized_fields = '__all__'
|
||||||
fields = [
|
fields = [
|
||||||
|
'value',
|
||||||
'active',
|
'active',
|
||||||
'default_price',
|
'default_price',
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
<h1>{% trans "Modify product:" %} {{ object.name }}</h1>
|
<h1>{% trans "Modify product:" %} {{ object.name }}</h1>
|
||||||
<ul class="nav nav-pills">
|
<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" == 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>
|
<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>
|
</ul>
|
||||||
{% else %}
|
{% 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+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
|
||||||
url(r'^items/(?P<item>\d+)/variations$', item.ItemVariations.as_view(),
|
url(r'^items/(?P<item>\d+)/variations$', item.ItemVariations.as_view(),
|
||||||
name='event.item.variations'),
|
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+)/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+)/down$', item.item_move_down, name='event.items.down'),
|
||||||
url(r'^items/(?P<item>\d+)/delete$', item.ItemDelete.as_view(), name='event.items.delete'),
|
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.contrib import messages
|
||||||
from django.core.urlresolvers import resolve, reverse
|
from django.core.urlresolvers import resolve, reverse
|
||||||
from django.db import transaction
|
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.http import Http404, HttpResponseRedirect
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.utils.functional import cached_property
|
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.detail import SingleObjectMixin
|
||||||
from django.views.generic.edit import DeleteView
|
from django.views.generic.edit import DeleteView
|
||||||
|
|
||||||
|
from pretix.base.i18n import I18nFormSet
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Item, ItemCategory, ItemVariation, Property, PropertyValue, Question,
|
Item, ItemCategory, ItemVariation, Question, Quota,
|
||||||
Quota,
|
|
||||||
)
|
|
||||||
from pretix.control.forms import (
|
|
||||||
NestedInnerI18nInlineFormSet, VariationsField, nestedformset_factory,
|
|
||||||
)
|
)
|
||||||
from pretix.control.forms.item import (
|
from pretix.control.forms.item import (
|
||||||
CategoryForm, ItemFormGeneral, ItemVariationForm, PropertyForm,
|
CategoryForm, ItemFormGeneral, ItemVariationForm, QuestionForm, QuotaForm,
|
||||||
PropertyValueForm, QuestionForm, QuotaForm,
|
|
||||||
)
|
)
|
||||||
from pretix.control.permissions import (
|
from pretix.control.permissions import (
|
||||||
EventPermissionRequiredMixin, event_permission_required,
|
EventPermissionRequiredMixin, event_permission_required,
|
||||||
@@ -350,7 +344,7 @@ class QuotaList(ListView):
|
|||||||
class QuotaEditorMixin:
|
class QuotaEditorMixin:
|
||||||
@cached_property
|
@cached_property
|
||||||
def items(self) -> "List[Item]":
|
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):
|
def get_form(self, form_class=QuotaForm):
|
||||||
if not hasattr(self, '_form'):
|
if not hasattr(self, '_form'):
|
||||||
@@ -376,7 +370,7 @@ class QuotaEditorMixin:
|
|||||||
for item in self.items:
|
for item in self.items:
|
||||||
field = form.fields['item_%s' % item.id]
|
field = form.fields['item_%s' % item.id]
|
||||||
data = form.cleaned_data['item_%s' % item.id]
|
data = form.cleaned_data['item_%s' % item.id]
|
||||||
if isinstance(field, VariationsField):
|
if isinstance(field, ModelMultipleChoiceField):
|
||||||
for v in data:
|
for v in data:
|
||||||
selected_variations.append(v)
|
selected_variations.append(v)
|
||||||
if data and item not in items:
|
if data and item not in items:
|
||||||
@@ -548,264 +542,59 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie
|
|||||||
return super().form_valid(form)
|
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):
|
class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
|
||||||
permission = 'can_change_items'
|
permission = 'can_change_items'
|
||||||
|
template_name = 'pretixcontrol/item/variations.html'
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.item = None
|
self.item = None
|
||||||
|
|
||||||
def get_form(self, variation, data=None) -> ItemVariationForm:
|
@cached_property
|
||||||
"""
|
def formset(self):
|
||||||
Return the dict for one given variation. Variations are expected to be
|
formsetclass = inlineformset_factory(
|
||||||
dictionaries in the format of Item.get_all_variations()
|
Item, ItemVariation,
|
||||||
"""
|
form=ItemVariationForm, formset=I18nFormSet,
|
||||||
# Values are all dictionary ite
|
can_order=True, can_delete=True, extra=0
|
||||||
values = [i[1] for i in sorted([it for it in variation.items() if it[0] != 'variation'])]
|
)
|
||||||
if 'variation' in variation:
|
return formsetclass(self.request.POST if self.request.method == "POST" else None,
|
||||||
form = ItemVariationForm(
|
queryset=ItemVariation.objects.filter(item=self.get_object()),
|
||||||
data,
|
event=self.request.event)
|
||||||
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)
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
self.main(request, *args, **kwargs)
|
|
||||||
context = self.get_context_data(object=self.object)
|
|
||||||
valid = True
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
for form in self.forms_flat:
|
if self.formset.is_valid():
|
||||||
if form.is_valid() and form.has_changed():
|
for form in self.formset.deleted_forms:
|
||||||
form.save()
|
if not form.instance.pk:
|
||||||
change_data = {
|
continue
|
||||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
self.get_object().log_action(
|
||||||
}
|
'pretix.event.item.variation.deleted', user=self.request.user, data={
|
||||||
change_data['id'] = form.instance.pk
|
'id': form.instance.pk
|
||||||
self.object.log_action(
|
}
|
||||||
'pretix.event.item.variation.changed', user=self.request.user, data=change_data
|
|
||||||
)
|
)
|
||||||
if hasattr(form.instance, 'creation') and form.instance.creation:
|
form.instance.delete()
|
||||||
# We need this special 'creation' field set to true in get_form
|
form.instance.pk = None
|
||||||
# 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)
|
|
||||||
|
|
||||||
def get_template_names(self) -> "List[str]":
|
for i, form in enumerate(self.formset.ordered_forms + [
|
||||||
if self.dimension == 0:
|
ef for ef in self.formset.extra_forms if (ef not in self.formset.ordered_forms and ef not in
|
||||||
return ['pretixcontrol/item/variations_0d.html']
|
self.formset.deleted_forms)
|
||||||
elif self.dimension == 1:
|
]):
|
||||||
return ['pretixcontrol/item/variations_1d.html']
|
form.instance.position = i
|
||||||
elif self.dimension >= 2:
|
form.instance.item = self.get_object()
|
||||||
return ['pretixcontrol/item/variations_nd.html']
|
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:
|
def get_success_url(self) -> str:
|
||||||
return reverse('control:event.item.variations', kwargs={
|
return reverse('control:event.item.variations', kwargs={
|
||||||
@@ -815,9 +604,9 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
|
|||||||
})
|
})
|
||||||
|
|
||||||
def get_context_data(self, **kwargs) -> dict:
|
def get_context_data(self, **kwargs) -> dict:
|
||||||
|
self.object = self.get_object()
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['forms'] = self.forms
|
context['formset'] = self.formset
|
||||||
context['properties'] = self.properties
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@@ -835,13 +624,13 @@ class ItemDelete(EventPermissionRequiredMixin, DeleteView):
|
|||||||
def is_allowed(self) -> bool:
|
def is_allowed(self) -> bool:
|
||||||
return not self.get_object().positions.exists()
|
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:
|
if not hasattr(self, 'object') or not self.object:
|
||||||
try:
|
try:
|
||||||
self.object = self.request.event.items.get(
|
self.object = self.request.event.items.get(
|
||||||
id=self.kwargs['item']
|
id=self.kwargs['item']
|
||||||
)
|
)
|
||||||
except Property.DoesNotExist:
|
except Item.DoesNotExist:
|
||||||
raise Http404(_("The requested product does not exist."))
|
raise Http404(_("The requested product does not exist."))
|
||||||
return self.object
|
return self.object
|
||||||
|
|
||||||
|
|||||||
@@ -126,8 +126,7 @@ class OrderDetail(OrderView):
|
|||||||
).select_related(
|
).select_related(
|
||||||
'item', 'variation'
|
'item', 'variation'
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'variation__values', 'variation__values__prop', 'item__questions',
|
'item__questions', 'answers'
|
||||||
'answers'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Group items of the same variation
|
# Group items of the same variation
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9 on 2015-12-13 11:44
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('pretixbase', '0001_initial'),
|
('pretixbase', '0002_auto_20151213_1144'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='KnownDomain',
|
name='KnownDomain',
|
||||||
fields=[
|
fields=[
|
||||||
('domainname', models.CharField(primary_key=True, max_length=255, serialize=False)),
|
('domainname', models.CharField(max_length=255, primary_key=True, serialize=False)),
|
||||||
('organizer', models.ForeignKey(to='pretixbase.Organizer', null=True, blank=True, related_name='domains')),
|
('organizer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='domains', to='pretixbase.Organizer')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name_plural': 'Known domains',
|
'verbose_name_plural': 'Known domains',
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
<div class="col-md-2 col-xs-6 availability-box available">
|
<div class="col-md-2 col-xs-6 availability-box available">
|
||||||
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
|
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
|
||||||
max="{{ var.cached_availability.1 }}"
|
max="{{ var.cached_availability.1 }}"
|
||||||
name="variation_{{ item.id }}_{{ var.variation.id }}">
|
name="variation_{{ item.id }}_{{ var.id }}">
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% include "pretixpresale/event/fragment_availability.html" with avail=var.cached_availability.0 %}
|
{% include "pretixpresale/event/fragment_availability.html" with avail=var.cached_availability.0 %}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ class CartMixin:
|
|||||||
).select_related(
|
).select_related(
|
||||||
'item', 'variation'
|
'item', 'variation'
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'variation__values', 'variation__values__prop',
|
|
||||||
'item__questions', 'answers'
|
'item__questions', 'answers'
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -30,7 +29,7 @@ class CartMixin:
|
|||||||
cart_id=self.request.session.session_key, event=self.request.event
|
cart_id=self.request.session.session_key, event=self.request.event
|
||||||
)
|
)
|
||||||
|
|
||||||
prefetch = ['variation__values', 'variation__values__prop']
|
prefetch = []
|
||||||
if answers:
|
if answers:
|
||||||
prefetch.append('item__questions')
|
prefetch.append('item__questions')
|
||||||
prefetch.append('answers')
|
prefetch.append('answers')
|
||||||
|
|||||||
@@ -20,35 +20,32 @@ class EventIndex(EventViewMixin, CartMixin, TemplateView):
|
|||||||
).select_related(
|
).select_related(
|
||||||
'category', # for re-grouping
|
'category', # for re-grouping
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'properties', # for .get_all_available_variations()
|
|
||||||
'quotas', 'variations__quotas', 'quotas__event' # for .availability()
|
'quotas', 'variations__quotas', 'quotas__event' # for .availability()
|
||||||
).annotate(quotac=Count('quotas')).filter(
|
).annotate(quotac=Count('quotas')).filter(
|
||||||
quotac__gt=0
|
quotac__gt=0
|
||||||
).order_by('category__position', 'category_id', 'position', 'name')
|
).order_by('category__position', 'category_id', 'position', 'name')
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
item.available_variations = sorted(item.get_all_available_variations(),
|
item.available_variations = list(item.variations.filter(active=True, quotas__isnull=False).distinct())
|
||||||
key=lambda vd: vd.ordered_values())
|
item.has_variations = item.variations.exists()
|
||||||
item.has_variations = (len(item.available_variations) != 1
|
|
||||||
or not item.available_variations[0].empty())
|
|
||||||
if not item.has_variations:
|
if not item.has_variations:
|
||||||
item.cached_availability = list(item.check_quotas())
|
item.cached_availability = list(item.check_quotas())
|
||||||
item.cached_availability[1] = min((item.cached_availability[1]
|
item.cached_availability[1] = min((item.cached_availability[1]
|
||||||
if item.cached_availability[1] is not None else sys.maxsize),
|
if item.cached_availability[1] is not None else sys.maxsize),
|
||||||
int(self.request.event.settings.max_items_per_order))
|
int(self.request.event.settings.max_items_per_order))
|
||||||
item.price = item.available_variations[0]['price']
|
item.price = item.default_price
|
||||||
else:
|
else:
|
||||||
for var in item.available_variations:
|
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]
|
var.cached_availability[1] = min(var.cached_availability[1]
|
||||||
if var.cached_availability[1] is not None else sys.maxsize,
|
if var.cached_availability[1] is not None else sys.maxsize,
|
||||||
int(self.request.event.settings.max_items_per_order))
|
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:
|
if len(item.available_variations) > 0:
|
||||||
item.min_price = min([v.price for v in item.available_variations])
|
item.min_price = min([v.price for v in item.available_variations])
|
||||||
item.max_price = max([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
|
# Regroup those by category
|
||||||
context['items_by_category'] = sorted(
|
context['items_by_category'] = sorted(
|
||||||
|
|||||||
@@ -3,27 +3,12 @@
|
|||||||
$(function () {
|
$(function () {
|
||||||
"use strict";
|
"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(
|
$("[data-formset]").formset(
|
||||||
{
|
{
|
||||||
animateForms: true,
|
animateForms: true,
|
||||||
reorderMode: 'animate'
|
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) {
|
$(document).on("click", ".variations .variations-select-all", function (e) {
|
||||||
$(this).parent().parent().find("input[type=checkbox]").prop("checked", true).change();
|
$(this).parent().parent().find("input[type=checkbox]").prop("checked", true).change();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|||||||
@@ -7,143 +7,9 @@ from django.utils.timezone import now
|
|||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedFile, CartPosition, Event, Item, ItemCategory, ItemVariation, Order,
|
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.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):
|
class UserTestCase(TestCase):
|
||||||
@@ -184,12 +50,7 @@ class BaseQuotaTestCase(TestCase):
|
|||||||
self.item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
self.item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||||||
admission=True)
|
admission=True)
|
||||||
self.item2 = Item.objects.create(event=self.event, name="T-Shirt", default_price=23)
|
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)
|
self.var1 = ItemVariation.objects.create(item=self.item2, value='S')
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class QuotaTestCase(BaseQuotaTestCase):
|
class QuotaTestCase(BaseQuotaTestCase):
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ import os
|
|||||||
import time
|
import time
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from django.utils.timezone import now
|
|
||||||
from selenium.webdriver.support.select import Select
|
from selenium.webdriver.support.select import Select
|
||||||
from tests.base import BrowserTest
|
from tests.base import BrowserTest
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Event, EventPermission, Item, ItemCategory, Organizer, OrganizerPermission,
|
Event, EventPermission, Item, ItemCategory, ItemVariation, Organizer,
|
||||||
Property, PropertyValue, Question, Quota, User,
|
OrganizerPermission, Question, Quota, User,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -105,62 +104,6 @@ class CategoriesTest(ItemFormTest):
|
|||||||
assert not ItemCategory.objects.filter(id=c.id).exists()
|
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):
|
class QuestionsTest(ItemFormTest):
|
||||||
|
|
||||||
def test_create(self):
|
def test_create(self):
|
||||||
@@ -215,9 +158,8 @@ class QuotaTest(ItemFormTest):
|
|||||||
c = Quota.objects.create(event=self.event1, name="Full house", size=500)
|
c = Quota.objects.create(event=self.event1, name="Full house", size=500)
|
||||||
item1 = Item.objects.create(event=self.event1, name="Standard", default_price=0)
|
item1 = Item.objects.create(event=self.event1, name="Standard", default_price=0)
|
||||||
item2 = Item.objects.create(event=self.event1, name="Business", 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)
|
ItemVariation.objects.create(item=item2, value="Silver")
|
||||||
PropertyValue.objects.create(prop=prop1, value="Silver")
|
ItemVariation.objects.create(item=item2, value="Gold")
|
||||||
PropertyValue.objects.create(prop=prop1, value="Gold")
|
|
||||||
self.driver.get('%s/control/event/%s/%s/quotas/%s/' % (
|
self.driver.get('%s/control/event/%s/%s/quotas/%s/' % (
|
||||||
self.live_server_url, self.orga1.slug, self.event1.slug, c.id
|
self.live_server_url, self.orga1.slug, self.event1.slug, c.id
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ event_urls = [
|
|||||||
"items/add",
|
"items/add",
|
||||||
"items/1/",
|
"items/1/",
|
||||||
"items/1/variations",
|
"items/1/variations",
|
||||||
"items/1/properties",
|
|
||||||
"categories/",
|
"categories/",
|
||||||
"categories/add",
|
"categories/add",
|
||||||
"categories/2/",
|
"categories/2/",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from django.utils.timezone import now
|
|||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Event, EventPermission, Item, ItemVariation, Order, OrderPosition,
|
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')
|
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
||||||
EventPermission.objects.create(user=user, event=event)
|
EventPermission.objects.create(user=user, event=event)
|
||||||
shirt = Item.objects.create(event=event, name='T-Shirt', default_price=12)
|
shirt = Item.objects.create(event=event, name='T-Shirt', default_price=12)
|
||||||
prop1 = Property.objects.create(event=event, name="Color", item=shirt)
|
shirt_red = ItemVariation.objects.create(item=shirt, default_price=14, value="Red")
|
||||||
val1 = PropertyValue.objects.create(prop=prop1, value="Red", position=0)
|
ItemVariation.objects.create(item=shirt, value="Blue")
|
||||||
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)
|
|
||||||
ticket = Item.objects.create(event=event, name='Ticket', default_price=23)
|
ticket = Item.objects.create(event=event, name='Ticket', default_price=23)
|
||||||
o1 = Order.objects.create(
|
o1 = Order.objects.create(
|
||||||
code='FOO', event=event, status=Order.STATUS_PENDING,
|
code='FOO', event=event, status=Order.STATUS_PENDING,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from django.utils.timezone import now
|
|||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CartPosition, Event, Item, ItemCategory, ItemVariation, Organizer,
|
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.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.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)
|
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.quota_shirts.items.add(self.shirt)
|
||||||
self.shirt_red = ItemVariation.objects.create(item=self.shirt, default_price=14)
|
self.shirt_red = ItemVariation.objects.create(item=self.shirt, default_price=14, value='Red')
|
||||||
self.shirt_red.values.add(val1)
|
var2 = ItemVariation.objects.create(item=self.shirt, value='Blue')
|
||||||
var2 = ItemVariation.objects.create(item=self.shirt)
|
|
||||||
var2.values.add(val2)
|
|
||||||
self.quota_shirts.variations.add(self.shirt_red)
|
self.quota_shirts.variations.add(self.shirt_red)
|
||||||
self.quota_shirts.variations.add(var2)
|
self.quota_shirts.variations.add(var2)
|
||||||
self.quota_tickets = Quota.objects.create(event=self.event, name='Tickets', size=5)
|
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 tests.base import BrowserTest
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Event, Item, ItemCategory, ItemVariation, Organizer, Property,
|
Event, Item, ItemCategory, ItemVariation, Organizer, Quota,
|
||||||
PropertyValue, Quota, User,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -99,9 +98,7 @@ class ItemDisplayTest(EventTestMixin, BrowserTest):
|
|||||||
c = ItemCategory.objects.create(event=self.event, name="Entry tickets", position=0)
|
c = ItemCategory.objects.create(event=self.event, name="Entry tickets", position=0)
|
||||||
q = Quota.objects.create(event=self.event, name='Quota', size=2)
|
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)
|
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)
|
ItemVariation.objects.create(item=item, value='Blue')
|
||||||
PropertyValue.objects.create(prop=prop1, value="Red")
|
|
||||||
PropertyValue.objects.create(prop=prop1, value="Black")
|
|
||||||
q.items.add(item)
|
q.items.add(item)
|
||||||
self.driver.get('%s/%s/%s/' % (self.live_server_url, self.orga.slug, self.event.slug))
|
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))
|
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)
|
c = ItemCategory.objects.create(event=self.event, name="Entry tickets", position=0)
|
||||||
q = Quota.objects.create(event=self.event, name='Quota', size=2)
|
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)
|
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)
|
var1 = ItemVariation.objects.create(item=item, value='Red')
|
||||||
val1 = PropertyValue.objects.create(prop=prop1, value="Red")
|
ItemVariation.objects.create(item=item, value='Blue')
|
||||||
PropertyValue.objects.create(prop=prop1, value="Black")
|
|
||||||
q.items.add(item)
|
q.items.add(item)
|
||||||
var1 = ItemVariation.objects.create(item=item)
|
|
||||||
var1.values.add(val1)
|
|
||||||
q.variations.add(var1)
|
q.variations.add(var1)
|
||||||
self._assert_variation_found()
|
self._assert_variation_found()
|
||||||
|
|
||||||
@@ -124,12 +118,9 @@ class ItemDisplayTest(EventTestMixin, BrowserTest):
|
|||||||
c = ItemCategory.objects.create(event=self.event, name="Entry tickets", position=0)
|
c = ItemCategory.objects.create(event=self.event, name="Entry tickets", position=0)
|
||||||
q = Quota.objects.create(event=self.event, name='Quota', size=None)
|
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)
|
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)
|
var1 = ItemVariation.objects.create(item=item, value='Red')
|
||||||
val1 = PropertyValue.objects.create(prop=prop1, value="Red")
|
ItemVariation.objects.create(item=item, value='Blue')
|
||||||
PropertyValue.objects.create(prop=prop1, value="Black")
|
|
||||||
q.items.add(item)
|
q.items.add(item)
|
||||||
var1 = ItemVariation.objects.create(item=item)
|
|
||||||
var1.values.add(val1)
|
|
||||||
q.variations.add(var1)
|
q.variations.add(var1)
|
||||||
self._assert_variation_found()
|
self._assert_variation_found()
|
||||||
|
|
||||||
@@ -149,16 +140,11 @@ class ItemDisplayTest(EventTestMixin, BrowserTest):
|
|||||||
c = ItemCategory.objects.create(event=self.event, name="Entry tickets", position=0)
|
c = ItemCategory.objects.create(event=self.event, name="Entry tickets", position=0)
|
||||||
q = Quota.objects.create(event=self.event, name='Quota', size=2)
|
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)
|
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)
|
var1 = ItemVariation.objects.create(item=item, value='Red', default_price=14, position=1)
|
||||||
val1 = PropertyValue.objects.create(prop=prop1, value="Red", position=0)
|
var2 = ItemVariation.objects.create(item=item, value='Black', position=2)
|
||||||
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)
|
|
||||||
q.variations.add(var1)
|
q.variations.add(var1)
|
||||||
q.variations.add(var2)
|
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.driver.get('%s/%s/%s/' % (self.live_server_url, self.orga.slug, self.event.slug))
|
||||||
self.assertIn("Early-bird",
|
self.assertIn("Early-bird",
|
||||||
self.driver.find_element_by_css_selector("section:nth-of-type(1) div:nth-of-type(1)").text)
|
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 (
|
from pretix.base.models import (
|
||||||
Event, Item, ItemCategory, ItemVariation, Order, OrderPosition, Organizer,
|
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.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.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)
|
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.quota_shirts.items.add(self.shirt)
|
||||||
self.shirt_red = ItemVariation.objects.create(item=self.shirt, default_price=14)
|
self.shirt_red = ItemVariation.objects.create(item=self.shirt, default_price=14, value="Red")
|
||||||
self.shirt_red.values.add(val1)
|
var2 = ItemVariation.objects.create(item=self.shirt, value="Blue")
|
||||||
var2 = ItemVariation.objects.create(item=self.shirt)
|
|
||||||
var2.values.add(val2)
|
|
||||||
self.quota_shirts.variations.add(self.shirt_red)
|
self.quota_shirts.variations.add(self.shirt_red)
|
||||||
self.quota_shirts.variations.add(var2)
|
self.quota_shirts.variations.add(var2)
|
||||||
self.quota_tickets = Quota.objects.create(event=self.event, name='Tickets', size=5)
|
self.quota_tickets = Quota.objects.create(event=self.event, name='Tickets', size=5)
|
||||||
|
|||||||
Reference in New Issue
Block a user