Compare commits

..

4 Commits

Author SHA1 Message Date
Raphael Michel
52f057053e Bump version 2017-04-17 13:36:10 +02:00
Raphael Michel
c3eb909742 Fix locale formatting in PDF exporter 2017-04-17 13:34:02 +02:00
Jan Felix Wiebe
8b88fb0988 Switched checkbox order (#461)
The boxes vor adding and editing vouchers were switched for new users.
2017-04-17 13:33:48 +02:00
Raphael Michel
8884502d19 Update minimal django version 2017-04-17 13:33:20 +02:00
141 changed files with 3664 additions and 9550 deletions

2
.gitignore vendored
View File

@@ -21,6 +21,4 @@ pretixeu/
local/
.project
.pydevproject
.DS_Store

View File

@@ -2,32 +2,11 @@ before_script:
tests:
stage: test
script:
- virtualenv env
- virtualenv-3.4 env
- source env/bin/activate
- pip install -U pip wheel setuptools
- XDG_CACHE_HOME=/cache bash .travis.sh style
- XDG_CACHE_HOME=/cache bash .travis.sh tests
- XDG_CACHE_HOME=/cache bash .travis.sh doctests
tags:
- python3
pypi:
stage: release
script:
- cp /keys/.pypirc ~/.pypirc
- virtualenv env
- source env/bin/activate
- pip install -U pip wheel setuptools
- XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt -r src/requirements/py34.txt
- cd src
- python setup.py sdist upload
- python setup.py bdist_wheel upload
tags:
- python3
only:
- release
artifacts:
paths:
- src/dist/
stages:
- test
- build
- release

View File

@@ -22,11 +22,9 @@ http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
add_header X-Content-Type-Options nosniff;
access_log /var/log/nginx/access.log private;
error_log /var/log/nginx/error.log;
add_header Referrer-Policy same-origin;
gzip on;
gzip_disable "msie6";

View File

@@ -246,11 +246,11 @@ To install a plugin, you need to build your own docker image. To do so, create a
named ``Dockerfile`` in it. The Dockerfile could look like this (replace ``pretix-passbook`` with the plugins of your
choice)::
FROM pretix/standalone:stable
FROM pretix/standalone
USER root
RUN pip3 install pretix-passbook
USER pretixuser
RUN cd /pretix/src && make production
RUN make production
Then, go to that directory and build the image::

View File

@@ -213,9 +213,6 @@ The following snippet is an example on how to configure a nginx proxy for pretix
ssl_certificate /path/to/cert.chain.pem;
ssl_certificate_key /path/to/key.pem;
add_header Referrer-Options same-origin;
add_header X-Content-Type-Options nosniff;
location / {
proxy_pass http://localhost:8345/;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@@ -11,7 +11,7 @@ Core
----
.. automodule:: pretix.base.signals
:members: periodic_task, event_live_issues, event_copy_data
:members: periodic_task, event_live_issues
Order events
""""""""""""
@@ -47,7 +47,7 @@ Backend
-------
.. automodule:: pretix.control.signals
:members: nav_event, html_head, quota_detail_html, nav_topbar, nav_global, nav_organizer
:members: nav_event, html_head, quota_detail_html, nav_topbar, organizer_edit_tabs
.. automodule:: pretix.base.signals

View File

@@ -18,9 +18,8 @@ If you improved pretix in any way, we'd be very happy if you contribute it
back to the main code base! The easiest way to do so is to `create a pull request`_
on our `GitHub repository`_.
We recommend that you create a feature branch for every issue you work on so the changes can
be reviewed individually.
Please use the test suite to check whether your changes break any existing features and run
Before you do so, please `squash all your changes`_ into one single commit. Please
use the test suite to check whether your changes break any existing features and run
the code style checks to confirm you are consistent with pretix's coding style. You'll
find instructions on this in the :ref:`checksandtests` section of the development setup guide.
@@ -35,3 +34,4 @@ Again: If you get stuck, do not hesitate to contact any of us, or Raphael person
.. _create a pull request: https://help.github.com/articles/creating-a-pull-request/
.. _GitHub repository: https://github.com/pretix/pretix
.. _squash all your changes: https://davidwalsh.name/squash-commits-git

View File

@@ -2,10 +2,7 @@ Settings storage
================
pretix is highly configurable and therefore needs to store a lot of per-event and per-organizer settings.
For this purpose, we use `django-hierarkey`_ which started out as part of pretix and then got refactored into
its own library. It has a comprehensive `documentation`_ which you should read if you work with settings in pretix.
The settings are stored in the database and accessed through a ``HierarkeyProxy`` instance. You can obtain
Those settings are stored in the database and accessed through a ``SettingsProxy`` instance. You can obtain
such an instance from any event or organizer model instance by just accessing ``event.settings`` or
``organizer.settings``, respectively.
@@ -20,10 +17,12 @@ includes serializers for serializing the following types:
In code, we recommend to always use the ``.get()`` method on the settings object to access a value, but for
convenience in templates you can also access settings values at ``settings[name]`` and ``settings.name``.
See the hierarkey `documentation`_ for more information.
.. autoclass:: pretix.base.settings.SettingsProxy
:members: get, set, delete, freeze
To avoid naming conflicts, plugins are requested to prefix all settings they use with the name of the plugin
or something unique, e.g. ``payment_paypal_api_key``. To reduce redundant typing of this prefix, we provide
or something unique, e.g. ``payment.paypal.api_key``. To reduce redundant typing of this prefix, we provide
another helper class:
.. autoclass:: pretix.base.settings.SettingsSandbox
@@ -34,10 +33,10 @@ you will just be passed a sandbox object with a prefix generated from your provi
Forms
-----
Hierarkey also provides a base class for forms that allow the modification of settings. pretix contains a
subclass that also adds suport for internationalized fields:
We also provide a base class for forms that allow the modification of settings:
.. autoclass:: pretix.base.forms.SettingsForm
:members: save
You can simply use it like this::
@@ -52,17 +51,3 @@ You can simply use it like this::
help_text=_("The number of days after placing an order the user has to pay to "
"preserve his reservation."),
)
Defaults in plugins
-------------------
Plugins can add custom hardcoded defaults in the following way::
from pretix.base.settings import settings_hierarkey
settings_hierarkey.add_default('key', 'value', type)
Make sure that you include this code in a module that is imported at app loading time.
.. _django-hierarkey: https://github.com/raphaelm/django-hierarkey
.. _documentation: https://django-hierarkey.readthedocs.io/en/latest/

View File

@@ -83,10 +83,6 @@ As we did not implement an overall front page yet, you need to go directly to
http://localhost:8000/control/ for the admin view or, if you imported the test
data as suggested above, to the event page at http://localhost:8000/bigevents/2017/
.. note:: If you want the development server to listen on a different interface or
port (for example because you develop on `pretixdroid`_), you can check
`Django's documentation`_ for more options.
.. _`checksandtests`:
Code checks and unit tests
@@ -152,7 +148,3 @@ To build the documentation, run the following command from the ``doc/`` director
make html
You will now find the generated documentation in the ``doc/_build/html/`` subdirectory.
.. _Django's documentation: https://docs.djangoproject.com/en/1.11/ref/django-admin/#runserver
.. _pretixdroid: https://github.com/pretix/pretixdroid

View File

@@ -1 +1 @@
__version__ = "1.3.1"
__version__ = "1.2.1"

View File

@@ -68,9 +68,7 @@ class JSONExporter(BaseExporter):
'variation': position.variation_id,
'price': position.price,
'attendee_name': position.attendee_name,
'attendee_email': position.attendee_email,
'secret': position.secret,
'addon_to': position.addon_to_id,
'answers': [
{
'question': answer.question_id,

View File

@@ -4,8 +4,6 @@ from django import forms
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import OrderPosition
from ..exporter import BaseExporter
from ..models import Order
from ..signals import register_data_exporters
@@ -18,11 +16,7 @@ class MailExporter(BaseExporter):
def render(self, form_data: dict):
qs = self.event.orders.filter(status__in=form_data['status'])
addrs = qs.values('email')
pos = OrderPosition.objects.filter(
order__event=self.event, order__status__in=form_data['status']
).values('attendee_email')
data = "\r\n".join(set(a['email'] for a in addrs)
| set(a['attendee_email'] for a in pos if a['attendee_email']))
data = "\r\n".join(set(a['email'] for a in addrs))
return 'pretixemails.txt', 'text/plain', data.encode("utf-8")
@property

View File

@@ -59,7 +59,7 @@ class OrderListExporter(BaseExporter):
headers = [
_('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'),
_('Company'), _('Name'), _('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
_('Payment date'), _('Payment type'), _('Payment method fee'),
_('Payment date'), _('Payment type'), _('Payment method fee'), _('Invoice numbers')
]
for tr in tax_rates:
@@ -69,8 +69,6 @@ class OrderListExporter(BaseExporter):
_('Tax value at {rate} % tax').format(rate=tr),
]
headers.append(_('Invoice numbers'))
writer.writerow(headers)
provider_names = {}

View File

@@ -1,15 +1,17 @@
import logging
import i18nfield.forms
from django import forms
from django.core.files import File
from django.core.files.storage import default_storage
from django.core.files.uploadedfile import UploadedFile
from django.forms.models import ModelFormMetaclass
from django.utils import six
from django.utils.crypto import get_random_string
from hierarkey.forms import HierarkeyForm
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import Event
from .validators import PlaceholderValidator # NOQA
logger = logging.getLogger('pretix.plugins.ticketoutputpdf')
@@ -47,22 +49,67 @@ class I18nInlineFormSet(i18nfield.forms.I18nInlineFormSet):
super().__init__(*args, **kwargs)
class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
class SettingsForm(i18nfield.forms.I18nForm):
"""
This form is meant to be used for modifying EventSettings or OrganizerSettings. It takes
care of loading the current values of the fields and saving the field inputs to the
settings storage. It also deals with setting the available languages for internationalized
fields.
:param obj: The event or organizer object which should be used for the settings storage
"""
BOOL_CHOICES = (
('False', _('disabled')),
('True', _('enabled')),
)
def __init__(self, *args, **kwargs):
self.obj = kwargs.get('obj', None)
self.locales = self.obj.settings.get('locales') if self.obj else kwargs.pop('locales', None)
kwargs['attribute_name'] = 'settings'
kwargs['locales'] = self.locales
self.obj = kwargs.pop('obj', None)
self.locales = kwargs.pop('locales', None)
kwargs['locales'] = self.obj.settings.get('locales') if self.obj else self.locales
kwargs['initial'] = self.obj.settings.freeze()
super().__init__(*args, **kwargs)
def get_new_filename(self, name: str) -> str:
nonce = get_random_string(length=8)
if isinstance(self.obj, Event):
fname = '%s/%s/%s.%s.%s' % (
self.obj.organizer.slug, self.obj.slug, name, nonce, name.split('.')[-1]
)
else:
fname = '%s/%s.%s.%s' % (self.obj.slug, name, nonce, name.split('.')[-1])
return fname
def save(self):
"""
Performs the save operation
"""
for name, field in self.fields.items():
value = self.cleaned_data[name]
if isinstance(value, UploadedFile):
# Delete old file
fname = self.obj.settings.get(name, as_type=File)
if fname:
try:
default_storage.delete(fname.name)
except OSError:
logger.error('Deleting file %s failed.' % fname.name)
# Create new file
nonce = get_random_string(length=8)
if isinstance(self.obj, Event):
fname = '%s/%s/%s.%s.%s' % (
self.obj.organizer.slug, self.obj.slug, name, nonce, value.name.split('.')[-1]
)
else:
fname = '%s/%s.%s.%s' % (self.obj.slug, name, nonce, value.name.split('.')[-1])
newname = default_storage.save(fname, value)
value._name = newname
self.obj.settings.set(name, value)
elif isinstance(value, File):
# file is unchanged
continue
elif isinstance(field, forms.FileField):
# file is deleted
fname = self.obj.settings.get(name, as_type=File)
if fname:
try:
default_storage.delete(fname.name)
except OSError:
logger.error('Deleting file %s failed.' % fname.name)
del self.obj.settings[name]
elif value is None:
del self.obj.settings[name]
elif self.obj.settings.get(name, as_type=type(value)) != value:
self.obj.settings.set(name, value)

View File

@@ -1,38 +0,0 @@
import re
from django.core.exceptions import ValidationError
from django.core.validators import BaseValidator
from django.utils.translation import ugettext_lazy as _
from i18nfield.strings import LazyI18nString
class PlaceholderValidator(BaseValidator):
"""
Takes list of allowed placeholders,
validates form field by checking for placeholders,
which are not presented in taken list.
"""
def __init__(self, limit_value):
super().__init__(limit_value)
self.limit_value = limit_value
def __call__(self, value):
if isinstance(value, LazyI18nString):
for l, v in value.data.items():
self.__call__(v)
return
data_placeholders = list(re.findall(r'({[\w\s]*})', value, re.X))
invalid_placeholders = []
for placeholder in data_placeholders:
if placeholder not in self.limit_value:
invalid_placeholders.append(placeholder)
if invalid_placeholders:
raise ValidationError(
_('Invalid placeholder(s): %(value)s'),
code='invalid',
params={'value': ", ".join(invalid_placeholders,)})
def clean(self, x):
return x

View File

@@ -12,12 +12,7 @@ class Command(BaseCommand):
call_command('compilejsi18n', verbosity=1, interactive=False)
call_command('collectstatic', verbosity=1, interactive=False)
call_command('compress', verbosity=1, interactive=False)
try:
gs = GlobalSettingsObject()
del gs.settings.update_check_last
del gs.settings.update_check_result
del gs.settings.update_check_result_warning
except:
# Fails when this is executed without a valid database configuration.
# We don't care.
pass
gs = GlobalSettingsObject()
del gs.settings.update_check_last
del gs.settings.update_check_result
del gs.settings.update_check_result_warning

File diff suppressed because one or more lines are too long

View File

@@ -1,59 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-04-09 16:51
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
def migrate_global_settings(apps, schema_editor):
GlobalSetting = apps.get_model('pretixbase', 'GlobalSetting')
GlobalSettingsObject_SettingsStore = apps.get_model('pretixbase', 'GlobalSettingsObject_SettingsStore')
l = []
for s in GlobalSetting.objects.all():
l.append(GlobalSettingsObject_SettingsStore(key=s.key, value=s.value))
GlobalSettingsObject_SettingsStore.objects.bulk_create(l)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0052_auto_20170324_1506'),
]
operations = [
migrations.RenameModel(
old_name='EventSetting',
new_name='Event_SettingsStore',
),
migrations.RenameModel(
old_name='OrganizerSetting',
new_name='Organizer_SettingsStore',
),
migrations.CreateModel(
name='GlobalSettingsObject_SettingsStore',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(db_index=True, max_length=255)),
('value', models.TextField()),
],
),
migrations.RunPython(
migrate_global_settings, migrations.RunPython.noop
),
migrations.DeleteModel(
name='GlobalSetting',
),
migrations.AlterField(
model_name='event_settingsstore',
name='object',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='_settings_objects', to='pretixbase.Event'),
),
migrations.AlterField(
model_name='organizer_settingsstore',
name='object',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='_settings_objects', to='pretixbase.Organizer'),
),
]

File diff suppressed because one or more lines are too long

View File

@@ -1,40 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-04-13 15:37
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0054_auto_20170413_1050'),
]
operations = [
migrations.AddField(
model_name='cartposition',
name='attendee_email',
field=models.EmailField(blank=True, help_text='Empty, if this product is not an admission ticket', max_length=254, null=True, verbose_name='Attendee email'),
),
migrations.AddField(
model_name='orderposition',
name='attendee_email',
field=models.EmailField(blank=True, help_text='Empty, if this product is not an admission ticket', max_length=254, null=True, verbose_name='Attendee email'),
),
migrations.AlterField(
model_name='event_settingsstore',
name='key',
field=models.CharField(max_length=255),
),
migrations.AlterField(
model_name='globalsettingsobject_settingsstore',
name='key',
field=models.CharField(max_length=255),
),
migrations.AlterField(
model_name='organizer_settingsstore',
name='key',
field=models.CharField(max_length=255),
),
]

View File

@@ -1,58 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-04-14 10:44
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0055_auto_20170413_1537'),
]
operations = [
migrations.CreateModel(
name='ItemAddOn',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('min_count', models.PositiveIntegerField(default=0, verbose_name='Minimum number')),
('max_count', models.PositiveIntegerField(default=1, verbose_name='Maximum number')),
],
),
migrations.AddField(
model_name='cartposition',
name='addon_to',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='addons', to='pretixbase.CartPosition'),
),
migrations.AddField(
model_name='itemcategory',
name='is_addon',
field=models.BooleanField(default=False, help_text='If selected, the products belonging to this category are not for sale on their own. They can only be bought in combination with a product that has this category configured as a possible source for add-ons.', verbose_name='Products in this category are add-on products'),
),
migrations.AddField(
model_name='orderposition',
name='addon_to',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='addons', to='pretixbase.OrderPosition'),
),
migrations.AlterField(
model_name='item',
name='free_price',
field=models.BooleanField(default=False, help_text='If this option is active, your users can choose the price themselves. The price configured above is then interpreted as the minimum price a user has to enter. You could use this e.g. to collect additional donations for your event. This is currently not supported for products that are bought as an add-on to other products.', verbose_name='Free price input'),
),
migrations.AddField(
model_name='itemaddon',
name='addon_category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addon_to', to='pretixbase.ItemCategory', verbose_name='Category'),
),
migrations.AddField(
model_name='itemaddon',
name='base_item',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addons', to='pretixbase.Item'),
),
migrations.AlterUniqueTogether(
name='itemaddon',
unique_together=set([('base_item', 'addon_category')]),
),
]

View File

@@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-05-01 21:16
from __future__ import unicode_literals
import i18nfield.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0056_auto_20170414_1044'),
]
operations = [
migrations.AlterModelOptions(
name='itemaddon',
options={'ordering': ('position', 'pk')},
),
migrations.AddField(
model_name='itemaddon',
name='position',
field=models.PositiveIntegerField(default=0, verbose_name='Position'),
),
migrations.AddField(
model_name='itemvariation',
name='description',
field=i18nfield.fields.I18nTextField(blank=True, help_text='This is shown below the variation name in lists.', null=True, verbose_name='Description'),
),
]

View File

@@ -1,15 +1,14 @@
from ..settings import GlobalSettingsObject_SettingsStore
from .auth import U2FDevice, User
from .base import CachedFile, LoggedModel, cachedfile_name
from .checkin import Checkin
from .event import (
Event, Event_SettingsStore, EventLock, EventPermission, RequiredAction,
Event, EventLock, EventPermission, EventSetting, RequiredAction,
generate_invite_token,
)
from .invoices import Invoice, InvoiceLine, invoice_filename
from .items import (
Item, ItemAddOn, ItemCategory, ItemVariation, Question, QuestionOption,
Quota, itempicture_upload_to,
Item, ItemCategory, ItemVariation, Question, QuestionOption, Quota,
itempicture_upload_to,
)
from .log import LogEntry
from .orders import (
@@ -18,6 +17,6 @@ from .orders import (
cachedcombinedticket_name, cachedticket_name, generate_position_secret,
generate_secret,
)
from .organizer import Organizer, Organizer_SettingsStore, OrganizerPermission
from .organizer import Organizer, OrganizerPermission, OrganizerSetting
from .vouchers import Voucher
from .waitinglist import WaitingListEntry

View File

@@ -11,21 +11,22 @@ from django.core.validators import RegexValidator
from django.db import models
from django.template.defaultfilters import date as _date
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext_lazy as _
from i18nfield.fields import I18nCharField
from pretix.base.email import CustomSMTPBackend
from pretix.base.models.base import LoggedModel
from pretix.base.settings import SettingsProxy
from pretix.base.validators import EventSlugBlacklistValidator
from pretix.helpers.daterange import daterange
from ..settings import settings_hierarkey
from .auth import User
from .organizer import Organizer
from .settings import EventSetting
@settings_hierarkey.add(parent_field='organizer', cache_namespace='event')
class Event(LoggedModel):
"""
This model represents an event. An event is anything you can buy
@@ -58,7 +59,6 @@ class Event(LoggedModel):
"""
settings_namespace = 'event'
CURRENCY_CHOICES = [(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES]
organizer = models.ForeignKey(Organizer, related_name="events", on_delete=models.PROTECT)
name = I18nCharField(
max_length=200,
@@ -83,7 +83,6 @@ class Event(LoggedModel):
related_name="events", )
currency = models.CharField(max_length=10,
verbose_name=_("Default currency"),
choices=CURRENCY_CHOICES,
default=settings.DEFAULT_CURRENCY)
date_from = models.DateTimeField(verbose_name=_("Event start time"))
date_to = models.DateTimeField(null=True, blank=True,
@@ -182,6 +181,17 @@ class Event(LoggedModel):
return ObjectRelatedCache(self)
@cached_property
def settings(self) -> SettingsProxy:
"""
Returns an object representing this event's settings.
"""
try:
return SettingsProxy(self, type=EventSetting, parent=self.organizer)
except Organizer.DoesNotExist:
# Should only happen when creating new events
return SettingsProxy(self, type=EventSetting)
@property
def presale_has_ended(self):
if self.presale_end and now() > self.presale_end:
@@ -225,9 +235,7 @@ class Event(LoggedModel):
), tz)
def copy_data_from(self, other):
from . import ItemAddOn, ItemCategory, Item, Question, Quota
from ..signals import event_copy_data
from . import ItemCategory, Item, Question, Quota
self.plugins = other.plugins
self.save()
@@ -256,12 +264,6 @@ class Event(LoggedModel):
v.item = i
v.save()
for ia in ItemAddOn.objects.filter(base_item__event=other).prefetch_related('base_item', 'addon_category'):
ia.pk = None
ia.base_item = item_map[ia.base_item.pk]
ia.addon_category = category_map[ia.addon_category.pk]
ia.save()
for q in Quota.objects.filter(event=other).prefetch_related('items', 'variations'):
items = list(q.items.all())
vars = list(q.variations.all())
@@ -269,8 +271,7 @@ class Event(LoggedModel):
q.event = self
q.save()
for i in items:
if i.pk in item_map:
q.items.add(item_map[i.pk])
q.items.add(item_map[i.pk])
for v in vars:
q.variations.add(variation_map[v.pk])
@@ -287,7 +288,7 @@ class Event(LoggedModel):
o.question = q
o.save()
for s in other.settings._objects.all():
for s in EventSetting.objects.filter(object=other):
s.object = self
s.pk = None
if s.value.startswith('file://'):
@@ -300,8 +301,6 @@ class Event(LoggedModel):
s.value = 'file://' + newname
s.save()
event_copy_data.send(sender=self, other=other)
def generate_invite_token():
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)

View File

@@ -5,7 +5,6 @@ from decimal import Decimal
from typing import Tuple
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import F, Func, Q, Sum
from django.utils.functional import cached_property
@@ -45,13 +44,6 @@ class ItemCategory(LoggedModel):
position = models.IntegerField(
default=0
)
is_addon = models.BooleanField(
default=False,
verbose_name=_('Products in this category are add-on products'),
help_text=_('If selected, the products belonging to this category are not for sale on their own. They can '
'only be bought in combination with a product that has this category configured as a possible '
'source for add-ons.')
)
class Meta:
verbose_name = _("Product category")
@@ -59,8 +51,6 @@ class ItemCategory(LoggedModel):
ordering = ('position', 'id')
def __str__(self):
if self.is_addon:
return _('{category} (Add-On products)').format(category=str(self.name))
return str(self.name)
def delete(self, *args, **kwargs):
@@ -123,8 +113,6 @@ class Item(LoggedModel):
:type allow_cancel: bool
:param max_per_order: Maximum number of times this item can be in an order. None for unlimited.
:type max_per_order: int
:param min_per_order: Minimum number of times this item needs to be in an order if bought at all. None for unlimited.
:type min_per_order: int
"""
event = models.ForeignKey(
@@ -165,8 +153,7 @@ class Item(LoggedModel):
verbose_name=_("Free price input"),
help_text=_("If this option is active, your users can choose the price themselves. The price configured above "
"is then interpreted as the minimum price a user has to enter. You could use this e.g. to collect "
"additional donations for your event. This is currently not supported for products that are "
"bought as an add-on to other products.")
"additional donations for your event.")
)
tax_rate = models.DecimalField(
verbose_name=_("Taxes included in percent"),
@@ -218,12 +205,6 @@ class Item(LoggedModel):
'canceled by the user until the order is paid for. Users cannot cancel paid orders on their own '
'and you can cancel orders at all times, regardless of this setting')
)
min_per_order = models.IntegerField(
verbose_name=_('Minimum amount per order'),
null=True, blank=True,
help_text=_('This product can only be bought if it is added to the cart at least this many times. If you keep '
'the field empty or set it to 0, there is no special limit for this product.')
)
max_per_order = models.IntegerField(
verbose_name=_('Maximum amount per order'),
null=True, blank=True,
@@ -231,8 +212,6 @@ class Item(LoggedModel):
'empty or set it to 0, there is no special limit for this product. The limit for the maximum '
'number of items in the whole order applies regardless.')
)
# !!! Attention: If you add new fields here, also add them to the copying code in
# pretix/control/views/item.py if applicable.
class Meta:
verbose_name = _("Product")
@@ -310,8 +289,6 @@ class ItemVariation(models.Model):
:type item: Item
:param value: A string defining this variation
:type value: str
:param description: A short description
:type description: str
:param active: Whether this variation is being sold.
:type active: bool
:param default_price: This variation's default price
@@ -329,11 +306,6 @@ class ItemVariation(models.Model):
default=True,
verbose_name=_("Active"),
)
description = I18nTextField(
verbose_name=_("Description"),
help_text=_("This is shown below the variation name in lists."),
null=True, blank=True,
)
position = models.PositiveIntegerField(
default=0,
verbose_name=_("Position")
@@ -397,43 +369,6 @@ class ItemVariation(models.Model):
return self.position < other.position
class ItemAddOn(models.Model):
"""
An instance of this model indicates that buying a ticket of the time ``base_item``
allows you to add up to ``max_count`` items from the category ``addon_category``
to your order that will be associated with the base item.
"""
base_item = models.ForeignKey(
Item,
related_name='addons'
)
addon_category = models.ForeignKey(
ItemCategory,
related_name='addon_to',
verbose_name=_('Category')
)
min_count = models.PositiveIntegerField(
default=0,
verbose_name=_('Minimum number')
)
max_count = models.PositiveIntegerField(
default=1,
verbose_name=_('Maximum number')
)
position = models.PositiveIntegerField(
default=0,
verbose_name=_("Position")
)
class Meta:
unique_together = (('base_item', 'addon_category'),)
ordering = ('position', 'pk')
def clean(self):
if self.max_count < self.min_count:
raise ValidationError(_('The minimum number needs to be lower than the maximum number.'))
class Question(LoggedModel):
"""
A question is an input field that can be used to extend a ticket by custom information,
@@ -688,7 +623,6 @@ class Quota(LoggedModel):
func = 'GREATEST'
return Voucher.objects.filter(
Q(event=self.event) &
Q(block_quota=True) &
Q(Q(valid_until__isnull=True) | Q(valid_until__gte=now_dt)) &
Q(Q(self._position_lookup) | Q(quota=self))
@@ -708,7 +642,6 @@ class Quota(LoggedModel):
now_dt = now_dt or now()
return CartPosition.objects.filter(
Q(event=self.event) &
Q(expires__gte=now_dt) &
~Q(
Q(voucher__isnull=False) & Q(voucher__block_quota=True)
@@ -722,14 +655,14 @@ class Quota(LoggedModel):
# This query has beeen benchmarked against a Count('id', distinct=True) aggregate and won by a small margin.
return OrderPosition.objects.filter(
self._position_lookup, order__status=Order.STATUS_PENDING, order__event=self.event
self._position_lookup, order__status=Order.STATUS_PENDING,
).values('id').distinct().count()
def count_paid_orders(self):
from pretix.base.models import Order, OrderPosition
return OrderPosition.objects.filter(
self._position_lookup, order__status=Order.STATUS_PAID, order__event=self.event
self._position_lookup, order__status=Order.STATUS_PAID
).values('id').distinct().count()
@cached_property

View File

@@ -394,8 +394,6 @@ class AbstractPosition(models.Model):
:type price: decimal.Decimal
:param attendee_name: The attendee's name, if entered.
:type attendee_name: str
:param attendee_email: The attendee's email, if entered.
:type attendee_email: str
:param voucher: A voucher that has been applied to this sale
:type voucher: Voucher
"""
@@ -420,17 +418,9 @@ class AbstractPosition(models.Model):
blank=True, null=True,
help_text=_("Empty, if this product is not an admission ticket")
)
attendee_email = models.EmailField(
verbose_name=_("Attendee email"),
blank=True, null=True,
help_text=_("Empty, if this product is not an admission ticket")
)
voucher = models.ForeignKey(
'Voucher', null=True, blank=True
)
addon_to = models.ForeignKey(
'self', null=True, blank=True, on_delete=models.CASCADE, related_name='addons'
)
class Meta:
abstract = True
@@ -496,19 +486,13 @@ class OrderPosition(AbstractPosition):
from . import Voucher
ops = []
cp_mapping = {}
# The sorting key ensures that all addons come directly after the position they refer to
for i, cartpos in enumerate(sorted(cp, key=lambda c: (c.addon_to_id or c.pk, c.addon_to_id or 0))):
for i, cartpos in enumerate(cp):
op = OrderPosition(order=order)
for f in AbstractPosition._meta.fields:
if f.name == 'addon_to':
setattr(op, f.name, cp_mapping.get(cartpos.addon_to_id))
else:
setattr(op, f.name, getattr(cartpos, f.name))
setattr(op, f.name, getattr(cartpos, f.name))
op._calculate_tax()
op.positionid = i + 1
op.save()
cp_mapping[cartpos.pk] = op
for answ in cartpos.answers.all():
answ.orderposition = op
answ.cartposition = None

View File

@@ -3,16 +3,17 @@ import string
from django.core.validators import RegexValidator
from django.db import models
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from pretix.base.models.base import LoggedModel
from pretix.base.settings import SettingsProxy
from pretix.base.validators import OrganizerSlugBlacklistValidator
from ..settings import settings_hierarkey
from .auth import User
from .settings import OrganizerSetting
@settings_hierarkey.add(cache_namespace='organizer')
class Organizer(LoggedModel):
"""
This model represents an entity organizing events, e.g. a company, institution,
@@ -58,6 +59,14 @@ class Organizer(LoggedModel):
self.get_cache().clear()
return obj
@cached_property
def settings(self) -> SettingsProxy:
"""
Returns an object representing this organizer's settings
"""
from pretix.base.settings import GlobalSettingsObject
return SettingsProxy(self, type=OrganizerSetting, parent=GlobalSettingsObject())
def get_cache(self) -> "pretix.base.cache.ObjectRelatedCache":
"""
Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to

View File

@@ -0,0 +1,34 @@
from django.db import models
class GlobalSetting(models.Model):
"""
A global setting is a key-value setting which can be set for a
pretix instance. It will be inherited by all events and organizers.
It is filled via the register_global_settings signal.
"""
key = models.CharField(max_length=255, primary_key=True)
value = models.TextField()
def __init__(self, *args, object=None, **kwargs):
super().__init__(*args, **kwargs)
class OrganizerSetting(models.Model):
"""
An organizer setting is a key-value setting which can be set for an
organizer. It will be inherited by the events of this organizer
"""
object = models.ForeignKey('Organizer', related_name='setting_objects', on_delete=models.CASCADE)
key = models.CharField(max_length=255)
value = models.TextField()
class EventSetting(models.Model):
"""
An event setting is a key-value setting which can be set for a
specific event
"""
object = models.ForeignKey('Event', related_name='setting_objects', on_delete=models.CASCADE)
key = models.CharField(max_length=255)
value = models.TextField()

View File

@@ -1,4 +1,4 @@
from collections import Counter, defaultdict, namedtuple
from collections import Counter, namedtuple
from datetime import timedelta
from decimal import Decimal
from typing import List, Optional
@@ -27,7 +27,6 @@ error_messages = {
'busy': _('We were not able to process your request completely as the '
'server was too busy. Please try again.'),
'empty': _('You did not select any products.'),
'unknown_position': _('Unknown cart position.'),
'not_for_sale': _('You selected a product which is not available for sale.'),
'unavailable': _('Some of the products you selected are no longer available. '
'Please see below for details.'),
@@ -35,9 +34,6 @@ error_messages = {
'the quantity you selected. Please see below for details.'),
'max_items': _("You cannot select more than %s items per order."),
'max_items_per_product': _("You cannot select more than %(max)s items of the product %(product)s."),
'min_items_per_product': _("You need to select at least %(min)s items of the product %(product)s."),
'min_items_per_product_removed': _("We removed %(product)s from your cart as you can not buy less than "
"%(min)s items of it."),
'not_started': _('The presale period for this event has not yet started.'),
'ended': _('The presale period has ended.'),
'price_too_high': _('The entered price is to high.'),
@@ -49,18 +45,11 @@ error_messages = {
'voucher_expired': _('This voucher is expired.'),
'voucher_invalid_item': _('This voucher is not valid for this product.'),
'voucher_required': _('You need a valid voucher code to order this product.'),
'addon_invalid_base': _('You can not select an add-on for the selected product.'),
'addon_duplicate_item': _('You can not select two variations of the same add-on product.'),
'addon_max_count': _('You can select at most %(max)s add-ons from the category %(cat)s for the product %(base)s.'),
'addon_min_count': _('You need to select at least %(min)s add-ons from the category %(cat)s for the '
'product %(base)s.'),
'addon_only': _('One of the products you selected can only be bought as an add-on to another project.'),
}
class CartManager:
AddOperation = namedtuple('AddOperation', ('count', 'item', 'variation', 'price', 'voucher', 'quotas',
'addon_to'))
AddOperation = namedtuple('AddOperation', ('count', 'item', 'variation', 'price', 'voucher', 'quotas'))
RemoveOperation = namedtuple('RemoveOperation', ('position',))
ExtendOperation = namedtuple('ExtendOperation', ('position', 'count', 'item', 'variation', 'price', 'voucher',
'quotas'))
@@ -85,7 +74,7 @@ class CartManager:
def positions(self):
return CartPosition.objects.filter(
Q(cart_id=self.cart_id) & Q(event=self.event)
).select_related('item')
)
def _calculate_expiry(self):
self._expiry = self.now_dt + timedelta(minutes=self.event.settings.get('reservation_time', as_type=int))
@@ -108,15 +97,9 @@ class CartManager:
def _update_items_cache(self, item_ids: List[int], variation_ids: List[int]):
self._items_cache.update(
{
i.pk: i
for i
in self.event.items.select_related('category').prefetch_related(
'addons', 'addons__addon_category', 'quotas'
).filter(
id__in=[i for i in item_ids if i and i not in self._items_cache]
)
}
{i.pk: i for i in self.event.items.prefetch_related('quotas').filter(
id__in=[i for i in item_ids if i and i not in self._items_cache]
)}
)
self._variations_cache.update(
{v.pk: v for v in
@@ -128,10 +111,9 @@ class CartManager:
)
def _check_max_cart_size(self):
cartsize = self.positions.filter(addon_to__isnull=True).count()
cartsize += sum([op.count for op in self._operations if isinstance(op, self.AddOperation) and not op.addon_to])
cartsize -= len([1 for op in self._operations if isinstance(op, self.RemoveOperation) if
not op.position.addon_to_id])
cartsize = self.positions.count()
cartsize += sum([op.count for op in self._operations if isinstance(op, self.AddOperation)])
cartsize -= len([1 for op in self._operations if isinstance(op, self.RemoveOperation)])
if cartsize > int(self.event.settings.max_items_per_order):
# TODO: i18n plurals
raise CartError(_(error_messages['max_items']) % (self.event.settings.max_items_per_order,))
@@ -151,10 +133,7 @@ class CartManager:
raise CartError(error_messages['voucher_invalid_item'])
if isinstance(op, self.AddOperation):
if op.item.category and op.item.category.is_addon and not op.addon_to:
raise CartError(error_messages['addon_only'])
if op.item.max_per_order or op.item.min_per_order:
if op.item.max_per_order:
new_total = (
len([1 for p in self.positions if p.item_id == op.item.pk]) +
sum([_op.count for _op in self._operations
@@ -164,21 +143,13 @@ class CartManager:
if isinstance(_op, self.RemoveOperation) and _op.position.item_id == op.item.pk])
)
if op.item.max_per_order and new_total > op.item.max_per_order:
raise CartError(
_(error_messages['max_items_per_product']) % {
'max': op.item.max_per_order,
'product': op.item.name
}
)
if op.item.min_per_order and new_total < op.item.min_per_order:
raise CartError(
_(error_messages['min_items_per_product']) % {
'min': op.item.min_per_order,
'product': op.item.name
}
)
if new_total > op.item.max_per_order:
raise CartError(
_(error_messages['max_items_per_product']) % {
'max': op.item.max_per_order,
'product': op.item.name
}
)
def _get_price(self, item: Item, variation: Optional[ItemVariation],
voucher: Optional[Voucher], custom_price: Optional[Decimal]):
@@ -265,8 +236,7 @@ class CartManager:
price = self._get_price(item, variation, voucher, i.get('price'))
op = self.AddOperation(
count=i['count'], item=item, variation=variation, price=price, voucher=voucher, quotas=quotas,
addon_to=False
count=i['count'], item=item, variation=variation, price=price, voucher=voucher, quotas=quotas
)
self._check_item_constraints(op)
operations.append(op)
@@ -275,144 +245,29 @@ class CartManager:
self._voucher_use_diff += voucher_use_diff
self._operations += operations
def remove_item(self, pos_id: int):
def remove_items(self, items: List[dict]):
# TODO: We could calculate quotadiffs and voucherdiffs here, which would lead to more
# flexible usages (e.g. a RemoveOperation and an AddOperation in the same transaction
# could cancel each other out quota-wise). However, we are not taking this performance
# penalty for now as there is currently no outside interface that would allow building
# such a transaction.
try:
cp = self.positions.get(pk=pos_id)
except CartPosition.DoesNotExist:
raise CartError(error_messages['unknown_position'])
self._operations.append(self.RemoveOperation(position=cp))
def clear(self):
# TODO: We could calculate quotadiffs and voucherdiffs here, which would lead to more
# flexible usages (e.g. a RemoveOperation and an AddOperation in the same transaction
# could cancel each other out quota-wise). However, we are not taking this performance
# penalty for now as there is currently no outside interface that would allow building
# such a transaction.
for cp in self.positions.all():
self._operations.append(self.RemoveOperation(position=cp))
def set_addons(self, addons):
self._update_items_cache(
[a['item'] for a in addons],
[a['variation'] for a in addons],
)
# Prepare various containers to hold data later
current_addons = defaultdict(dict) # CartPos -> currently attached add-ons
input_addons = defaultdict(set) # CartPos -> add-ons according to input
selected_addons = defaultdict(set) # CartPos -> final desired set of add-ons
cpcache = {} # CartPos.pk -> CartPos
quota_diff = Counter() # Quota -> Number of usages
operations = []
available_categories = defaultdict(set) # CartPos -> Category IDs to choose from
toplevel_cp = self.positions.filter(
addon_to__isnull=True
).prefetch_related(
'addons', 'item__addons', 'item__addons__addon_category'
).select_related('item', 'variation')
# Prefill some of the cache containers
for cp in toplevel_cp:
available_categories[cp.pk] = {iao.addon_category_id for iao in cp.item.addons.all()}
cpcache[cp.pk] = cp
current_addons[cp] = {
(a.item_id, a.variation_id): a
for a in cp.addons.all()
}
# Create operations, perform various checks
for a in addons:
# Check whether the specified items are part of what we just fetched from the database
# If they are not, the user supplied item IDs which either do not exist or belong to
# a different event
if a['item'] not in self._items_cache or (a['variation'] and a['variation'] not in self._variations_cache):
raise CartError(error_messages['not_for_sale'])
# Only attach addons to things that are actually in this user's cart
if a['addon_to'] not in cpcache:
raise CartError(error_messages['addon_invalid_base'])
cp = cpcache[a['addon_to']]
item = self._items_cache[a['item']]
variation = self._variations_cache[a['variation']] if a['variation'] is not None else None
if item.category_id not in available_categories[cp.pk]:
raise CartError(error_messages['addon_invalid_base'])
# Fetch all quotas. If there are no quotas, this item is not allowed to be sold.
quotas = list(item.quotas.all()) if variation is None else list(variation.quotas.all())
if not quotas:
raise CartError(error_messages['unavailable'])
# Every item can be attached to very CartPosition at most once
if a['item'] in ([_a[0] for _a in input_addons[cp.id]]):
raise CartError(error_messages['addon_duplicate_item'])
input_addons[cp.id].add((a['item'], a['variation']))
selected_addons[cp.id, item.category_id].add((a['item'], a['variation']))
if (a['item'], a['variation']) not in current_addons[cp]:
# This add-on is new, add it to the cart
for quota in quotas:
quota_diff[quota] += 1
price = self._get_price(item, variation, None, None)
op = self.AddOperation(
count=1, item=item, variation=variation, price=price, voucher=None, quotas=quotas,
addon_to=cp
)
self._check_item_constraints(op)
operations.append(op)
# Check constraints on the add-on combinations
for cp in toplevel_cp:
item = cp.item
for iao in item.addons.all():
selected = selected_addons[cp.id, iao.addon_category_id]
if len(selected) > iao.max_count:
# TODO: Proper i18n
# TODO: Proper pluralization
raise CartError(
error_messages['addon_max_count'],
{
'base': str(item.name),
'max': iao.max_count,
'cat': str(iao.addon_category.name),
}
)
elif len(selected) < iao.min_count:
# TODO: Proper i18n
# TODO: Proper pluralization
raise CartError(
error_messages['addon_min_count'],
{
'base': str(item.name),
'min': iao.min_count,
'cat': str(iao.addon_category.name),
}
)
# Detect removed add-ons and create RemoveOperations
for cp, al in current_addons.items():
for k, v in al.items():
if k not in input_addons[cp.id]:
if v.expires > self.now_dt:
quotas = list(cp.item.quotas.all()) if cp.variation is None else list(cp.variation.quotas.all())
for quota in quotas:
quota_diff[quota] -= 1
op = self.RemoveOperation(position=v)
operations.append(op)
self._quota_diff += quota_diff
self._operations += operations
for i in items:
cw = Q(cart_id=self.cart_id) & Q(item_id=i['item']) & Q(event=self.event)
if i['variation']:
cw &= Q(variation_id=i['variation'])
else:
cw &= Q(variation__isnull=True)
# Prefer to delete positions that have the same price as the one the user clicked on, after thet
# prefer the most expensive ones.
cnt = i['count']
if i['price']:
correctprice = CartPosition.objects.filter(cw).filter(price=Decimal(i['price'].replace(",", ".")))[:cnt]
for cp in correctprice:
self._operations.append(self.RemoveOperation(position=cp))
cnt -= len(correctprice)
if cnt > 0:
for cp in CartPosition.objects.filter(cw).order_by("-price")[:cnt]:
self._operations.append(self.RemoveOperation(position=cp))
def _get_quota_availability(self):
quotas_ok = {}
@@ -443,47 +298,12 @@ class CartManager:
return vouchers_ok
def _check_min_per_product(self):
per_product = Counter()
min_per_product = {}
for p in self.positions:
per_product[p.item_id] += 1
min_per_product[p.item.pk] = p.item.min_per_order
for op in self._operations:
if isinstance(op, self.AddOperation):
per_product[op.item.pk] += op.count
min_per_product[op.item.pk] = op.item.min_per_order
elif isinstance(op, self.RemoveOperation):
per_product[op.position.item_id] -= 1
min_per_product[op.position.item.pk] = op.position.item.min_per_order
err = None
for itemid, num in per_product.items():
min_p = min_per_product[itemid]
if min_p and num < min_p:
self._operations = [o for o in self._operations if not (
isinstance(o, self.AddOperation) and o.item.pk == itemid
)]
removals = [o.position.pk for o in self._operations if isinstance(o, self.RemoveOperation)]
for p in self.positions:
if p.item_id == itemid and p.pk not in removals:
self._operations.append(self.RemoveOperation(position=p))
err = _(error_messages['min_items_per_product_removed']) % {
'min': min_p,
'product': p.item.name
}
return err
def _perform_operations(self):
vouchers_ok = self._get_voucher_availability()
quotas_ok = self._get_quota_availability()
err = None
new_cart_positions = []
err = err or self._check_min_per_product()
self._operations.sort(key=lambda a: self.order[type(a)])
for op in self._operations:
@@ -522,8 +342,7 @@ class CartManager:
new_cart_positions.append(CartPosition(
event=self.event, item=op.item, variation=op.variation,
price=op.price, expires=self._expiry,
cart_id=self.cart_id, voucher=op.voucher,
addon_to=op.addon_to if op.addon_to else None
cart_id=self.cart_id, voucher=op.voucher
))
elif isinstance(op, self.ExtendOperation):
if available_count == 1:
@@ -558,7 +377,7 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
"""
Adds a list of items to a user's cart.
:param event: The event ID in question
:param items: A list of dicts with the keys item, variation, number, custom_price, voucher
:param items: A list of tuple of the form (item id, variation id or None, number, custom_price, voucher)
:param session: Session ID of a guest
:param coupon: A coupon that should also be reeemed
:raises CartError: On any error that occured
@@ -577,11 +396,11 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def remove_cart_position(self, event: int, position: int, cart_id: str=None, locale='en') -> None:
def remove_items_from_cart(self, event: int, items: List[dict], cart_id: str=None, locale='en') -> None:
"""
Removes a list of items from a user's cart.
:param event: The event ID in question
:param position: A cart position ID
:param items: A list of tuple of the form (item id, variation id or None, number)
:param session: Session ID of a guest
"""
with language(locale):
@@ -589,48 +408,7 @@ def remove_cart_position(self, event: int, position: int, cart_id: str=None, loc
try:
try:
cm = CartManager(event=event, cart_id=cart_id)
cm.remove_item(position)
cm.commit()
except LockTimeoutException:
self.retry()
except (MaxRetriesExceededError, LockTimeoutException):
raise CartError(error_messages['busy'])
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def clear_cart(self, event: int, cart_id: str=None, locale='en') -> None:
"""
Removes a list of items from a user's cart.
:param event: The event ID in question
:param session: Session ID of a guest
"""
with language(locale):
event = Event.objects.get(id=event)
try:
try:
cm = CartManager(event=event, cart_id=cart_id)
cm.clear()
cm.commit()
except LockTimeoutException:
self.retry()
except (MaxRetriesExceededError, LockTimeoutException):
raise CartError(error_messages['busy'])
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def set_cart_addons(self, event: int, addons: List[dict], cart_id: str=None, locale='en') -> None:
"""
Removes a list of items from a user's cart.
:param event: The event ID in question
:param addons: A list of dicts with the keys addon_to, item, variation
:param session: Session ID of a guest
"""
with language(locale):
event = Event.objects.get(id=event)
try:
try:
cm = CartManager(event=event, cart_id=cart_id)
cm.set_addons(addons)
cm.remove_items(items)
cm.commit()
except LockTimeoutException:
self.retry()

View File

@@ -21,4 +21,3 @@ def export(event: str, fileid: str, provider: str, form_data: Dict[str, Any]) ->
file.filename, file.type, data = ex.render(form_data)
file.file.save(cachedfile_name(file, file.filename), ContentFile(data))
file.save()
return file.pk

View File

@@ -72,8 +72,6 @@ def build_invoice(invoice: Invoice) -> Invoice:
desc = str(p.item.name)
if p.variation:
desc += " - " + str(p.variation.value)
if p.addon_to_id:
desc = " + " + desc
InvoiceLine.objects.create(
invoice=invoice, description=desc,
gross_value=p.price, tax_value=p.tax_value,
@@ -202,14 +200,6 @@ def _invoice_generate_german(invoice, f):
p_size = p.wrap(85 * mm, 50 * mm)
p.drawOn(canvas, 25 * mm, (297 - 52) * mm - p_size[1])
textobject = canvas.beginText(125 * mm, (297 - 38) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(_('Order code').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(invoice.order.full_code)
canvas.drawText(textobject)
textobject = canvas.beginText(125 * mm, (297 - 50) * mm)
textobject.setFont('OpenSansBd', 8)
if invoice.is_cancellation:
@@ -253,6 +243,20 @@ def _invoice_generate_german(invoice, f):
canvas.drawText(textobject)
textobject = canvas.beginText(165 * mm, (297 - 50) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(_('Order code').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(invoice.order.full_code)
textobject.moveCursor(0, 5)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(_('Order date').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(date_format(invoice.order.datetime, "DATE_FORMAT"))
canvas.drawText(textobject)
if invoice.event.settings.invoice_logo_image:
logo_file = invoice.event.settings.get('invoice_logo_image', binary_file=True)
canvas.drawImage(ImageReader(logo_file),

View File

@@ -1,5 +1,5 @@
import logging
from typing import Any, Dict, List, Union
from typing import Any, Dict, Union
import bleach
import cssutils
@@ -138,7 +138,7 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
@app.task
def mail_send_task(to: List[str], subject: str, body: str, html: str, sender: str,
def mail_send_task(to: str, subject: str, body: str, html: str, sender: str,
event: int=None, headers: dict=None) -> bool:
email = EmailMultiAlternatives(subject, body, sender, to=to, headers=headers)
email.attach_alternative(inline_css(html), "text/html")

View File

@@ -562,7 +562,6 @@ class OrderChangeManager:
'new_item': op.item.pk,
'new_variation': op.variation.pk if op.variation else None,
'old_price': op.position.price,
'addon_to': op.position.addon_to_id,
'new_price': op.price
})
op.position.item = op.item
@@ -575,29 +574,18 @@ class OrderChangeManager:
'position': op.position.pk,
'positionid': op.position.positionid,
'old_price': op.position.price,
'addon_to': op.position.addon_to_id,
'new_price': op.price
})
op.position.price = op.price
op.position._calculate_tax()
op.position.save()
elif isinstance(op, self.CancelOperation):
for opa in op.position.addons.all():
self.order.log_action('pretix.event.order.changed.cancel', user=self.user, data={
'position': opa.pk,
'positionid': opa.positionid,
'old_item': opa.item.pk,
'old_variation': opa.variation.pk if opa.variation else None,
'addon_to': opa.addon_to_id,
'old_price': opa.price,
})
self.order.log_action('pretix.event.order.changed.cancel', user=self.user, data={
'position': op.position.pk,
'positionid': op.position.positionid,
'old_item': op.position.item.pk,
'old_variation': op.position.variation.pk if op.position.variation else None,
'old_price': op.position.price,
'addon_to': None,
})
op.position.delete()

View File

@@ -54,7 +54,7 @@ def assign_automatically(event_id: int, user_id: int=None):
@receiver(signal=periodic_task)
def process_waitinglist(sender, **kwargs):
qs = Event.objects.prefetch_related('_settings_objects', 'organizer___settings_objects').select_related('organizer')
qs = Event.objects.prefetch_related('setting_objects', 'organizer__setting_objects').select_related('organizer')
for e in qs:
if e.settings.waiting_list_enabled and e.settings.waiting_list_auto:
assign_automatically.apply_async(args=(e.pk,))

View File

@@ -1,14 +1,19 @@
import decimal
import json
from datetime import datetime
from datetime import date, datetime, time
from django.core.cache import cache
from typing import Any, Dict, Optional
import dateutil.parser
from django.conf import settings
from django.core.files import File
from django.core.files.storage import default_storage
from django.db.models import Model
from django.utils.translation import ugettext_noop
from hierarkey.models import GlobalSettingsBase, Hierarkey
from i18nfield.strings import LazyI18nString
from typing import Any
from pretix.base.models.settings import GlobalSetting
DEFAULTS = {
'max_items_per_order': {
@@ -27,14 +32,6 @@ DEFAULTS = {
'default': 'False',
'type': bool
},
'attendee_emails_asked': {
'default': 'False',
'type': bool
},
'attendee_emails_required': {
'default': 'False',
'type': bool
},
'invoice_address_asked': {
'default': 'True',
'type': bool,
@@ -159,10 +156,6 @@ DEFAULTS = {
'default': None,
'type': datetime
},
'ticket_download_addons': {
'default': 'False',
'type': bool
},
'last_order_modification_date': {
'default': None,
'type': datetime
@@ -389,27 +382,194 @@ Your {event} team"""))
}
}
settings_hierarkey = Hierarkey(attribute_name='settings')
for k, v in DEFAULTS.items():
settings_hierarkey.add_default(k, v['default'], v['type'])
class SettingsProxy:
"""
This object allows convenient access to settings stored in the
EventSettings/OrganizerSettings database model. It exposes all settings as
properties and it will do all the nasty inheritance and defaults stuff for
you.
"""
def __init__(self, obj: Model, parent: Optional[Model]=None, type=None):
self._obj = obj
self._parent = parent
self._cached_obj = None
self._write_cached_obj = None
self._type = type
def i18n_uns(v):
try:
return LazyI18nString(json.loads(v))
except ValueError:
return LazyI18nString(str(v))
def _cache(self) -> Dict[str, Any]:
if self._cached_obj is None:
self._cached_obj = cache.get_or_set(
'settings_{}_{}'.format(self._obj.settings_namespace, self._obj.pk),
lambda: {s.key: s.value for s in self._obj.setting_objects.all()},
timeout=1800
)
return self._cached_obj
def _write_cache(self) -> Dict[str, Any]:
if self._write_cached_obj is None:
self._write_cached_obj = {
s.key: s for s in self._obj.setting_objects.all()
}
return self._write_cached_obj
settings_hierarkey.add_type(LazyI18nString,
serialize=lambda s: json.dumps(s.data),
unserialize=i18n_uns)
def _flush(self) -> None:
self._cached_obj = None
self._write_cached_obj = None
self._flush_external_cache()
def _flush_external_cache(self):
cache.delete('settings_{}_{}'.format(self._obj.settings_namespace, self._obj.pk))
@settings_hierarkey.set_global(cache_namespace='global')
class GlobalSettingsObject(GlobalSettingsBase):
slug = '_global'
def freeze(self) -> dict:
"""
Returns a dictionary of all settings set for this object, including
any default values of its parents or hardcoded in pretix.
"""
settings = {}
for key, v in DEFAULTS.items():
settings[key] = self._unserialize(v['default'], v['type'])
if self._parent:
settings.update(self._parent.settings.freeze())
for key in self._cache():
settings[key] = self.get(key)
return settings
def _unserialize(self, value: str, as_type: type, binary_file=False) -> Any:
if as_type is None and value is not None and value.startswith('file://'):
as_type = File
if as_type is not None and isinstance(value, as_type):
return value
elif value is None:
return None
elif as_type == int or as_type == float or as_type == decimal.Decimal:
return as_type(value)
elif as_type == dict or as_type == list:
return json.loads(value)
elif as_type == bool or value in ('True', 'False'):
return value == 'True'
elif as_type == File:
try:
fi = default_storage.open(value[7:], 'rb' if binary_file else 'r')
fi.url = default_storage.url(value[7:])
return fi
except OSError:
return False
elif as_type == datetime:
return dateutil.parser.parse(value)
elif as_type == date:
return dateutil.parser.parse(value).date()
elif as_type == time:
return dateutil.parser.parse(value).time()
elif as_type == LazyI18nString and not isinstance(value, LazyI18nString):
try:
return LazyI18nString(json.loads(value))
except ValueError:
return LazyI18nString(str(value))
elif as_type is not None and issubclass(as_type, Model):
return as_type.objects.get(pk=value)
return value
def _serialize(self, value: Any) -> str:
if isinstance(value, str):
return value
elif isinstance(value, int) or isinstance(value, float) \
or isinstance(value, bool) or isinstance(value, decimal.Decimal):
return str(value)
elif isinstance(value, list) or isinstance(value, dict):
return json.dumps(value)
elif isinstance(value, datetime) or isinstance(value, date) or isinstance(value, time):
return value.isoformat()
elif isinstance(value, Model):
return value.pk
elif isinstance(value, LazyI18nString):
return json.dumps(value.data)
elif isinstance(value, File):
return 'file://' + value.name
raise TypeError('Unable to serialize %s into a setting.' % str(type(value)))
def get(self, key: str, default=None, as_type: type=None, binary_file=False):
"""
Get a setting specified by key ``key``. Normally, settings are strings, but
if you put non-strings into the settings object, you can request unserialization
by specifying ``as_type``. If the key does not have a harcdoded type in the pretix source,
omitting ``as_type`` always will get you a string.
If the setting with the specified name does not exist on this object, any parent object
will be queried (e.g. the organizer of an event). If still no value is found, a default
value hardcoded will be returned if one exists. If not, the value of the ``default`` argument
will be returned instead.
"""
if as_type is None and key in DEFAULTS:
as_type = DEFAULTS[key]['type']
if key in self._cache():
value = self._cache()[key]
else:
value = None
if self._parent:
value = self._parent.settings.get(key, as_type=str)
if value is None and key in DEFAULTS:
value = DEFAULTS[key]['default']
if value is None and default is not None:
value = default
return self._unserialize(value, as_type, binary_file=binary_file)
def __getitem__(self, key: str) -> Any:
return self.get(key)
def __getattr__(self, key: str) -> Any:
if key.startswith('_'):
return super().__getattr__(key)
return self.get(key)
def __setattr__(self, key: str, value: Any) -> None:
if key.startswith('_'):
return super().__setattr__(key, value)
self.set(key, value)
def __setitem__(self, key: str, value: Any) -> None:
self.set(key, value)
def set(self, key: str, value: Any) -> None:
"""
Stores a setting to the database of its object.
"""
wc = self._write_cache()
if key in wc:
s = wc[key]
else:
s = self._type(object=self._obj, key=key)
s.value = self._serialize(value)
s.save()
self._cache()[key] = s.value
wc[key] = s
self._flush_external_cache()
def __delattr__(self, key: str) -> None:
if key.startswith('_'):
return super().__delattr__(key)
self.delete(key)
def __delitem__(self, key: str) -> None:
self.delete(key)
def delete(self, key: str) -> None:
"""
Deletes a setting from this object's storage.
"""
if key in self._write_cache():
self._write_cache()[key].delete()
del self._write_cache()[key]
if key in self._cache():
del self._cache()[key]
self._flush_external_cache()
class SettingsSandbox:
@@ -454,3 +614,13 @@ class SettingsSandbox:
def set(self, key: str, value: Any):
self._event.settings.set(self._convert_key(key), value)
class GlobalSettingsObject():
settings_namespace = 'global'
def __init__(self):
self.settings = SettingsProxy(self, type=GlobalSetting)
self.setting_objects = GlobalSetting.objects
self.slug = '_global'
self.pk = '_global'

View File

@@ -1,4 +1,3 @@
import warnings
from typing import Any, Callable, List, Tuple
import django.dispatch
@@ -53,13 +52,6 @@ class EventPluginSignal(django.dispatch.Signal):
return responses
class DeprecatedSignal(django.dispatch.Signal):
def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
warnings.warn('This signal is deprecated and will soon be removed', stacklevel=3)
super().connect(receiver, sender=None, weak=True, dispatch_uid=None)
event_live_issues = EventPluginSignal(
providing_args=[]
)
@@ -161,21 +153,6 @@ to the user. The receivers are expected to return HTML code.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
event_copy_data = EventPluginSignal(
providing_args=["other"]
)
"""
This signal is sent out when a new event is created as a clone of an existing event, i.e.
the settings from the older event are copied to the newer one. You can listen to this
signal to copy data or configuration stored within your plugin's models as well.
You don't need to copy data inside the general settings storage which is cloned automatically,
but you might need to modify that data.
The ``sender`` keyword argument will contain the event of the **new** event. The ``other``
keyword argument will contain the event to **copy from**.
"""
periodic_task = django.dispatch.Signal()
"""
This is a regular django signal (no pretix event signal) that we send out every

View File

@@ -11,7 +11,6 @@ ALLOWED_TAGS = [
'acronym',
'b',
'blockquote',
'br',
'code',
'em',
'i',

View File

@@ -51,15 +51,10 @@ class BaseTicketOutput:
This method should generate a download file and return a tuple consisting of a
filename, a file type and file content. The extension will be taken from the filename
which is otherwise ignored.
If you override this method, make sure that positions that are addons (i.e. ``addon_to``
is set) are only outputted if the event setting ``ticket_download_addons`` is active.
"""
with tempfile.TemporaryDirectory() as d:
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
for pos in order.positions.all():
if pos.addon_to_id and not self.event.settings.ticket_download_addons:
continue
fname, __, content = self.generate(pos)
zipf.writestr('{}-{}{}'.format(
order.code, pos.positionid, os.path.splitext(fname)[1]

View File

@@ -72,7 +72,6 @@ class AsyncAction:
# but handle the mssage itself
data.update({
'redirect': self.get_success_url(res.info),
'success': True,
'message': str(self.get_success_message(res.info))
})
else:
@@ -81,7 +80,6 @@ class AsyncAction:
# but handle the mssage itself
data.update({
'redirect': self.get_error_url(),
'success': False,
'message': str(self.get_error_message(res.info))
})
return data
@@ -105,7 +103,6 @@ class AsyncAction:
if "ajax" in self.request.POST or "ajax" in self.request.GET:
return JsonResponse({
'ready': True,
'success': True,
'redirect': self.get_success_url(value),
'message': str(self.get_success_message(value))
})
@@ -116,7 +113,6 @@ class AsyncAction:
if "ajax" in self.request.POST or "ajax" in self.request.GET:
return JsonResponse({
'ready': True,
'success': False,
'redirect': self.get_error_url(),
'message': str(self.get_error_message(exception))
})

View File

@@ -5,7 +5,7 @@ from django.core.urlresolvers import Resolver404, get_script_prefix, resolve
from pretix.base.settings import GlobalSettingsObject
from .signals import html_head, nav_event, nav_global, nav_topbar
from .signals import html_head, nav_event, nav_topbar
from .utils.i18n import get_javascript_format, get_moment_locale
@@ -38,14 +38,8 @@ def contextprocessor(request):
_nav_event += response
if request.event.settings.get('payment_term_weekdays'):
_js_payment_weekdays_disabled = '[0,6]'
ctx['nav_event'] = _nav_event
ctx['js_payment_weekdays_disabled'] = _js_payment_weekdays_disabled
_nav_global = []
if not hasattr(request, 'event'):
for receiver, response in nav_global.send(request, request=request):
_nav_global += response
ctx['nav_global'] = _nav_global
ctx['nav_event'] = _nav_event
_nav_topbar = []
for receiver, response in nav_topbar.send(request, request=request):

View File

@@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _
from i18nfield.forms import I18nFormField, I18nTextarea
from pytz import common_timezones, timezone
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
from pretix.base.forms import I18nModelForm, SettingsForm
from pretix.base.models import Event, Organizer
from pretix.control.forms import ExtFileField
@@ -61,11 +61,9 @@ class EventWizardBasicsForm(I18nModelForm):
]
widgets = {
'date_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'date_to': forms.DateTimeInput(attrs={'class': 'datetimepicker',
'data-date-after': '#id_basics-date_from'}),
'date_to': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'presale_start': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'presale_end': forms.DateTimeInput(attrs={'class': 'datetimepicker',
'data-date-after': '#id_basics-presale_start'}),
'presale_end': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
}
def __init__(self, *args, **kwargs):
@@ -153,10 +151,9 @@ class EventUpdateForm(I18nModelForm):
]
widgets = {
'date_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'date_to': forms.DateTimeInput(attrs={'class': 'datetimepicker', 'data-date-after': '#id_date_from'}),
'date_to': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'presale_start': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'presale_end': forms.DateTimeInput(attrs={'class': 'datetimepicker',
'data-date-after': '#id_presale_start'}),
'presale_end': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
}
@@ -185,7 +182,6 @@ class EventSettingsForm(SettingsForm):
presale_start_show_date = forms.BooleanField(
label=_("Show start date"),
help_text=_("Show the presale start date before presale has started."),
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_presale_start'}),
required=False
)
last_order_modification_date = forms.DateTimeField(
@@ -225,48 +221,28 @@ class EventSettingsForm(SettingsForm):
min_value=6,
help_text=_("If a ticket voucher is sent to a person on the waiting list, it has to be redeemed within this "
"number of hours until it expires and can be re-assigned to the next person on the list."),
required=False,
widget=forms.NumberInput(attrs={'data-display-dependency': '#id_settings-waiting_list_enabled'}),
required=False
)
waiting_list_auto = forms.BooleanField(
label=_("Automatic waiting list assignments"),
help_text=_("If ticket capacity becomes free, automatically create a voucher and send it to the first person "
"on the waiting list for that product. If this is not active, mails will not be send automatically "
"but you can send them manually via the control panel."),
required=False,
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_settings-waiting_list_enabled'}),
required=False
)
attendee_names_asked = forms.BooleanField(
label=_("Ask for attendee names"),
help_text=_("Ask for a name for all tickets which include admission to the event."),
required=False,
required=False
)
attendee_names_required = forms.BooleanField(
label=_("Require attendee names"),
help_text=_("Require customers to fill in the names of all attendees."),
required=False,
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_names_asked'}),
)
attendee_emails_asked = forms.BooleanField(
label=_("Ask for email addresses per ticket"),
help_text=_("Normally, pretix asks for one email address per order and the order confirmation will be send "
"to that email address. If you enable this option, the system will additionally ask for "
"individual email addresses for every admission ticket. This might be useful if you want to "
"obtain individual addresses for every attendee even in case of group orders."),
required=False
)
attendee_emails_required = forms.BooleanField(
label=_("Require email addresses per ticket"),
help_text=_("Require customers to fill in individual e-mail addresses for all admission tickets. See the "
"above option for more details. One email address for the order confirmation will always be "
"required regardless of this setting."),
required=False,
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_emails_asked'}),
)
max_items_per_order = forms.IntegerField(
min_value=1,
label=_("Maximum number of items per order"),
help_text=_("Add-on products will not be counted.")
label=_("Maximum number of items per order")
)
reservation_time = forms.IntegerField(
min_value=0,
@@ -298,10 +274,6 @@ class EventSettingsForm(SettingsForm):
raise ValidationError({
'attendee_names_required': _('You cannot require specifying attendee names if you do not ask for them.')
})
if data['attendee_emails_required'] and not data['attendee_emails_asked']:
raise ValidationError({
'attendee_emails_required': _('You have to ask for attendee emails if you want to make them required.')
})
return data
@@ -374,7 +346,7 @@ class ProviderForm(SettingsForm):
if isinstance(v, I18nFormField):
v._required = v.one_required
v.one_required = False
v.widget.enabled_locales = self.locales
v.widget.enabled_langcodes = self.obj.settings.get('locales')
def clean(self):
cleaned_data = super().clean()
@@ -394,13 +366,11 @@ class InvoiceSettingsForm(SettingsForm):
)
invoice_address_required = forms.BooleanField(
label=_("Require invoice address"),
required=False,
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_asked'}),
required=False
)
invoice_address_vatid = forms.BooleanField(
label=_("Ask for VAT ID"),
help_text=_("Does only work if an invoice address is asked for. VAT ID is not required."),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_asked'}),
required=False
)
invoice_numbers_consecutive = forms.BooleanField(
@@ -472,44 +442,37 @@ class MailSettingsForm(SettingsForm):
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {total}, {currency}, {date}, {paymentinfo}, {url}, "
"{invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{event}', '{total}', '{currency}', '{date}', '{paymentinfo}',
'{url}', '{invoice_name}', '{invoice_company}'])]
"{invoice_name}, {invoice_company}")
)
mail_text_order_paid = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}, {payment_info}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{invoice_name}', '{invoice_company}', '{payment_info}'])]
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}, {payment_info}")
)
mail_text_order_free = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{invoice_name}', '{invoice_company}'])]
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}")
)
mail_text_order_changed = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{invoice_name}', '{invoice_company}'])]
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}")
)
mail_text_resend_link = I18nFormField(
label=_("Text (sent by admin)"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{invoice_name}', '{invoice_company}'])]
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}")
)
mail_text_resend_all_links = I18nFormField(
label=_("Text (requested by user)"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {orders}"),
validators=[PlaceholderValidator(['{event}', '{orders}'])]
help_text=_("Available placeholders: {event}, {orders}")
)
mail_days_order_expire_warning = forms.IntegerField(
label=_("Number of days"),
@@ -522,15 +485,13 @@ class MailSettingsForm(SettingsForm):
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {expire_date}, {invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{expire_date}', '{invoice_name}', '{invoice_company}'])]
help_text=_("Available placeholders: {event}, {url}, {expire_date}, {invoice_name}, {invoice_company}")
)
mail_text_waiting_list = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {product}, {hours}, {code}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{product}', '{hours}', '{code}'])]
help_text=_("Available placeholders: {event}, {url}, {product}, {hours}, {code}")
)
smtp_use_custom = forms.BooleanField(
label=_("Use custom SMTP server"),
@@ -616,13 +577,7 @@ class TicketSettingsForm(SettingsForm):
label=_("Download date"),
help_text=_("Ticket download will be offered after this date."),
required=True,
widget=forms.DateTimeInput(attrs={'class': 'datetimepicker',
'data-display-dependency': '#id_ticket_download'}),
)
ticket_download_addons = forms.BooleanField(
label=_("Offer to download tickets separately for add-on products"),
required=False,
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_ticket_download'}),
widget=forms.DateTimeInput(attrs={'class': 'datetimepicker'})
)
def prepare_fields(self):
@@ -631,10 +586,6 @@ class TicketSettingsForm(SettingsForm):
v._required = v.required
v.required = False
v.widget.is_required = False
if isinstance(v, I18nFormField):
v._required = v.one_required
v.one_required = False
v.widget.enabled_locales = self.locales
def clean(self):
# required=True files should only be required if the feature is enabled

View File

@@ -2,7 +2,6 @@ import copy
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Max
from django.forms import BooleanField, ModelMultipleChoiceField
from django.forms.formsets import DELETION_FIELD_NAME
from django.utils.translation import ugettext as __, ugettext_lazy as _
@@ -12,7 +11,6 @@ from pretix.base.forms import I18nFormSet, I18nModelForm
from pretix.base.models import (
Item, ItemCategory, ItemVariation, Question, QuestionOption, Quota,
)
from pretix.base.models.items import ItemAddOn
class CategoryForm(I18nModelForm):
@@ -21,8 +19,7 @@ class CategoryForm(I18nModelForm):
localized_fields = '__all__'
fields = [
'name',
'description',
'is_addon'
'description'
]
@@ -111,7 +108,6 @@ class ItemCreateForm(I18nModelForm):
def __init__(self, *args, **kwargs):
self.event = kwargs['event']
super().__init__(*args, **kwargs)
self.fields['category'].queryset = self.instance.event.categories.all()
self.fields['copy_from'] = forms.ModelChoiceField(
label=_("Copy product information"),
queryset=self.event.items.all(),
@@ -121,20 +117,7 @@ class ItemCreateForm(I18nModelForm):
)
def save(self, *args, **kwargs):
if self.cleaned_data.get('copy_from'):
self.instance.description = self.cleaned_data['copy_from'].description
self.instance.active = self.cleaned_data['copy_from'].active
self.instance.available_from = self.cleaned_data['copy_from'].available_from
self.instance.available_until = self.cleaned_data['copy_from'].available_until
self.instance.require_voucher = self.cleaned_data['copy_from'].require_voucher
self.instance.hide_without_voucher = self.cleaned_data['copy_from'].hide_without_voucher
self.instance.allow_cancel = self.cleaned_data['copy_from'].allow_cancel
self.instance.min_per_order = self.cleaned_data['copy_from'].min_per_order
self.instance.max_per_order = self.cleaned_data['copy_from'].max_per_order
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
instance = super().save(*args, **kwargs)
if self.cleaned_data.get('has_variations'):
if self.cleaned_data.get('copy_from') and self.cleaned_data.get('copy_from').has_variations:
for variation in self.cleaned_data['copy_from'].variations.all():
@@ -145,9 +128,8 @@ class ItemCreateForm(I18nModelForm):
item=instance, value=__('Standard')
)
if self.cleaned_data.get('copy_from'):
for question in self.cleaned_data['copy_from'].questions.all():
question.items.add(instance)
for question in Question.objects.filter(items=self.cleaned_data.get('copy_from')):
question.items.add(instance)
return instance
@@ -156,7 +138,6 @@ class ItemCreateForm(I18nModelForm):
localized_fields = '__all__'
fields = [
'name',
'category',
'admission',
'default_price',
'tax_rate',
@@ -187,8 +168,7 @@ class ItemUpdateForm(I18nModelForm):
'require_voucher',
'hide_without_voucher',
'allow_cancel',
'max_per_order',
'min_per_order',
'max_per_order'
]
widgets = {
'available_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
@@ -226,64 +206,4 @@ class ItemVariationForm(I18nModelForm):
'value',
'active',
'default_price',
'description',
]
class ItemAddOnsFormSet(I18nFormSet):
def __init__(self, *args, **kwargs):
self.event = kwargs.get('event')
super().__init__(*args, **kwargs)
def _construct_form(self, i, **kwargs):
kwargs['event'] = self.event
return super()._construct_form(i, **kwargs)
def clean(self):
super().clean()
categories = set()
for i in range(0, self.total_form_count()):
form = self.forms[i]
if self.can_delete:
if self._should_delete_form(form):
# This form is going to be deleted so any of its errors
# should not cause the entire formset to be invalid.
continue
if form.cleaned_data['addon_category'] in categories:
raise ValidationError(_('You added the same add-on category twice'))
categories.add(form.cleaned_data['addon_category'])
@property
def empty_form(self):
self.is_valid()
form = self.form(
auto_id=self.auto_id,
prefix=self.add_prefix('__prefix__'),
empty_permitted=True,
locales=self.locales,
event=self.event
)
self.add_fields(form, None)
return form
class ItemAddOnForm(I18nModelForm):
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
super().__init__(*args, **kwargs)
self.fields['addon_category'].queryset = self.event.categories.all()
class Meta:
model = ItemAddOn
localized_fields = '__all__'
fields = [
'addon_category',
'min_count',
'max_count',
]
help_texts = {
'min_count': _('Be aware that setting a minimal number makes it impossible to buy this product if all '
'available add-ons are sold out.')
}

View File

@@ -1,5 +1,4 @@
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.formats import localize
@@ -119,16 +118,3 @@ class OrderContactForm(forms.ModelForm):
class Meta:
model = Order
fields = ['email']
class OrderLocaleForm(forms.ModelForm):
locale = forms.ChoiceField()
class Meta:
model = Order
fields = ['locale']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
locale_names = dict(settings.LANGUAGES)
self.fields['locale'].choices = [(a, locale_names[a]) for a in self.instance.event.settings.locales]

View File

@@ -3,7 +3,6 @@ from django.utils.translation import ugettext_lazy as _
from pretix.base.forms import I18nModelForm
from pretix.base.models import Organizer
from pretix.multidomain.models import KnownDomain
class OrganizerForm(I18nModelForm):
@@ -26,42 +25,9 @@ class OrganizerForm(I18nModelForm):
class OrganizerUpdateForm(OrganizerForm):
def __init__(self, *args, **kwargs):
self.domain = kwargs.pop('domain', False)
kwargs.setdefault('initial', {})
self.instance = kwargs['instance']
if self.domain and self.instance:
initial_domain = self.instance.domains.first()
if initial_domain:
kwargs['initial'].setdefault('domain', initial_domain.domainname)
super().__init__(*args, **kwargs)
self.fields['slug'].widget.attrs['readonly'] = 'readonly'
if self.domain:
self.fields['domain'] = forms.CharField(
max_length=255,
label=_('Custom domain'),
required=False,
help_text=_('You need to configure the custom domain in the webserver beforehand.')
)
def clean_slug(self):
return self.instance.slug
def save(self, commit=True):
instance = super().save(commit)
if self.domain:
current_domain = instance.domains.first()
if self.cleaned_data['domain']:
if current_domain and current_domain.domainname != self.cleaned_data['domain']:
current_domain.delete()
KnownDomain.objects.create(organizer=instance, domainname=self.cleaned_data['domain'])
elif not current_domain:
KnownDomain.objects.create(organizer=instance, domainname=self.cleaned_data['domain'])
elif current_domain:
current_domain.delete()
instance.get_cache().clear()
return instance

View File

@@ -76,9 +76,6 @@ class VoucherForm(I18nModelForm):
else:
self.instance.variation = None
self.instance.quota = None
if self.instance.item.category and self.instance.item.category.is_addon:
raise ValidationError(_('It is currently not possible to create vouchers for add-on products.'))
else:
self.instance.quota = Quota.objects.get(pk=quotaid, event=self.instance.event)
self.instance.item = None

View File

@@ -95,9 +95,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.item.variation.added': _('The variation "{value}" has been created.'),
'pretix.event.item.variation.deleted': _('The variation "{value}" has been deleted.'),
'pretix.event.item.variation.changed': _('The variation "{value}" has been modified.'),
'pretix.event.item.addons.added': _('An add-on has been added to this product.'),
'pretix.event.item.addons.removed': _('An add-on has been removed from this product.'),
'pretix.event.item.addons.changed': _('An add-on has been changed on this product.'),
'pretix.event.quota.added': _('The quota has been added.'),
'pretix.event.quota.deleted': _('The quota has been deleted.'),
'pretix.event.quota.changed': _('The quota has been modified.'),

View File

@@ -1,6 +1,6 @@
from django.dispatch import Signal
from pretix.base.signals import DeprecatedSignal, EventPluginSignal
from pretix.base.signals import EventPluginSignal
restriction_formset = EventPluginSignal(
providing_args=["item"]
@@ -47,31 +47,11 @@ nav_topbar = Signal(
)
"""
This signal allows you to add additional views to the top navigation bar.
You will get the request as a keyword argument ``request``.
You will get the request as a keyword argument ``return``.
Receivers are expected to return a list of dictionaries. The dictionaries
should contain at least the keys ``label`` and ``url``. You can also return
a fontawesome icon name with the key ``icon``, it will be respected depending
on the type of navigation. If set, on desktops only the ``icon`` will be shown.
The ``title`` property can be used to set the alternative text.
If you use this, you should read the documentation on :ref:`how to deal with URLs <urlconf>`
in pretix.
This is no ``EventPluginSignal``, so you do not get the event in the ``sender`` argument
and you may get the signal regardless of whether your plugin is active.
"""
nav_global = Signal(
providing_args=["request"]
)
"""
This signal allows you to add additional views to the navigation bar when no event is
selected. You will get the request as a keyword argument ``request``.
Receivers are expected to return a list of dictionaries. The dictionaries
should contain at least the keys ``label`` and ``url``. You can also return
a fontawesome icon name with the key ``icon``, it will be respected depending
on the type of navigation. You should also return an ``active`` key with a boolean
set to ``True``, when this item should be marked as active.
If you use this, you should read the documentation on :ref:`how to deal with URLs <urlconf>`
in pretix.
@@ -143,29 +123,14 @@ quota as argument in the ``quota`` keyword argument.
As with all plugin signals, the ``sender`` keyword argument will contain the event.
"""
organizer_edit_tabs = DeprecatedSignal(
organizer_edit_tabs = Signal(
providing_args=['organizer', 'request']
)
"""
Deprecated signal, no longer works. We just keep the definition so old plugins don't
break the installation.
"""
nav_organizer = Signal(
providing_args=['organizer', 'request']
)
"""
This signal is sent out to include tab links on the detail page of an organizer.
Receivers are expected to return a list of dictionaries. The dictionaries
should contain at least the keys ``label`` and ``url``. You should also return
an ``active`` key with a boolean set to ``True``, when this item should be marked
as active.
If your linked view should stay in the tab-like context of this page, we recommend
that you use ``pretix.control.views.organizer.OrganizerDetailViewMixin`` for your view
and your tempalte inherits from ``pretixcontrol/organizers/base.html``.
This is a regular django signal (no pretix event signal). Receivers will be passed
the keyword arguments ``organizer`` and ``request``.
This signal is sent out to include tabs on the detail page of an organizer. Receivers
should return a tuple with the first item being the tab title and the second item
being the content as HTML. The receivers get the ``organizer`` and the ``request`` as
keyword arguments.
This is a regular django signal (no pretix event signal).
"""

View File

@@ -32,7 +32,6 @@
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/main.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quota.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/question.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/mail.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/asyncdownload.js" %}"></script>
{% endcompress %}
@@ -79,7 +78,7 @@
<ul class="nav navbar-nav navbar-top-links navbar-right">
{% for nav in nav_topbar %}
<li {% if nav.children %}class="dropdown"{% endif %}>
<a href="{{ nav.url }}" title="{{ nav.title }}" {% if nav.active %}class="active"{% endif %}
<a href="{{ nav.url }}" {% if nav.active %}class="active"{% endif %}
{% if nav.children %}class="dropdown-toggle" data-toggle="dropdown"{% endif %}>
{% if nav.icon %}
<span class="fa fa-{{ nav.icon }}"></span>
@@ -114,7 +113,7 @@
</li>
{% endif %}
<li>
<a href="{% url 'control:user.settings' %}" title="{% trans "Account Settings" %}" >
<a href="{% url 'control:user.settings' %}">
<i class="fa fa-user"></i> {{ request.user.get_full_name }}
</a>
</li>
@@ -168,32 +167,6 @@
{% trans "Organizers" %}
</a>
</li>
{% for nav in nav_global %}
<li>
<a href="{{ nav.url }}" {% if nav.active %}class="active"{% endif %}
{% if nav.children %}class="has-children"{% endif %}>
{% if nav.icon %}
<i class="fa fa-{{ nav.icon }} fa-fw"></i>
{% endif %}
{{ nav.label }}
</a>
{% if nav.children %}
<a href="#" class="arrow">
<span class="fa arrow"></span>
</a>
<ul class="nav nav-second-level">
{% for item in nav.children %}
<li>
<a href="{{ item.url }}"
{% if item.active %}class="active"{% endif %}>
{{ item.label }}
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
{% endblock %}
</ul>
</div>

View File

@@ -85,15 +85,12 @@
</div>
<div class="col-lg-2 col-sm-6 col-xs-12">
{% if log.user %}
<span class="fa fa-user"></span> {{ log.user.get_full_name }}
{% if log.user.is_superuser %}
<span class="fa fa-id-card fa-danger fa-fw"
data-toggle="tooltip"
title="{% trans "This change was performed by a pretix administrator." %}">
</span>
{% else %}
<span class="fa fa-user fa-fw"></span>
<img src="{% static "pretixbase/img/pretix-icon-colored.svg" %}"
data-toggle="tooltip" class="user-admin-icon"
title="{% trans "This change was performed by a pretix administrator." %}">
{% endif %}
{{ log.user.get_full_name }}
{% endif %}
</div>
<div class="col-lg-2 col-sm-12 col-xs-12">

View File

@@ -32,15 +32,12 @@
</div>
<div class="col-lg-2 col-sm-6 col-xs-12">
{% if log.user %}
<span class="fa fa-user"></span> {{ log.user.get_full_name }}
{% if log.user.is_superuser %}
<span class="fa fa-id-card fa-danger fa-fw"
data-toggle="tooltip"
<img src="{% static "pretixbase/img/pretix-icon-colored.svg" %}"
data-toggle="tooltip" class="user-admin-icon"
title="{% trans "This change was performed by a pretix administrator." %}">
</span>
{% else %}
<span class="fa fa-user fa-fw"></span>
{% endif %}
{{ log.user.get_full_name }}
{% endif %}
</div>
<div class="col-lg-2 col-sm-12 col-xs-12">

View File

@@ -2,8 +2,7 @@
{% load i18n %}
{% load bootstrap3 %}
{% block inside %}
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data"
mail-preview-url="{% url "control:event.settings.mail.preview" event=request.event.slug organizer=request.event.organizer.slug %}">
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
@@ -14,26 +13,106 @@
<fieldset>
<legend>{% trans "E-mail content" %}</legend>
<div class="panel-group" id="questions_group">
{% blocktrans asvar title_placed_order %}Placed order{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_placed" title=title_placed_order items="mail_text_order_placed" %}
{% blocktrans asvar title_paid_order %}Paid order{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_paid" title=title_paid_order items="mail_text_order_paid" %}
{% blocktrans asvar title_free_order %}Free order{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_free" title=title_free_order items="mail_text_order_free" %}
{% blocktrans asvar title_resend_link %}Resend link{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="resend_link" title=title_resend_link items="mail_text_resend_link,mail_text_resend_all_links" %}
{% blocktrans asvar title_order_changed %}Order changed{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_changed" title=title_order_changed items="mail_text_order_changed" %}
{% blocktrans asvar title_payment_reminder %}Payment reminder{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_expirew" title=title_payment_reminder items="mail_days_order_expire_warning,mail_text_order_expire_warning" exclude="mail_days_order_expire_warning" %}
{% blocktrans asvar title_waiting_list_notification %}Waiting list notification{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="waiting_list" title=title_waiting_list_notification items="mail_text_waiting_list" %}
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#order_placed">
<strong>{% trans "Placed order" %}</strong>
</a>
</h4>
</div>
<div id="order_placed" class="panel-collapse collapse">
<div class="panel-body">
{% bootstrap_field form.mail_text_order_placed layout="horizontal" %}
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#order_paid">
<strong>{% trans "Paid order" %}</strong>
</a>
</h4>
</div>
<div id="order_paid" class="panel-collapse collapse">
<div class="panel-body">
{% bootstrap_field form.mail_text_order_paid layout="horizontal" %}
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#order_free">
<strong>{% trans "Free order" %}</strong>
</a>
</h4>
</div>
<div id="order_free" class="panel-collapse collapse">
<div class="panel-body">
{% bootstrap_field form.mail_text_order_free layout="horizontal" %}
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#resend_link">
<strong>{% trans "Resend link" %}</strong>
</a>
</h4>
</div>
<div id="resend_link" class="panel-collapse collapse">
<div class="panel-body">
{% bootstrap_field form.mail_text_resend_link layout="horizontal" %}
{% bootstrap_field form.mail_text_resend_all_links layout="horizontal" %}
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#order_changed">
<strong>{% trans "Order changed" %}</strong>
</a>
</h4>
</div>
<div id="order_changed" class="panel-collapse collapse">
<div class="panel-body">
{% bootstrap_field form.mail_text_order_changed layout="horizontal" %}
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#order_expirew">
<strong>{% trans "Payment reminder" %}</strong>
</a>
</h4>
</div>
<div id="order_expirew" class="panel-collapse collapse">
<div class="panel-body">
{% bootstrap_field form.mail_days_order_expire_warning layout="horizontal" %}
{% bootstrap_field form.mail_text_order_expire_warning layout="horizontal" %}
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#waiting_list">
<strong>{% trans "Waiting list notification" %}</strong>
</a>
</h4>
</div>
<div id="waiting_list" class="panel-collapse collapse">
<div class="panel-body">
{% bootstrap_field form.mail_text_waiting_list layout="horizontal" %}
</div>
</div>
</div>
</div>
</fieldset>
<fieldset>

View File

@@ -1,53 +0,0 @@
{% load i18n %}
{% load bootstrap3 %}
{% load mail_settings_preview %}
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#{{ pid }}">
<strong>{% trans title %}</strong>
</a>
</h4>
</div>
<div id="{{ pid }}" class="panel-collapse collapse">
<div class="panel-body">
{% with exclude|split as exclusion %}
{% with items|split as item_list %}
{% for item in item_list %}
{% if item in exclusion %}
{% with form|getattr:item as field %}
{% bootstrap_field field layout="horizontal" %}
{% endwith %}
{% else %}
<div id="{{ item }}_panel" class="preview-panel form-group" for="{{ item }}">
{% with form|getattr:item as field %}
<label class="col-md-3 control-label">{{ field.label }}</label>
<div class="col-md-9">
<div class="tab-content">
<div id="{{ item }}_edit" class="tab-pane fade in active">
{% bootstrap_field field show_label=False form_group_class="" %}
</div>
<div id="{{ item }}_preview" class="tab-pane mail-preview-group">
{% for l in request.event.settings.locales %}
<pre lang="{{ l }}" for="{{ item }}" class="mail-preview"></pre>
{% endfor %}
</div>
</div>
<ul class="nav nav-pills pull-right">
<li role="presentation" class="active">
<a data-toggle="pill" type="edit" href="#{{ item }}_edit"><i class="fa fa-pencil-square-o fa-fw"></i> {% trans "Edit" %}</a>
</li>
<li role="presentation">
<a data-toggle="pill" type="preview" href="#{{ item }}_preview"><i class="fa fa-tv fa-fw"></i> {% trans "Preview" %}</a>
</li>
</ul>
</div>
{% endwith %}
</div>
{% endif %}
{% endfor %}
{% endwith %}
{% endwith %}
</div>
</div>
</div>

View File

@@ -41,8 +41,6 @@
{% bootstrap_field sform.max_items_per_order layout="horizontal" %}
{% bootstrap_field sform.attendee_names_asked layout="horizontal" %}
{% bootstrap_field sform.attendee_names_required layout="horizontal" %}
{% bootstrap_field sform.attendee_emails_asked layout="horizontal" %}
{% bootstrap_field sform.attendee_emails_required layout="horizontal" %}
{% bootstrap_field sform.cancel_allow_user layout="horizontal" %}
</fieldset>
<fieldset>

View File

@@ -9,7 +9,6 @@
{% bootstrap_form_errors form %}
{% bootstrap_field form.ticket_download layout="horizontal" %}
{% bootstrap_field form.ticket_download_date layout="horizontal" %}
{% bootstrap_field form.ticket_download_addons layout="horizontal" %}
{% for provider in providers %}
<div class="panel panel-default ticketoutput-panel">
<div class="panel-heading">

View File

@@ -6,15 +6,12 @@
<p class="meta">
<span class="fa fa-clock-o"></span> {{ log.datetime|date:"SHORT_DATETIME_FORMAT" }}
{% if log.user %}
<br/><span class="fa fa-user"></span> {{ log.user.get_full_name }}
{% if log.user.is_superuser %}
<span class="fa fa-id-card fa-danger fa-fw"
data-toggle="tooltip"
title="{% trans "This change was performed by a pretix administrator." %}">
</span>
{% else %}
<span class="fa fa-user fa-fw"></span>
<img src="{% static "pretixbase/img/pretix-icon-colored.svg" %}"
data-toggle="tooltip" class="user-admin-icon"
title="{% trans "This change was performed by a pretix administrator." %}">
{% endif %}
{{ log.user.get_full_name }}
{% endif %}
</p>

View File

@@ -1,96 +0,0 @@
{% extends "pretixcontrol/item/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load formset_tags %}
{% block inside %}
<p>
{% blocktrans trimmed %}
With add-ons, you can specify products that can be bought as an addition to this product. For example, if
you host a conference with a base conference ticket and a number of workshops, you could define the
workshops as add-ons to the conference ticket. With this configuration, the workshops cannot be bought
on their own but only in combination with a conference ticket. You can here specify categories of products
that can be used as add-ons to this product. You can also specify the minimum and maximum number of
add-ons of the given category that can or need to be chosen. The user can buy every add-on from the
category at most once. If an add-on product has multiple variations, only one of them can be bought.
{% endblocktrans %}
</p>
<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.ORDER form_group_class="" layout="inline" %}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<div class="row">
<div class="col-sm-8">
<h3 class="panel-title">{% trans "Add-On" %}</h3>
</div>
<div class="col-sm-4 text-right">
<button type="button" class="btn btn-xs btn-default" data-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-xs btn-default" data-formset-move-down-button>
<i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-xs btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_form_errors form %}
{% bootstrap_field form.addon_category layout='horizontal' %}
{% bootstrap_field form.min_count layout='horizontal' %}
{% bootstrap_field form.max_count 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.ORDER form_group_class="" layout="inline" %}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<div class="row">
<div class="col-sm-8">
<h3 class="panel-title">{% trans "Add-On" %}</h3>
</div>
<div class="col-sm-4">
<button type="button" class="btn btn-xs btn-default" data-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-xs btn-default" data-formset-move-down-button>
<i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-xs btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_field formset.empty_form.addon_category layout='horizontal' %}
{% bootstrap_field formset.empty_form.min_count layout='horizontal' %}
{% bootstrap_field formset.empty_form.max_count 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 add-on" %}</button>
</p>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -4,25 +4,20 @@
{% block content %}
{% if object.id %}
<h1>{% trans "Modify product:" %} {{ object.name }}</h1>
{% if object.has_variations %}
<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>
{% if object.has_variations %}
<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>
{% endif %}
<li {% if "event.item.addons" == url_name %}class="active"{% endif %}>
<a href="{% url 'control:event.item.addons' organizer=request.event.organizer.slug event=request.event.slug item=object.id %}">
{% trans "Add-Ons" %}
<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>
{% endif %}
{% else %}
<h1>{% trans "Create product" %}</h1>
<p>{% blocktrans trimmed %}
@@ -31,12 +26,12 @@
{% endif %}
{% if object.id and not object.quotas.exists %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
{% blocktrans trimmed %}
Please note that your product will <strong>not</strong> be available for sale until you have added your
item to an existing or newly created quota.
{% endblocktrans %}
{% endblocktrans %}
</div>
{% endif %}
{% block inside %}
{% endblock %}
{% block inside %}
{% endblock %}
{% endblock %}

View File

@@ -9,7 +9,6 @@
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.copy_from layout="horizontal" %}
{% bootstrap_field form.has_variations layout="horizontal" %}
{% bootstrap_field form.category layout="horizontal" %}
{% bootstrap_field form.admission layout="horizontal" %}
</fieldset>
<fieldset>

View File

@@ -26,7 +26,6 @@
{% bootstrap_field form.available_from layout="horizontal" %}
{% bootstrap_field form.available_until layout="horizontal" %}
{% bootstrap_field form.max_per_order layout="horizontal" %}
{% bootstrap_field form.min_per_order layout="horizontal" %}
{% bootstrap_field form.require_voucher layout="horizontal" %}
{% bootstrap_field form.hide_without_voucher layout="horizontal" %}
{% bootstrap_field form.allow_cancel layout="horizontal" %}

View File

@@ -37,7 +37,6 @@
{% bootstrap_form_errors form %}
{% bootstrap_field form.active layout='horizontal' %}
{% bootstrap_field form.default_price layout='horizontal' %}
{% bootstrap_field form.description layout='horizontal' %}
</div>
</div>
{% endfor %}
@@ -70,7 +69,6 @@
<div class="panel-body form-horizontal">
{% bootstrap_field formset.empty_form.active layout='horizontal' %}
{% bootstrap_field formset.empty_form.default_price layout='horizontal' %}
{% bootstrap_field formset.empty_form.description layout='horizontal' %}
</div>
</div>
{% endescapescript %}

View File

@@ -13,7 +13,6 @@
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.description layout="horizontal" %}
{% bootstrap_field form.is_addon layout="horizontal" %}
</fieldset>
</div>
{% if category %}

View File

@@ -48,13 +48,6 @@
{% if position.variation %}
{{ position.variation }}
{% endif %}
{% if position.addon_to %}
<em>
{% blocktrans trimmed with posid=position.addon_to.positionid %}
Add-On to position #{{ posid }}
{% endblocktrans %}
</em>
{% endif %}
</h3>
</div>
<div class="panel-body">
@@ -96,11 +89,6 @@
<input name="{{ position.form.prefix }}-operation" type="radio" value="cancel"
{% if position.form.operation.value == "cancel" %}checked="checked"{% endif %}>
{% trans "Remove from order" %}
{% if position.addons.exists %}
<em class="text-danger">
{% trans "Removing this position will also remove all add-ons to this position." %}
</em>
{% endif %}
</label>
</div>
</div>

View File

@@ -1,30 +0,0 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}
{% trans "Change locale information" %}
{% endblock %}
{% block content %}
<h1>
{% trans "Change locale information" %}
</h1>
<p>
This language will be used whenever emails are sent to the users.
</p>
<form method="post" class="form-horizontal" href="">
{% csrf_token %}
<input type="hidden" name="status" value="c" />
{% bootstrap_form form layout='horizontal' %}
<div class="form-group submit-group">
<a class="btn btn-default btn-lg"
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
{% trans "Cancel" %}
</a>
<button class="btn btn-primary btn-save btn-lg" type="submit">
{% trans "Save" %}
</button>
<div class="clearfix"></div>
</div>
</form>
{% endblock %}

View File

@@ -73,13 +73,6 @@
<dd>{{ order.code }}</dd>
<dt>{% trans "Order date" %}</dt>
<dd>{{ order.datetime }}</dd>
<dt>{% trans "Order locale" %}</dt>
<dd>
{{ display_locale }}
<a href="{% url "control:event.order.locale" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<span class="fa fa-edit"></span>
</a>
</dd>
{% if order.status == "p" %}
<dt>{% trans "Payment date" %}</dt>
<dd>{{ order.payment_date }}</dd>
@@ -166,11 +159,7 @@
{% for line in items.positions %}
<div class="row-fluid product-row">
<div class="col-md-9 col-xs-6">
{% if line.addon_to %}
<span class="addon-signifier">+</span>
{% else %}
#{{ line.positionid }}
{% endif %}
#{{ line.positionid }}
<strong>{{ line.item.name }}</strong>
{% if line.variation %}
{{ line.variation }}
@@ -191,11 +180,6 @@
<dd>{% if line.attendee_name %}{{ line.attendee_name }}{% else %}
<em>{% trans "not answered" %}</em>{% endif %}</dd>
{% endif %}
{% if line.item.admission and event.settings.attendee_emails_asked %}
<dt>{% trans "Attendee email" %}</dt>
<dd>{% if line.attendee_email %}{{ line.attendee_email }}{% else %}
<em>{% trans "not answered" %}</em>{% endif %}</dd>
{% endif %}
{% for q in line.questions %}
<dt>{{ q.question }}</dt>
<dd>{% if q.answer %}{{ q.answer|linebreaksbr }}{% else %}

View File

@@ -11,9 +11,7 @@
<h3 class="panel-title">{{ e.verbose_name }}</h3>
</div>
<div class="panel-body">
<form action="{% url "control:event.orders.export.do" event=request.event.slug organizer=request.organizer.slug %}"
method="post" class="form-horizontal" data-asynctask data-asynctask-download
data-asynctask-long>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<input type="hidden" name="exporter" value="{{ e.identifier }}" />
{% bootstrap_form e.form layout='horizontal' %}

View File

@@ -1,39 +0,0 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Organizer" %}{% endblock %}
{% block content %}
<h1>
{% blocktrans with name=organizer.name %}Organizer: {{ name }}{% endblocktrans %}
<a href="{% url "control:organizer.edit" organizer=organizer.slug %}"
class="btn btn-default">
<span class="fa fa-edit"></span>
{% trans "Edit" %}
</a>
</h1>
<ul class="nav nav-pills">
<li {% if "organizer" == url_name %}class="active"{% endif %}>
<a href="{% url "control:organizer" organizer=organizer.slug %}">
{% trans "Events" %}
</a>
</li>
{% if request.orgaperm.can_change_permissions %}
<li {% if "organizer.teams" == url_name %}class="active"{% endif %}>
<a href="{% url "control:organizer.teams" organizer=organizer.slug %}">
{% trans "Permissions" %}
</a>
</li>
{% endif %}
{% for nav in nav_organizer %}
<li {% if nav.active %}class="active"{% endif %}>
<a href="{{ nav.url }}">
{{ nav.label }}
</a>
</li>
{% endfor %}
</ul>
{% block inner %}
{% endblock %}
{% endblock %}

View File

@@ -1,34 +1,159 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
{% if events|length == 0 %}
<p>
<em>{% trans "You currently do not have access to any events." %}</em>
</p>
{% else %}
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "Event name" %}</th>
<th>{% trans "Start date" %}</th>
</tr>
</thead>
<tbody>
{% for e in events %}
<tr>
<td>
<strong><a
href="{% url "control:event.index" organizer=e.organizer.slug event=e.slug %}">{{ e.name }}</a></strong>
</td>
<td>{{ e.get_date_from_display }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<a href="{% url "control:events.add" %}" class="btn btn-default">
<span class="fa fa-plus"></span>
{% trans "Create a new event" %}
</a>
{% block title %}{% trans "Organizer" %}{% endblock %}
{% block content %}
<h1>
{% blocktrans with name=organizer.name %}Organizer: {{ name }}{% endblocktrans %}
<a href="{% url "control:organizer.edit" organizer=organizer.slug %}"
class="btn btn-default">
<span class="fa fa-edit"></span>
{% trans "Edit" %}
</a>
</h1>
<ul class="nav nav-tabs">
<li class="active">
<a href="#tab-events" data-toggle="tab">{% trans "Events" %}</a>
</li>
{% if request.orgaperm.can_change_permissions %}
<li>
<a href="#tab-permissions" data-toggle="tab">{% trans "Team" %}</a>
</li>
{% endif %}
{% for title, content in tabs %}
<li>
<a href="#tab-{{ forloop.counter }}" data-toggle="tab">
{{ title }}
</a>
</li>
{% endfor %}
</ul>
<div class="tab-content organizer-tabs">
<div class="tab-pane active" id="tab-events">
<div class="tab-inner">
{% if events|length == 0 %}
<p>
<em>{% trans "You currently do not have access to any events." %}</em>
</p>
{% else %}
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "Event name" %}</th>
<th>{% trans "Start date" %}</th>
</tr>
</thead>
<tbody>
{% for e in events %}
<tr>
<td>
<strong><a
href="{% url "control:event.index" organizer=e.organizer.slug event=e.slug %}">{{ e.name }}</a></strong>
</td>
<td>{{ e.get_date_from_display }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<a href="{% url "control:events.add" %}" class="btn btn-default">
<span class="fa fa-plus"></span>
{% trans "Create a new event" %}
</a>
</div>
</div>
{% if request.orgaperm.can_change_permissions %}
<div class="tab-pane" id="tab-permissions">
<div class="tab-inner">
<form action="" method="post" class="form-horizontal form-permissions">
{% csrf_token %}
<p>
{% blocktrans trimmed %}
You can use the following list to control who can create new events in the name of
this organizer and who can add more people to this list. This does <strong>not</strong>
control who has access to a particular event. You can control the access to an
event in the "Permissions" section of the event's settings. A user does not need to
be on the list here to get access to an event.
{% endblocktrans %}
</p>
<p>
{% trans "Everyone on this list can control the organizer settings on this page." %}
</p>
{% bootstrap_formset_errors formset %}
{{ formset.management_form }}
<div class="table-responsive">
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>{% trans "User" %}</th>
<th>{% trans "Create events" %}</th>
<th>{% trans "Change permissions" %}</th>
<th>{% trans "Delete" %}</th>
</tr>
</thead>
<tbody>
{% for form in formset %}
<tr>
<td>
{{ form.id }}
{% if form.instance.user %}
{{ form.instance.user }}
{% else %}
{{ form.instance.invite_email }}
<span class="fa fa-envelope-o" data-toggle="tooltip"
title="{% trans "invited, pending response" %}"></span>
{% endif %}
</td>
<td>{{ form.can_create_events }}</td>
<td>{{ form.can_change_permissions }}</td>
<td>{{ form.DELETE }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="9">
<strong>{% trans "Adding a new user" %}</strong><br>
{% blocktrans trimmed %}
To add a new user, you can enter their email address here. If they
already have a pretix account, they will immediately be added to the team.
Otherwise, they will be sent an email with an invitation.
{% endblocktrans %}
</td>
</tr>
<tr>
<td>
<div class="row-fluid">
<div class="col-sm-12">
{% bootstrap_field add_form.user layout='inline' %}
</div>
</div>
</td>
<td>{{ add_form.can_create_events }}</td>
<td>{{ add_form.can_change_permissions }}</td>
</tr>
</tfoot>
</table>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
</div>
</div>
{% endif %}
{% for title, content in tabs %}
<div class="tab-pane" id="tab-{{ forloop.counter }}">
<div class="tab-inner">
{{ content }}
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -11,9 +11,6 @@
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.slug layout="horizontal" %}
{% if form.domain %}
{% bootstrap_field form.domain layout="horizontal" %}
{% endif %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">

View File

@@ -1,84 +0,0 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<form action="" method="post" class="form-horizontal form-permissions">
{% csrf_token %}
<p>
{% blocktrans trimmed %}
You can use the following list to control who can create new events in the name of
this organizer and who can add more people to this list. This does <strong>not</strong>
control who has access to a particular event. You can control the access to an
event in the "Permissions" section of the event's settings. A user does not need to
be on the list here to get access to an event.
{% endblocktrans %}
</p>
<p>
{% trans "Everyone on this list can control the organizer settings on this page." %}
</p>
{% bootstrap_formset_errors formset %}
{{ formset.management_form }}
<div class="table-responsive">
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>{% trans "User" %}</th>
<th>{% trans "Create events" %}</th>
<th>{% trans "Change permissions" %}</th>
<th>{% trans "Delete" %}</th>
</tr>
</thead>
<tbody>
{% for form in formset %}
<tr>
<td>
{{ form.id }}
{% if form.instance.user %}
{{ form.instance.user }}
{% else %}
{{ form.instance.invite_email }}
<span class="fa fa-envelope-o" data-toggle="tooltip"
title="{% trans "invited, pending response" %}"></span>
{% endif %}
</td>
<td>{{ form.can_create_events }}</td>
<td>{{ form.can_change_permissions }}</td>
<td>{{ form.DELETE }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="9">
<strong>{% trans "Adding a new user" %}</strong><br>
{% blocktrans trimmed %}
To add a new user, you can enter their email address here. If they
already have a pretix account, they will immediately be added to the team.
Otherwise, they will be sent an email with an invitation.
{% endblocktrans %}
</td>
</tr>
<tr>
<td>
<div class="row-fluid">
<div class="col-sm-12">
{% bootstrap_field add_form.user layout='inline' %}
</div>
</div>
</td>
<td>{{ add_form.can_create_events }}</td>
<td>{{ add_form.can_change_permissions }}</td>
</tr>
</tfoot>
</table>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -1,13 +0,0 @@
from django import template
register = template.Library()
@register.filter(name='split', delimiter=",")
def split(value, delimiter=","):
return value.split(delimiter)
@register.filter(name="getattr")
def get_attribute(value, key):
return value[key]

View File

@@ -35,7 +35,6 @@ urlpatterns = [
url(r'^organizers/add$', organizer.OrganizerCreate.as_view(), name='organizers.add'),
url(r'^organizer/(?P<organizer>[^/]+)/$', organizer.OrganizerDetail.as_view(), name='organizer'),
url(r'^organizer/(?P<organizer>[^/]+)/edit$', organizer.OrganizerUpdate.as_view(), name='organizer.edit'),
url(r'^organizer/(?P<organizer>[^/]+)/teams$', organizer.OrganizerTeamView.as_view(), name='organizer.teams'),
url(r'^events/$', main.EventList.as_view(), name='events'),
url(r'^events/add$', main.EventWizard.as_view(), name='events.add'),
url(r'^event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include([
@@ -53,7 +52,6 @@ urlpatterns = [
url(r'^settings/tickets/preview/(?P<output>[^/]+)$', event.TicketSettingsPreview.as_view(),
name='event.settings.tickets.preview'),
url(r'^settings/email$', event.MailSettings.as_view(), name='event.settings.mail'),
url(r'^settings/email/preview$', event.MailSettingsPreview.as_view(), name='event.settings.mail.preview'),
url(r'^settings/invoice$', event.InvoiceSettings.as_view(), name='event.settings.invoice'),
url(r'^settings/invoice/preview$', event.InvoicePreview.as_view(), name='event.settings.invoice.preview'),
url(r'^settings/display', event.DisplaySettings.as_view(), name='event.settings.display'),
@@ -62,8 +60,6 @@ urlpatterns = [
url(r'^items/(?P<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
url(r'^items/(?P<item>\d+)/variations$', item.ItemVariations.as_view(),
name='event.item.variations'),
url(r'^items/(?P<item>\d+)/addons', item.ItemAddOns.as_view(),
name='event.item.addons'),
url(r'^items/(?P<item>\d+)/up$', item.item_move_up, name='event.items.up'),
url(r'^items/(?P<item>\d+)/down$', item.item_move_down, name='event.items.down'),
url(r'^items/(?P<item>\d+)/delete$', item.ItemDelete.as_view(), name='event.items.delete'),
@@ -115,8 +111,6 @@ urlpatterns = [
name='event.order.extend'),
url(r'^orders/(?P<code>[0-9A-Z]+)/contact$', orders.OrderContactChange.as_view(),
name='event.order.contact'),
url(r'^orders/(?P<code>[0-9A-Z]+)/locale', orders.OrderLocaleChange.as_view(),
name='event.order.locale'),
url(r'^orders/(?P<code>[0-9A-Z]+)/comment$', orders.OrderComment.as_view(),
name='event.order.comment'),
url(r'^orders/(?P<code>[0-9A-Z]+)/change$', orders.OrderChange.as_view(),
@@ -126,7 +120,6 @@ urlpatterns = [
name='event.invoice.download'),
url(r'^orders/overview/$', orders.OverView.as_view(), name='event.orders.overview'),
url(r'^orders/export/$', orders.ExportView.as_view(), name='event.orders.export'),
url(r'^orders/export/do$', orders.ExportDoView.as_view(), name='event.orders.export.do'),
url(r'^orders/go$', orders.OrderGo.as_view(), name='event.orders.go'),
url(r'^orders/$', orders.OrderList.as_view(), name='event.orders'),
url(r'^waitinglist/$', waitinglist.WaitingListView.as_view(), name='event.orders.waitinglist'),

View File

@@ -1,21 +1,15 @@
import re
from collections import OrderedDict
from datetime import timedelta
from django import forms
from django.conf import settings
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.core.files import File
from django.core.urlresolvers import reverse
from django.db import transaction
from django.forms import modelformset_factory
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.utils import translation
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.views.generic import FormView, ListView
from django.views.generic.base import TemplateView, View
@@ -406,112 +400,6 @@ class MailSettings(EventSettingsFormView):
return self.get(request)
class MailSettingsPreview(EventPermissionRequiredMixin, View):
permission = 'can_change_settings'
# return the origin text if key is missing in dict
class SafeDict(dict):
def __missing__(self, key):
return '{' + key + '}'
@staticmethod
def generate_order_fullname(slug, code):
return '{event}-{code}'.format(event=slug.upper(), code=code)
# create data which depend on locale
def localized_data(self):
return {
'date': date_format(now() + timedelta(days=7), 'SHORT_DATE_FORMAT'),
'expire_date': date_format(now() + timedelta(days=15), 'SHORT_DATE_FORMAT'),
'payment_info': _('{} {} has been transferred to account <9999-9999-9999-9999> at {}').format(
42.23, self.request.event.currency, date_format(now(), 'SHORT_DATETIME_FORMAT'))
}
# create index-language mapping
@cached_property
def supported_locale(self):
locales = {}
for idx, val in enumerate(settings.LANGUAGES):
if val[0] in self.request.event.settings.locales:
locales[str(idx)] = val[0]
return locales
@cached_property
def items(self):
return {
'mail_text_order_placed': ['total', 'currency', 'date', 'invoice_company',
'event', 'paymentinfo', 'url', 'invoice_name'],
'mail_text_order_paid': ['event', 'url', 'invoice_name', 'invoice_company', 'payment_info'],
'mail_text_order_free': ['event', 'url', 'invoice_name', 'invoice_company'],
'mail_text_resend_link': ['event', 'url', 'invoice_name', 'invoice_company'],
'mail_text_resend_all_links': ['event', 'orders'],
'mail_text_order_changed': ['event', 'url', 'invoice_name', 'invoice_company'],
'mail_text_order_expire_warning': ['event', 'url', 'expire_date', 'invoice_name', 'invoice_company'],
'mail_text_waiting_list': ['event', 'url', 'product', 'hours', 'code']
}
@cached_property
def base_data(self):
user_orders = [
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy'},
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd'},
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd'}
]
orders = [' - {} - {}'.format(self.generate_order_fullname(self.request.event.slug, order['code']),
self.generate_order_url(order['code'], order['secret']))
for order in user_orders]
return {
'event': self.request.event.name,
'total': 42.23,
'currency': self.request.event.currency,
'url': self.generate_order_url(user_orders[0]['code'], user_orders[0]['secret']),
'orders': '\n'.join(orders),
'hours': self.request.event.settings.waiting_list_hours,
'product': _('Sample Admission Ticket'),
'code': '68CYU2H6ZTP3WLK5',
'invoice_name': _('John Doe'),
'invoice_company': _('Sample Corporation'),
'paymentinfo': _('Please transfer money to this bank account: 9999-9999-9999-9999')
}
def generate_order_url(self, code, secret):
return build_absolute_uri('presale:event.order', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug,
'order': code,
'secret': secret
})
# get all supported placeholders with dummy values
def placeholders(self, item):
supported = {}
local_data = self.localized_data()
for key in self.items.get(item):
supported[key] = self.base_data.get(key) if key in self.base_data else local_data.get(key)
return self.SafeDict(supported)
def post(self, request, *args, **kwargs):
preview_item = request.POST.get('item', '')
if preview_item not in self.items:
return HttpResponseBadRequest(_('invalid item'))
regex = r"^" + re.escape(preview_item) + r"_(?P<idx>[\d+])$"
msgs = {}
for k, v in request.POST.items():
# only accept allowed fields
matched = re.search(regex, k)
if matched is not None:
idx = matched.group('idx')
if idx in self.supported_locale:
with translation.override(self.supported_locale[idx]):
msgs[self.supported_locale[idx]] = v.format_map(self.placeholders(preview_item))
return JsonResponse({
'item': preview_item,
'msgs': msgs
})
class TicketSettingsPreview(EventPermissionRequiredMixin, View):
permission = 'can_change_settings'

View File

@@ -21,11 +21,9 @@ from pretix.base.models import (
CachedTicket, Item, ItemCategory, ItemVariation, Order, Question,
QuestionAnswer, QuestionOption, Quota, Voucher,
)
from pretix.base.models.items import ItemAddOn
from pretix.control.forms.item import (
CategoryForm, ItemAddOnForm, ItemAddOnsFormSet, ItemCreateForm,
ItemUpdateForm, ItemVariationForm, ItemVariationsFormSet, QuestionForm,
QuestionOptionForm, QuotaForm,
CategoryForm, ItemCreateForm, ItemUpdateForm, ItemVariationForm,
ItemVariationsFormSet, QuestionForm, QuestionOptionForm, QuotaForm,
)
from pretix.control.permissions import (
EventPermissionRequiredMixin, event_permission_required,
@@ -72,7 +70,7 @@ def item_move(request, item, up=True):
if item.position != i:
item.position = i
item.save()
messages.success(request, _('The order of items has been updated.'))
messages.success(request, _('The order of items as been updated.'))
@event_permission_required("can_change_items")
@@ -792,6 +790,15 @@ class ItemCreate(EventPermissionRequiredMixin, CreateView):
@transaction.atomic
def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.'))
if form.cleaned_data['copy_from']:
form.instance.category = form.cleaned_data['copy_from'].category
form.instance.description = form.cleaned_data['copy_from'].description
form.instance.active = form.cleaned_data['copy_from'].active
form.instance.available_from = form.cleaned_data['copy_from'].available_from
form.instance.available_until = form.cleaned_data['copy_from'].available_until
form.instance.require_voucher = form.cleaned_data['copy_from'].require_voucher
form.instance.hide_without_voucher = form.cleaned_data['copy_from'].hide_without_voucher
form.instance.allow_cancel = form.cleaned_data['copy_from'].allow_cancel
ret = super().form_valid(form)
form.instance.log_action('pretix.event.item.added', user=self.request.user, data={
@@ -920,89 +927,6 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
return context
class ItemAddOns(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
permission = 'can_change_items'
template_name = 'pretixcontrol/item/addons.html'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.item = None
@cached_property
def formset(self):
formsetclass = inlineformset_factory(
Item, ItemAddOn,
form=ItemAddOnForm, formset=ItemAddOnsFormSet,
can_order=True, can_delete=True, extra=0
)
return formsetclass(self.request.POST if self.request.method == "POST" else None,
queryset=ItemAddOn.objects.filter(base_item=self.get_object()),
event=self.request.event)
def post(self, request, *args, **kwargs):
with transaction.atomic():
if self.formset.is_valid():
for form in self.formset.deleted_forms:
if not form.instance.pk:
continue
self.get_object().log_action(
'pretix.event.item.addons.removed', user=self.request.user, data={
'category': form.instance.addon_category.pk
}
)
form.instance.delete()
form.instance.pk = None
forms = self.formset.ordered_forms + [
ef for ef in self.formset.extra_forms
if ef not in self.formset.ordered_forms and ef not in self.formset.deleted_forms
]
for i, form in enumerate(forms):
form.instance.base_item = self.get_object()
form.instance.position = i
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.addons.changed' if not created else
'pretix.event.item.addons.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(self, request, *args, **kwargs):
if self.get_object().category and self.get_object().category.is_addon:
messages.error(self.request, _('You cannot add addons to a product that is only available as an add-on '
'itself.'))
return redirect(self.get_previous_url())
return super().get(request, *args, **kwargs)
def get_previous_url(self) -> str:
return reverse('control:event.item', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
'item': self.get_object().id,
})
def get_success_url(self) -> str:
return reverse('control:event.item.addons', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
'item': self.get_object().id,
})
def get_context_data(self, **kwargs) -> dict:
context = super().get_context_data(**kwargs)
context['formset'] = self.formset
return context
class ItemDelete(EventPermissionRequiredMixin, DeleteView):
model = Item
template_name = 'pretixcontrol/item/delete.html'

View File

@@ -22,13 +22,13 @@ class EventList(ListView):
def get_queryset(self):
if self.request.user.is_superuser:
return Event.objects.all().select_related("organizer").prefetch_related(
"_settings_objects", "organizer___settings_objects"
"setting_objects", "organizer__setting_objects"
)
else:
return Event.objects.filter(
permitted__id__exact=self.request.user.pk
).select_related("organizer").prefetch_related(
"_settings_objects", "organizer___settings_objects"
"setting_objects", "organizer__setting_objects"
)

View File

@@ -1,6 +1,5 @@
from datetime import timedelta
from django.conf import settings
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.db.models import Q
@@ -30,9 +29,8 @@ from pretix.base.services.stats import order_overview
from pretix.base.signals import (
register_data_exporters, register_payment_providers,
)
from pretix.base.views.async import AsyncAction
from pretix.control.forms.orders import (
CommentForm, ExporterForm, ExtendForm, OrderContactForm, OrderLocaleForm,
CommentForm, ExporterForm, ExtendForm, OrderContactForm,
OrderPositionChangeForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
@@ -54,7 +52,6 @@ class OrderList(EventPermissionRequiredMixin, ListView):
u = self.request.GET.get("user", "")
qs = qs.filter(
Q(email__icontains=u) | Q(positions__attendee_name__icontains=u)
| Q(positions__attendee_email__icontains=u)
)
if self.request.GET.get("status", "") != "":
s = self.request.GET.get("status", "")
@@ -155,7 +152,6 @@ class OrderDetail(OrderView):
ctx['payment'] = self.payment_provider.order_control_render(self.request, self.object)
ctx['invoices'] = list(self.order.invoices.all().select_related('event'))
ctx['comment_form'] = CommentForm(initial={'comment': self.order.comment})
ctx['display_locale'] = dict(settings.LANGUAGES)[self.object.locale or self.request.event.settings.locale]
return ctx
def get_items(self):
@@ -173,7 +169,6 @@ class OrderDetail(OrderView):
for p in cartpos:
p.has_questions = (
(p.item.admission and self.request.event.settings.attendee_names_asked) or
(p.item.admission and self.request.event.settings.attendee_emails_asked) or
p.item.questions.all()
)
p.cache_answers()
@@ -557,40 +552,6 @@ class OrderContactChange(OrderView):
return self.get(*args, **kwargs)
class OrderLocaleChange(OrderView):
permission = 'can_change_orders'
template_name = 'pretixcontrol/order/change_locale.html'
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['form'] = self.form
return ctx
@cached_property
def form(self):
return OrderLocaleForm(
instance=self.order,
data=self.request.POST if self.request.method == "POST" else None
)
def post(self, *args, **kwargs):
old_locale = self.order.locale
if self.form.is_valid():
self.order.log_action(
'pretix.event.order.locale.changed',
data={
'old_locale': old_locale,
'new_locale': self.form.cleaned_data['locale'],
},
user=self.request.user,
)
self.form.save()
messages.success(self.request, _('The order has been changed.'))
return redirect(self.get_order_url())
return self.get(*args, **kwargs)
class OverView(EventPermissionRequiredMixin, TemplateView):
template_name = 'pretixcontrol/orders/overview.html'
permission = 'can_view_orders'
@@ -625,7 +586,9 @@ class OrderGo(EventPermissionRequiredMixin, View):
return redirect('control:event.orders', event=request.event.slug, organizer=request.event.organizer.slug)
class ExportMixin:
class ExportView(EventPermissionRequiredMixin, TemplateView):
permission = 'can_view_orders'
template_name = 'pretixcontrol/orders/export.html'
@cached_property
def exporters(self):
@@ -641,22 +604,10 @@ class ExportMixin:
exporters.append(ex)
return exporters
class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, View):
permission = 'can_view_orders'
task = export
def get_success_message(self, value):
return None
def get_success_url(self, value):
return reverse('cachedfile.download', kwargs={'id': str(value)})
def get_error_url(self):
return reverse('control:event.orders.export', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug
})
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['exporters'] = self.exporters
return ctx
@cached_property
def exporter(self):
@@ -664,14 +615,13 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, View)
if ex.identifier == self.request.POST.get("exporter"):
return ex
def post(self, request, *args, **kwargs):
def post(self, *args, **kwargs):
if not self.exporter:
messages.error(self.request, _('The selected exporter was not found.'))
return redirect('control:event.orders.export', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug
})
if not self.exporter.form.is_valid():
messages.error(self.request, _('There was a problem processing your input. See below for error details.'))
return self.get(*args, **kwargs)
@@ -680,14 +630,6 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, View)
cf.date = now()
cf.expires = now() + timedelta(days=3)
cf.save()
return self.do(self.request.event.id, str(cf.id), self.exporter.identifier, self.exporter.form.cleaned_data)
class ExportView(EventPermissionRequiredMixin, ExportMixin, TemplateView):
permission = 'can_view_orders'
template_name = 'pretixcontrol/orders/export.html'
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['exporters'] = self.exporters
return ctx
export.apply_async(args=(self.request.event.id, str(cf.id), self.exporter.identifier,
self.exporter.form.cleaned_data))
return redirect(reverse('cachedfile.download', kwargs={'id': str(cf.id)}))

View File

@@ -14,7 +14,7 @@ from pretix.base.models import Organizer, OrganizerPermission, User
from pretix.base.services.mail import SendMailException, mail
from pretix.control.forms.organizer import OrganizerForm, OrganizerUpdateForm
from pretix.control.permissions import OrganizerPermissionRequiredMixin
from pretix.control.signals import nav_organizer
from pretix.control.signals import organizer_edit_tabs
from pretix.helpers.urls import build_absolute_uri
@@ -45,23 +45,7 @@ class OrganizerPermissionCreateForm(OrganizerPermissionForm):
user = forms.EmailField(required=False, label=_('User'))
class OrganizerDetailViewMixin:
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['nav_organizer'] = []
ctx['organizer'] = self.request.organizer
for recv, retv in nav_organizer.send(sender=self.request.organizer, request=self.request,
organizer=self.request.organizer):
ctx['nav_organizer'] += retv
return ctx
def get_object(self, queryset=None) -> Organizer:
return self.request.organizer
class OrganizerDetail(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
class OrganizerDetail(OrganizerPermissionRequiredMixin, DetailView):
model = Organizer
template_name = 'pretixcontrol/organizers/detail.html'
permission = None
@@ -70,18 +54,6 @@ class OrganizerDetail(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
def get_object(self, queryset=None) -> Organizer:
return self.request.organizer
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['events'] = self.request.organizer.events.all()
return ctx
class OrganizerTeamView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
model = Organizer
template_name = 'pretixcontrol/organizers/teams.html'
permission = 'can_change_permissions'
context_object_name = 'organizer'
@cached_property
def formset(self):
fs = modelformset_factory(
@@ -114,6 +86,13 @@ class OrganizerTeamView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMix
ctx = super().get_context_data(**kwargs)
ctx['formset'] = self.formset
ctx['add_form'] = self.add_form
ctx['events'] = self.request.organizer.events.all()
ctx['tabs'] = []
for recv, retv in organizer_edit_tabs.send(sender=self.request.organizer, request=self.request,
organizer=self.request.organizer):
ctx['tabs'].append(retv)
return ctx
def _send_invite(self, instance):
@@ -137,6 +116,12 @@ class OrganizerTeamView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMix
@transaction.atomic
def post(self, *args, **kwargs):
if not self.request.orgaperm.can_change_permissions:
raise PermissionDenied(_("You have no permission to do this."))
if 'formset-TOTAL_FORMS' not in self.request.POST:
return self.get(*args, **kwargs)
if self.formset.is_valid() and self.add_form.is_valid():
if self.add_form.has_changed():
logdata = {
@@ -201,7 +186,7 @@ class OrganizerTeamView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMix
return self.get(*args, **kwargs)
def get_success_url(self) -> str:
return reverse('control:organizer.teams', kwargs={
return reverse('control:organizer', kwargs={
'organizer': self.request.organizer.slug,
})
@@ -220,12 +205,6 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
if self.request.user.is_superuser:
kwargs['domain'] = True
return kwargs
def get_success_url(self) -> str:
return reverse('control:organizer.edit', kwargs={
'organizer': self.request.organizer.slug,

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-02 08:59+0000\n"
"PO-Revision-Date: 2017-04-17 17:10+0200\n"
"POT-Creation-Date: 2017-04-01 13:41+0000\n"
"PO-Revision-Date: 2017-01-01 20:40+0100\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: \n"
"Language: de\n"
@@ -46,9 +46,9 @@ msgstr "Gesamtumsatz"
msgid "Contacting Stripe …"
msgstr "Kontaktiere Stripe …"
#: pretix/static/pretixbase/js/asyncdownload.js:28
#: pretix/static/pretixbase/js/asynctask.js:42
#: pretix/static/pretixbase/js/asynctask.js:94
#: pretix/static/pretixbase/js/asyncdownload.js:27
#: pretix/static/pretixbase/js/asynctask.js:27
#: pretix/static/pretixbase/js/asynctask.js:64
msgid ""
"Your request has been queued on the server and will now be processed. If "
"this takes longer than two minutes, please contact us or go back in your "
@@ -58,19 +58,19 @@ msgstr ""
"dies länger als zwei Minuten dauert, kontaktieren Sie uns bitte oder gehen "
"Sie in Ihrem Browser einen Schritt zurück und versuchen es erneut."
#: pretix/static/pretixbase/js/asyncdownload.js:41
#: pretix/static/pretixbase/js/asynctask.js:62
#: pretix/static/pretixbase/js/asynctask.js:116
#: pretix/static/pretixbase/js/asyncdownload.js:40
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:85
msgid "An error of type {code} occured."
msgstr "Ein Fehler ist aufgetreten. Fehlercode: {code}"
#: pretix/static/pretixbase/js/asyncdownload.js:54
#: pretix/static/pretixbase/js/asynctask.js:137
#: pretix/static/pretixbase/js/asyncdownload.js:53
#: pretix/static/pretixbase/js/asynctask.js:103
msgid "We are processing your request …"
msgstr "Wir verarbeiten Ihre Anfrage …"
#: pretix/static/pretixbase/js/asyncdownload.js:55
#: pretix/static/pretixbase/js/asynctask.js:138
#: pretix/static/pretixbase/js/asyncdownload.js:54
#: pretix/static/pretixbase/js/asynctask.js:104
msgid ""
"We are currently sending your request to the server. If this takes longer "
"than one minute, please check your internet connection and then reload this "
@@ -80,16 +80,7 @@ msgstr ""
"dauert, prüfen Sie bitte Ihre Internetverbindung. Danach können Sie diese "
"Seite neu laden und es erneut versuchen."
#: pretix/static/pretixbase/js/asynctask.js:38
#: pretix/static/pretixbase/js/asynctask.js:90
msgid ""
"Your request has been queued on the server and will now be processed. "
"Depending on the size of your event, this might take up to a few minutes."
msgstr ""
"Ihre Anfrage ist auf dem Server angekommen und wird nun verarbeitet. Je nach "
"Größe der Veranstaltung kann dies einige Minuten dauern."
#: pretix/static/pretixbase/js/asynctask.js:65
#: pretix/static/pretixbase/js/asynctask.js:46
msgid ""
"We currenctly cannot reach the server, but we keep trying. Last error code: "
"{code}"
@@ -97,19 +88,18 @@ msgstr ""
"Wir können den Server aktuell nicht erreichen, versuchen es aber weiter. "
"Letzter Fehlercode: {code}"
#: pretix/static/pretixbase/js/asynctask.js:107
#: pretix/static/pretixcontrol/js/ui/mail.js:20
#: pretix/static/pretixbase/js/asynctask.js:76
msgid "The request took to long. Please try again."
msgstr "Diese Anfrage hat zu lange gedauert. Bitte erneut versuchen."
#: pretix/static/pretixbase/js/asynctask.js:119
#: pretix/static/pretixbase/js/asynctask.js:88
msgid ""
"We currenctly cannot reach the server. Please try again. Error code: {code}"
msgstr ""
"Wir können den Server aktuell nicht erreichen. Bitte versuchen Sie es noch "
"einmal. Fehlercode: {code}"
#: pretix/static/pretixbase/js/asynctask.js:174
#: pretix/static/pretixbase/js/asynctask.js:140
#: pretix/static/pretixcontrol/js/ui/main.js:28
msgid "Close message"
msgstr "Schließen"
@@ -122,25 +112,6 @@ msgstr "Kopiert!"
msgid "Press Ctrl-C to copy!"
msgstr "Drücken Sie Strg+C zum Kopieren!"
#: pretix/static/pretixcontrol/js/ui/mail.js:18
msgid "An error has occurred."
msgstr "Ein Fehler ist aufgetreten."
#: pretix/static/pretixcontrol/js/ui/mail.js:23
msgid "An error of type {code} occurred."
msgstr "Ein Fehler vom Typ {code} ist aufgetreten."
#: pretix/static/pretixcontrol/js/ui/mail.js:25
msgid ""
"We currently cannot reach the server. Please try again. Error code: {code}"
msgstr ""
"Wir können den Server aktuell nicht erreichen. Bitte versuche es noch "
"einmal. Fehlercode: {code}"
#: pretix/static/pretixcontrol/js/ui/mail.js:52
msgid "Generating messages …"
msgstr "Generiere Nachrichten…"
#: pretix/static/pretixcontrol/js/ui/main.js:43
msgid "Unknown error."
msgstr "Unbekannter Fehler."

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-02 08:59+0000\n"
"PO-Revision-Date: 2017-04-17 17:09+0200\n"
"POT-Creation-Date: 2017-04-01 13:41+0000\n"
"PO-Revision-Date: 2017-01-18 09:42+0100\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: \n"
"Language: de\n"
@@ -46,9 +46,9 @@ msgstr "Gesamtumsatz"
msgid "Contacting Stripe …"
msgstr "Kontaktiere Stripe …"
#: pretix/static/pretixbase/js/asyncdownload.js:28
#: pretix/static/pretixbase/js/asynctask.js:42
#: pretix/static/pretixbase/js/asynctask.js:94
#: pretix/static/pretixbase/js/asyncdownload.js:27
#: pretix/static/pretixbase/js/asynctask.js:27
#: pretix/static/pretixbase/js/asynctask.js:64
msgid ""
"Your request has been queued on the server and will now be processed. If "
"this takes longer than two minutes, please contact us or go back in your "
@@ -58,19 +58,19 @@ msgstr ""
"dies länger als zwei Minuten dauert, kontaktiere uns bitte oder gehe in "
"deinem Browser einen Schritt zurück und versuche es erneut."
#: pretix/static/pretixbase/js/asyncdownload.js:41
#: pretix/static/pretixbase/js/asynctask.js:62
#: pretix/static/pretixbase/js/asynctask.js:116
#: pretix/static/pretixbase/js/asyncdownload.js:40
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:85
msgid "An error of type {code} occured."
msgstr "Ein Fehler ist aufgetreten. Fehlercode: {code}"
#: pretix/static/pretixbase/js/asyncdownload.js:54
#: pretix/static/pretixbase/js/asynctask.js:137
#: pretix/static/pretixbase/js/asyncdownload.js:53
#: pretix/static/pretixbase/js/asynctask.js:103
msgid "We are processing your request …"
msgstr "Wir verarbeiten deine Anfrage …"
#: pretix/static/pretixbase/js/asyncdownload.js:55
#: pretix/static/pretixbase/js/asynctask.js:138
#: pretix/static/pretixbase/js/asyncdownload.js:54
#: pretix/static/pretixbase/js/asynctask.js:104
msgid ""
"We are currently sending your request to the server. If this takes longer "
"than one minute, please check your internet connection and then reload this "
@@ -80,16 +80,7 @@ msgstr ""
"dauert, prüfe bitte deine Internetverbindung. Danach kannst du diese Seite "
"neu laden und es erneut versuchen."
#: pretix/static/pretixbase/js/asynctask.js:38
#: pretix/static/pretixbase/js/asynctask.js:90
msgid ""
"Your request has been queued on the server and will now be processed. "
"Depending on the size of your event, this might take up to a few minutes."
msgstr ""
"Deine Anfrage ist auf dem Server angekommen und wird nun verarbeitet. Je "
"nach Größe der Veranstaltung kann dies einige Minuten dauern."
#: pretix/static/pretixbase/js/asynctask.js:65
#: pretix/static/pretixbase/js/asynctask.js:46
msgid ""
"We currenctly cannot reach the server, but we keep trying. Last error code: "
"{code}"
@@ -97,19 +88,18 @@ msgstr ""
"Wir können den Server aktuell nicht erreichen, versuchen es aber weiter. "
"Letzter Fehlercode: {code}"
#: pretix/static/pretixbase/js/asynctask.js:107
#: pretix/static/pretixcontrol/js/ui/mail.js:20
#: pretix/static/pretixbase/js/asynctask.js:76
msgid "The request took to long. Please try again."
msgstr "Diese Anfrage hat zu lange gedauert. Bitte erneut versuchen."
#: pretix/static/pretixbase/js/asynctask.js:119
#: pretix/static/pretixbase/js/asynctask.js:88
msgid ""
"We currenctly cannot reach the server. Please try again. Error code: {code}"
msgstr ""
"Wir können den Server aktuell nicht erreichen. Bitte versuche es noch "
"einmal. Fehlercode: {code}"
#: pretix/static/pretixbase/js/asynctask.js:174
#: pretix/static/pretixbase/js/asynctask.js:140
#: pretix/static/pretixcontrol/js/ui/main.js:28
msgid "Close message"
msgstr "Schließen"
@@ -122,25 +112,6 @@ msgstr "Kopiert!"
msgid "Press Ctrl-C to copy!"
msgstr "Drücke Strg+C zum kopieren!"
#: pretix/static/pretixcontrol/js/ui/mail.js:18
msgid "An error has occurred."
msgstr "Ein Fehler ist aufgetreten."
#: pretix/static/pretixcontrol/js/ui/mail.js:23
msgid "An error of type {code} occurred."
msgstr "Ein Fehler vom Typ {code} ist aufgetreten."
#: pretix/static/pretixcontrol/js/ui/mail.js:25
msgid ""
"We currently cannot reach the server. Please try again. Error code: {code}"
msgstr ""
"Wir können den Server aktuell nicht erreichen. Bitte versuche es noch "
"einmal. Fehlercode: {code}"
#: pretix/static/pretixcontrol/js/ui/mail.js:52
msgid "Generating messages …"
msgstr "Generiere Nachrichten…"
#: pretix/static/pretixcontrol/js/ui/main.js:43
msgid "Unknown error."
msgstr "Unbekannter Fehler."

View File

@@ -3,7 +3,6 @@ import io
from collections import OrderedDict
from django import forms
from django.db.models.functions import Coalesce
from django.utils.translation import ugettext as _, ugettext_lazy
from pretix.base.exporter import BaseExporter
@@ -71,10 +70,10 @@ class CSVCheckinList(BaseCheckinList):
order__event=self.event, item_id__in=form_data['items']
).prefetch_related(
'answers', 'answers__question'
).select_related('order', 'item', 'variation', 'addon_to')
).select_related('order', 'item', 'variation')
if form_data['sort'] == 'name':
qs = qs.order_by(Coalesce('attendee_name', 'addon_to__attendee_name'))
qs = qs.order_by('attendee_name')
elif form_data['sort'] == 'code':
qs = qs.order_by('order__code')
@@ -90,9 +89,6 @@ class CSVCheckinList(BaseCheckinList):
if form_data['secrets']:
headers.append(_('Secret'))
if self.event.settings.attendee_emails_asked:
headers.append(_('E-mail'))
for q in questions:
headers.append(str(q.question))
@@ -101,7 +97,7 @@ class CSVCheckinList(BaseCheckinList):
for op in qs:
row = [
op.order.code,
op.attendee_name or (op.addon_to.attendee_name if op.addon_to else ''),
op.attendee_name,
str(op.item.name) + (" " + str(op.variation.value) if op.variation else ""),
op.price,
]
@@ -109,8 +105,6 @@ class CSVCheckinList(BaseCheckinList):
row.append(_('Yes') if op.order.status == Order.STATUS_PAID else _('No'))
if form_data['secrets']:
row.append(op.secret)
if self.event.settings.attendee_emails_asked:
row.append(op.attendee_email or (op.addon_to.attendee_name if op.addon_to else ''))
acache = {}
for a in op.answers.all():
acache[a.question_id] = str(a)

View File

@@ -39,10 +39,6 @@ class Paypal(BasePaymentProvider):
('client_id',
forms.CharField(
label=_('Client ID'),
help_text=_('<a target="_blank" href="{docs_url}">{text}</a>').format(
text=_('Click here for a tutorial on how to obtain the required keys'),
docs_url='https://docs.pretix.eu/en/latest/user/payments/paypal.html'
)
)),
('secret',
forms.CharField(

View File

@@ -74,7 +74,7 @@ class ApiRedeemView(ApiView):
try:
with transaction.atomic():
created = False
op = OrderPosition.objects.select_related('item', 'variation', 'order', 'addon_to').get(
op = OrderPosition.objects.select_related('item', 'variation', 'order').get(
order__event=self.event, secret=secret
)
if op.order.status == Order.STATUS_PAID:
@@ -105,7 +105,7 @@ class ApiRedeemView(ApiView):
'order': op.order.code,
'item': str(op.item),
'variation': str(op.variation) if op.variation else None,
'attendee_name': op.attendee_name or (op.addon_to.attendee_name if op.addon_to else ''),
'attendee_name': op.attendee_name
}
except OrderPosition.DoesNotExist:
@@ -136,7 +136,7 @@ class ApiSearchView(ApiView):
'order': op.order.code,
'item': str(op.item),
'variation': str(op.variation) if op.variation else None,
'attendee_name': op.attendee_name or (op.addon_to.attendee_name if op.addon_to else ''),
'attendee_name': op.attendee_name,
'redeemed': bool(op.checkins.all()),
'paid': op.order.status == Order.STATUS_PAID,
} for op in ops
@@ -201,7 +201,7 @@ class ApiStatusView(ApiView):
}
response['items'] = []
for item in self.event.items.order_by('pk').prefetch_related('variations'):
for item in self.event.items.prefetch_related('variations'):
i = {
'id': item.pk,
'name': str(item),

View File

@@ -2,7 +2,6 @@ from django import forms
from django.utils.translation import ugettext_lazy as _
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from pretix.base.forms import PlaceholderValidator
from pretix.base.models import Order
@@ -22,9 +21,7 @@ class MailForm(forms.Form):
widget=I18nTextarea, required=True,
locales=event.settings.get('locales'),
help_text=_("Available placeholders: {due_date}, {event}, {order}, {order_date}, {order_url}, "
"{invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{due_date}', '{event}', '{order}', '{order_date}', '{order_url}',
'{invoice_name}', '{invoice_company}'])]
"{invoice_name}, {invoice_company}")
)
choices = list(Order.STATUS_CHOICE)
if not event.settings.get('payment_term_expire_automatically', as_type=bool):

View File

@@ -13,15 +13,22 @@
{% if request.method == "POST" %}
<fieldset>
<legend>{% trans "E-mail preview" %}</legend>
<div class="tab-pane mail-preview-group">
{% for locale, segments in output.items %}
<pre lang="{{ locale }}" class="mail-preview">{% spaceless %}
{% for locale, segments in output.items %}
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<strong>{% blocktrans %}For locale: {{ locale }}{% endblocktrans %}</strong>
</h4>
</div>
<div class="panel-body">
{% for value in segments %}
{{ value|linebreaksbr }}
<p>
{{ value|linebreaksbr }}
</p>
{% endfor %}
{% endspaceless %}</pre>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</fieldset>
{% endif %}
<div class="form-group submit-group">

View File

@@ -65,14 +65,14 @@ class IndexView(EventPermissionRequiredMixin, ChartContainingView, TemplateView)
for p in (OrderPosition.objects
.filter(order__event=self.request.event)
.values('item')
.annotate(cnt=Count('id')).order_by())
.annotate(cnt=Count('id')))
}
num_paid = {
p['item']: p['cnt']
for p in (OrderPosition.objects
.filter(order__event=self.request.event, order__status=Order.STATUS_PAID)
.values('item')
.annotate(cnt=Count('id')).order_by())
.annotate(cnt=Count('id')))
}
item_names = {
i.id: str(i.name)

View File

@@ -28,10 +28,6 @@ class Stripe(BasePaymentProvider):
('secret_key',
forms.CharField(
label=_('Secret key'),
help_text=_('<a target="_blank" href="{docs_url}">{text}</a>').format(
text=_('Click here for a tutorial on how to obtain the required keys'),
docs_url='https://docs.pretix.eu/en/latest/user/payments/stripe.html'
)
)),
('publishable_key',
forms.CharField(

View File

@@ -79,7 +79,7 @@ class PdfTicketOutput(BaseTicketOutput):
p.drawString(code_x * units.mm, code_y * units.mm, op.secret)
attendee_s = self.settings.get('attendee_s', default=0, as_type=float)
if attendee_s and op.attendee_name:
if code_s and op.attendee_name:
p.setFont("Helvetica", attendee_s)
attendee_x = self.settings.get('attendee_x', default=15, as_type=float)
attendee_y = self.settings.get('attendee_y', default=90, as_type=float)
@@ -91,8 +91,6 @@ class PdfTicketOutput(BaseTicketOutput):
buffer = BytesIO()
p = self._create_canvas(buffer)
for op in order.positions.all():
if op.addon_to_id and not self.event.settings.ticket_download_addons:
continue
self._draw_page(p, op, order)
p.save()
outbuffer = self._render_with_background(buffer)

View File

@@ -11,24 +11,19 @@ from django.views.generic.base import TemplateResponseMixin
from pretix.base.models import Order
from pretix.base.models.orders import InvoiceAddress
from pretix.base.services.cart import set_cart_addons
from pretix.base.services.orders import perform_order
from pretix.base.signals import register_payment_providers
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.forms.checkout import (
AddOnsForm, ContactForm, InvoiceAddressForm,
)
from pretix.presale.forms.checkout import ContactForm, InvoiceAddressForm
from pretix.presale.signals import (
checkout_confirm_messages, checkout_flow_steps, order_meta_from_request,
)
from pretix.presale.views import CartMixin, get_cart, get_cart_total
from pretix.presale.views import CartMixin, get_cart_total
from pretix.presale.views.async import AsyncAction
from pretix.presale.views.questions import QuestionsViewMixin
class BaseCheckoutFlowStep:
requires_valid_cart = True
def __init__(self, event):
self.event = event
self.request = None
@@ -131,120 +126,6 @@ class TemplateFlowStep(TemplateResponseMixin, BaseCheckoutFlowStep):
raise NotImplementedError()
class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
priority = 40
identifier = "addons"
template_name = "pretixpresale/event/checkout_addons.html"
task = set_cart_addons
known_errortypes = ['CartError']
requires_valid_cart = False
def is_applicable(self, request):
return get_cart(request).filter(item__addons__isnull=False).exists()
def is_completed(self, request, warn=False):
for cartpos in get_cart(request).filter(addon_to__isnull=True).prefetch_related(
'item__addons', 'item__addons__addon_category', 'addons', 'addons__item'
):
a = cartpos.addons.all()
for iao in cartpos.item.addons.all():
found = len([1 for p in a if p.item.category_id == iao.addon_category_id])
if found < iao.min_count or found > iao.max_count:
return False
return True
@cached_property
def forms(self):
"""
A list of forms with one form for each cart position that can have add-ons.
All forms have a custom prefix, so that they can all be submitted at once.
"""
formset = []
quota_cache = {}
item_cache = {}
for cartpos in get_cart(self.request).filter(addon_to__isnull=True).prefetch_related(
'item__addons', 'item__addons__addon_category', 'addons', 'addons__variation'
):
current_addon_products = {
a.item_id: a.variation_id for a in cartpos.addons.all()
}
formsetentry = {
'cartpos': cartpos,
'item': cartpos.item,
'variation': cartpos.variation,
'categories': []
}
for iao in cartpos.item.addons.all():
category = {
'category': iao.addon_category,
'min_count': iao.min_count,
'max_count': iao.max_count,
'form': AddOnsForm(
event=self.request.event,
prefix='{}_{}'.format(cartpos.pk, iao.addon_category.pk),
category=iao.addon_category,
initial=current_addon_products,
data=(self.request.POST if self.request.method == 'POST' else None),
quota_cache=quota_cache,
item_cache=item_cache
)
}
if len(category['form'].fields) > 0:
formsetentry['categories'].append(category)
formset.append(formsetentry)
return formset
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['forms'] = self.forms
return ctx
def get_success_message(self, value):
return None
def get_success_url(self, value):
return self.get_next_url(self.request)
def get_error_url(self):
return self.get_step_url()
def get(self, request):
self.request = request
if 'async_id' in request.GET and settings.HAS_CELERY:
return self.get_result(request)
return TemplateFlowStep.get(self, request)
def post(self, request, *args, **kwargs):
self.request = request
is_valid = True
data = []
for f in self.forms:
for c in f['categories']:
is_valid = is_valid and c['form'].is_valid()
if c['form'].is_valid():
for k, v in c['form'].cleaned_data.items():
itemid = int(k[5:])
if v is True:
data.append({
'addon_to': f['cartpos'].pk,
'item': itemid,
'variation': None
})
elif v:
data.append({
'addon_to': f['cartpos'].pk,
'item': itemid,
'variation': int(v)
})
if not is_valid:
return self.get(request, *args, **kwargs)
return self.do(self.request.event.id, data, self.request.session.session_key)
class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
priority = 50
identifier = "questions"
@@ -321,16 +202,11 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
if warn:
messages.warning(request, _('Please fill in answers to all required questions.'))
return False
if cp.item.admission and self.request.event.settings.get('attendee_emails_required', as_type=bool) \
and cp.attendee_email is None:
if warn:
messages.warning(request, _('Please fill in answers to all required questions.'))
return False
return True
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['formgroups'] = self.formdict.items()
ctx['forms'] = self.forms
ctx['contact_form'] = self.contact_form
ctx['invoice_form'] = self.invoice_form
return ctx
@@ -514,7 +390,6 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
DEFAULT_FLOW = (
AddOnsStep,
QuestionsStep,
PaymentStep,
ConfirmStep

View File

@@ -2,25 +2,14 @@ from decimal import Decimal
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Count, Prefetch, Q
from django.forms.widgets import RadioChoiceInput, RadioFieldRenderer
from django.utils.encoding import force_text
from django.utils.formats import number_format
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import ItemVariation, Question
from pretix.base.models import Question
from pretix.base.models.orders import InvoiceAddress
from pretix.base.templatetags.rich_text import rich_text
class ContactForm(forms.Form):
email = forms.EmailField(label=_('E-mail'),
help_text=_('Make sure to enter a valid email address. We will send you an order '
'confirmation including a link that you need in case you want to make '
'modifications to your order or download your ticket later.'))
email = forms.EmailField(label=_('E-mail'))
class InvoiceAddressForm(forms.ModelForm):
@@ -64,8 +53,8 @@ class QuestionsForm(forms.Form):
:param cartpos: The cart position the form should be for
:param event: The event this belongs to
"""
cartpos = self.cartpos = kwargs.pop('cartpos', None)
orderpos = self.orderpos = kwargs.pop('orderpos', None)
cartpos = kwargs.pop('cartpos', None)
orderpos = kwargs.pop('orderpos', None)
item = cartpos.item if cartpos else orderpos.item
questions = list(item.questions.all())
event = kwargs.pop('event')
@@ -78,12 +67,6 @@ class QuestionsForm(forms.Form):
label=_('Attendee name'),
initial=(cartpos.attendee_name if cartpos else orderpos.attendee_name)
)
if item.admission and event.settings.attendee_emails_asked:
self.fields['attendee_email'] = forms.EmailField(
required=event.settings.attendee_emails_required,
label=_('Attendee email'),
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email)
)
for q in questions:
# Do we already have an answer? Provide it as the initial value
@@ -149,171 +132,3 @@ class QuestionsForm(forms.Form):
# Cache the answer object for later use
field.answer = answers[0]
self.fields['question_%s' % q.id] = field
# The following will get totally different once Django 1.11 is integrated
class AddOnVariationSelectInput(RadioChoiceInput):
def __init__(self, name, value, attrs, choice, index):
super().__init__(name, value, attrs, choice, index)
self.description = force_text(choice[2])
def render(self, name=None, value=None, attrs=None):
if self.id_for_label:
label_for = format_html(' for="{}"', self.id_for_label)
else:
label_for = ''
attrs = dict(self.attrs, **attrs) if attrs else self.attrs
if self.description:
return format_html(
'<label{}>{} {}</label> <span class="fa fa-info-circle toggle-variation-description"></span>'
'<div class="variation-description addon-variation-description">{}</div>',
label_for, self.tag(attrs), self.choice_label,
rich_text(str(self.description))
)
else:
return format_html(
'<label{}>{} {}</label>',
label_for, self.tag(attrs), self.choice_label,
)
class AddOnVariationSelectRenderer(RadioFieldRenderer):
choice_input_class = AddOnVariationSelectInput
def render(self):
id_ = self.attrs.get('id')
output = []
for i, choice in enumerate(self.choices):
w = self.choice_input_class(self.name, self.value, self.attrs.copy(), choice, i)
output.append(format_html(self.inner_html, choice_value=force_text(w), sub_widgets=''))
return format_html(
self.outer_html,
id_attr=format_html(' id="{}"', id_) if id_ else '',
content=mark_safe('\n'.join(output)),
)
class AddOnVariationSelect(forms.RadioSelect):
renderer = AddOnVariationSelectRenderer
class AddOnVariationField(forms.ChoiceField):
def valid_value(self, value):
text_value = force_text(value)
for k, v, d in self.choices:
if value == k or text_value == force_text(k):
return True
return False
class AddOnsForm(forms.Form):
"""
This form class is responsible for selecting add-ons to a product in the cart.
"""
def _label(self, event, item_or_variation, avail):
if isinstance(item_or_variation, ItemVariation):
variation = item_or_variation
item = item_or_variation.item
price = variation.price
price_net = variation.net_price
label = variation.value
else:
item = item_or_variation
price = item.default_price
price_net = item.default_price_net
label = item.name
if not price:
n = '{name}'.format(
name=label
)
elif not item.tax_rate:
n = _('{name} (+ {currency} {price})').format(
name=label, currency=event.currency, price=number_format(price)
)
elif event.settings.display_net_prices:
n = _('{name} (+ {currency} {price} plus {taxes}% taxes)').format(
name=label, currency=event.currency, price=number_format(price_net),
taxes=number_format(item.tax_rate)
)
else:
n = _('{name} (+ {currency} {price} incl. {taxes}% taxes)').format(
name=label, currency=event.currency, price=number_format(price),
taxes=number_format(item.tax_rate)
)
if avail[0] < 20:
n += ' {}'.format(_('SOLD OUT'))
elif avail[0] < 100:
n += ' {}'.format(_('Currently unavailable'))
return n
def __init__(self, *args, **kwargs):
"""
Takes additional keyword arguments:
:param category: The category to choose from
:param event: The event this belongs to
:param initial: The current set of add-ons
:param quota_cache: A shared dictionary for quota caching
:param item_cache: A shared dictionary for item/category caching
"""
category = kwargs.pop('category')
event = kwargs.pop('event')
current_addons = kwargs.pop('initial')
quota_cache = kwargs.pop('quota_cache')
item_cache = kwargs.pop('item_cache')
super().__init__(*args, **kwargs)
if category.pk not in item_cache:
# Get all items to possibly show
items = category.items.filter(
Q(active=True)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& Q(hide_without_voucher=False)
).prefetch_related(
'variations__quotas', # for .availability()
Prefetch('quotas', queryset=event.quotas.all()),
Prefetch('variations', to_attr='available_variations',
queryset=ItemVariation.objects.filter(active=True, quotas__isnull=False).distinct()),
).annotate(
quotac=Count('quotas'),
has_variations=Count('variations')
).filter(
quotac__gt=0
).order_by('category__position', 'category_id', 'position', 'name')
item_cache[category.pk] = items
else:
items = item_cache[category.pk]
for i in items:
if i.has_variations:
choices = [('', _('no selection'), '')]
for v in i.available_variations:
cached_availability = v.check_quotas(_cache=quota_cache)
choices.append((v.pk, self._label(event, v, cached_availability), v.description))
field = AddOnVariationField(
choices=choices,
label=i.name,
required=False,
widget=AddOnVariationSelect,
help_text=rich_text(str(i.description)),
initial=current_addons.get(i.pk),
)
else:
cached_availability = i.check_quotas(_cache=quota_cache)
field = forms.BooleanField(
label=self._label(event, i, cached_availability),
required=False,
initial=i.pk in current_addons,
help_text=rich_text(str(i.description)),
)
self.fields['item_%s' % i.pk] = field

View File

@@ -1,6 +1,6 @@
from django.core.management.base import BaseCommand
from pretix.base.models import Event_SettingsStore
from pretix.base.models import EventSetting
from ...style import regenerate_css
@@ -9,5 +9,5 @@ class Command(BaseCommand):
help = "Re-generate all custom stylesheets"
def handle(self, *args, **options):
for es in Event_SettingsStore.objects.filter(key="presale_css_file"):
for es in EventSetting.objects.filter(key="presale_css_file"):
regenerate_css.apply_async(args=(es.object_id,))

View File

@@ -1,74 +0,0 @@
{% extends "pretixpresale/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Checkout" %}{% endblock %}
{% block content %}
<h2>{% trans "Checkout" %}</h2>
<p>
{% trans "For some of the products in your cart, you can choose additional options before you continue." %}
</p>
<form class="form-horizontal" method="post" data-asynctask>
{% csrf_token %}
<div class="panel-group" id="questions_group">
{% for form in forms %}
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#cp{{ form.cartpos.pk }}">
<strong>{{ form.item.name }}</strong>
{% if form.variation %}
{{ form.variation }}
{% endif %}
</a>
</h4>
</div>
<div id="cp{{ form.cartpos.pk }}"
class="panel-collapse collapsed in">
<div class="panel-body">
{% for c in form.categories %}
<fieldset>
<legend>{{ c.category.name }}</legend>
<p>
{% if c.min_count == c.max_count %}
{% blocktrans trimmed with min_count=c.min_count %}
You need to choose {{ min_count }} options from this category.
{% endblocktrans %}
{% elif c.min_count == 0 %}
{% blocktrans trimmed with max_count=c.max_count %}
You can to choose up to {{ max_count }} options from this category.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with min_count=c.min_count max_count=c.max_count %}
You can choose between {{ min_count }} and {{ max_count }} options from
this category.
{% endblocktrans %}
{% endif %}
</p>
{% bootstrap_form c.form layout="horizontal" %}
</fieldset>
{% empty %}
<em>
{% trans "There are no add-ons available for this product." %}
</em>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
<div class="row checkout-button-row">
<div class="col-md-4">
<a class="btn btn-block btn-default btn-lg"
href="{{ prev_url }}">
{% trans "Go back" %}
</a>
</div>
<div class="col-md-4 col-md-offset-4">
<button class="btn btn-block btn-primary btn-lg" type="submit">
{% trans "Continue" %}
</button>
</div>
<div class="clearfix"></div>
</div>
</form>
{% endblock %}

View File

@@ -42,14 +42,14 @@
</div>
{% endif %}
{% for pos, forms in formgroups %}
{% for form in forms %}
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#cp{{ pos.id }}">
<strong>{{ pos.item.name }}</strong>
{% if pos.variation %}
{{ pos.variation }}
<a data-toggle="collapse" href="#cp{{ form.pos.id }}">
<strong>{{ form.pos.item.name }}</strong>
{% if form.pos.variation %}
{{ form.pos.variation }}
{% endif %}
</a>
{% if forloop.counter > 1 %}
@@ -59,16 +59,10 @@
{% endif %}
</h4>
</div>
<div id="cp{{ pos.id }}"
<div id="cp{{ form.pos.id }}"
class="panel-collapse collapsed in">
<div class="panel-body" data-idx="{{ forloop.counter0 }}">
{% for form in forms %}
{% if form.pos.item != pos.item %}
{# Add-Ons #}
<legend>+ {{ form.pos.item }}</legend>
{% endif %}
{% bootstrap_form form layout="horizontal" %}
{% endfor %}
{% bootstrap_form form layout="horizontal" %}
</div>
</div>
</div>

View File

@@ -3,9 +3,6 @@
{% for line in cart.positions %}
<div class="row cart-row {% if download %}has-downloads{% endif %}">
<div class="product">
{% if line.addon_to %}
<span class="addon-signifier">+</span>
{% endif %}
<strong>{{ line.item.name }}</strong>
{% if line.variation %}
{{ line.variation }}
@@ -21,10 +18,6 @@
<dt>{% trans "Attendee name" %}</dt>
<dd>{% if line.attendee_name %}{{ line.attendee_name }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}</dd>
{% endif %}
{% if line.item.admission and event.settings.attendee_emails_asked%}
<dt>{% trans "Attendee email" %}</dt>
<dd>{% if line.attendee_email %}{{ line.attendee_email }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}</dd>
{% endif %}
{% for q in line.questions %}
<dt>{{ q.question }}</dt>
<dd>{% if q.answer %}{{ q.answer|linebreaksbr }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}</dd>
@@ -35,23 +28,12 @@
{% if download %}
<div class="download-desktop">
{% if not line.addon_to or event.settings.ticket_download_addons %}
{% for b in download_buttons %}
<a href="{% eventurl event "presale:event.order.download" secret=order.secret order=order.code output=b.identifier position=line.id %}"
class="btn btn-default btn-sm" data-asyncdownload>
<span class="fa fa-download"></span> {{ b.text }}
</a>
{% endfor %}
{% endif %}
</div>
{% elif line.addon_to %}
<div class="count">&nbsp;</div>
<div class="singleprice price">
{% if event.settings.display_net_prices %}
{{ event.currency }} {{ line.net_price|floatformat:2 }}
{% else %}
{{ event.currency }} {{ line.price|floatformat:2 }}
{% endif %}
{% for b in download_buttons %}
<a href="{% eventurl event "presale:event.order.download" secret=order.secret order=order.code output=b.identifier position=line.id %}"
class="btn btn-default btn-sm" data-asyncdownload>
<span class="fa fa-download"></span> {{ b.text }}
</a>
{% endfor %}
</div>
{% else %}
<div class="count">
@@ -59,10 +41,19 @@
<form action="{% eventurl event "presale:event.cart.remove" %}"
method="post" data-asynctask>
{% csrf_token %}
<input type="hidden" name="id" value="{{ line.id }}" />
<button class="btn btn-mini btn-link">
<i class="fa fa-minus"></i>
</button>
{% if line.variation %}
<input type="hidden" name="variation_{{ line.item.id }}_{{ line.variation.id }}"
value="1" />
<input type="hidden" name="price_{{ line.item.id }}_{{ line.variation.id }}"
value="{{ line.price }}" />
{% else %}
<input type="hidden" name="item_{{ line.item.id }}"
value="1" />
<input type="hidden" name="price_{{ line.item.id }}"
value="{{ line.price }}" />
{% endif %}
<button class="btn btn-mini btn-link"><i class="fa fa-minus"></i></button>
</form>
{% endif %}
{{ line.count }}
@@ -81,9 +72,7 @@
<input type="hidden" name="price_{{ line.item.id }}"
value="{% if event.settings.display_net_prices %}{{ line.net_price }}{% else %}{{ line.price }}{% endif %}" />
{% endif %}
<button class="btn btn-mini btn-link">
<i class="fa fa-plus"></i>
</button>
<button class="btn btn-mini btn-link"><i class="fa fa-plus"></i></button>
</form>
{% endif %}
</div>
@@ -114,14 +103,12 @@
</div>
{% if download %}
<div class="download-mobile">
{% if not line.addon_to or event.settings.ticket_download_addons %}
{% for b in download_buttons %}
<a href="{% eventurl event "presale:event.order.download" secret=order.secret order=order.code output=b.identifier position=line.id %}"
class="btn btn-default btn-sm" data-asyncdownload>
<span class="fa fa-download"></span> {{ b.text }}
</a>
{% endfor %}
{% endif %}
{% for b in download_buttons %}
<a href="{% eventurl event "presale:event.order.download" secret=order.secret order=order.code output=b.identifier position=line.id %}"
class="btn btn-default btn-sm" data-asyncdownload>
<span class="fa fa-download"></span> {{ b.text }}
</a>
{% endfor %}
</div>
{% endif %}
<div class="clearfix"></div>

View File

@@ -26,20 +26,24 @@
</em>
<div class="row checkout-button-row">
<div class="col-md-4 col-xs-12">
<form method="post" data-asynctask action="{% eventurl request.event "presale:event.cart.clear" %}">
<form method="post" data-asynctask action="{% eventurl request.event "presale:event.cart.remove" %}" >
{% csrf_token %}
{% for line in cart.positions %}
{% if line.variation %}
<input type="hidden" name="variation_{{ line.item.id }}_{{ line.variation.id }}" value="{{ line.count }}" />
<input type="hidden" name="price_{{ line.item.id }}_{{ line.variation.id }}" value="{{ line.price }}" />
{% else %}
<input type="hidden" name="item_{{ line.item.id }}" value="{{ line.count }}" />
<input type="hidden" name="price_{{ line.item.id }}" value="{{ line.price }}" />
{% endif %}
{% endfor %}
<button class="btn btn-block btn-default btn-lg" type="submit"><i class="fa fa-close"></i> {% trans "Empty cart" %}</button>
</form>
</div>
<div class="col-md-4 col-md-offset-4 col-xs-12">
<a class="btn btn-block btn-primary btn-lg"
href="{% eventurl request.event "presale:event.checkout.start" %}">
{% if has_addon_choices %}
<i class="fa fa-shopping-cart"></i> {% trans "Continue" %}
</a>
{% else %}
<i class="fa fa-shopping-cart"></i> {% trans "Proceed with checkout" %}
{% endif %}
<i class="fa fa-shopping-cart"></i> {% trans "Proceed with checkout" %}
</a>
</div>
<div class="clearfix"></div>
@@ -106,17 +110,7 @@
<a href="#" data-toggle="variations">
<strong>{{ item.name }}</strong>
</a>
{% if item.description %}
<div class="product-description">
{{ item.description|localize|rich_text }}
</div>
{% endif %}
{% if item.min_per_order %}
<p><small>
{% blocktrans trimmed with num=item.min_per_order %}
minimum amount to order: {{ num }}
{% endblocktrans %}
</small></p>
{% if item.description %}<p>{{ item.description|localize|rich_text }}</p>
{% endif %}
</div>
<div class="col-md-2 col-xs-6 price">
@@ -142,11 +136,6 @@
<div class="row-fluid product-row variation">
<div class="col-md-8 col-xs-12">
{{ var }}
{% if var.description %}
<div class="variation-description">
{{ var.description|localize|rich_text }}
</div>
{% endif %}
{% if event.settings.show_quota_left %}
{% include "pretixpresale/event/fragment_quota_left.html" with avail=var.cached_availability %}
{% endif %}
@@ -214,20 +203,10 @@
{% endif %}
<strong>{{ item.name }}</strong>
{% if item.description %}
<div class="product-description">
{{ item.description|localize|rich_text }}
</div>
{% endif %}
<p class="description">{{ item.description|localize|rich_text }}</p>{% endif %}
{% if event.settings.show_quota_left %}
{% include "pretixpresale/event/fragment_quota_left.html" with avail=item.cached_availability %}
{% endif %}
{% if item.min_per_order %}
<p><small>
{% blocktrans trimmed with num=item.min_per_order %}
minimum amount to order: {{ num }}
{% endblocktrans %}
</small></p>
{% endif %}
</div>
<div class="col-md-2 col-xs-6 price">
{% if item.free_price %}

View File

@@ -36,28 +36,23 @@
</div>
</div>
{% endif %}
{% for pos, forms in formgroups %}
{% for form in forms %}
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#cp{{ pos.id }}">
<strong>{{ pos.item.name }}</strong>
{% if pos.variation %}
{{ pos.variation }}
<a data-toggle="collapse" href="#cp{{ form.pos.id }}"
data-parent="#questions_accordion">
<strong>{{ form.pos.item }}</strong>
{% if form.pos.variation %}
{{ form.pos.variation }}
{% endif %}
</a>
</h4>
</div>
<div id="cp{{ pos.id }}"
class="panel-collapse collapsed in">
<div id="cp{{ form.pos.id }}"
class="panel-collapse collapsed {% if forloop.counter0 == 0 %}in{% endif %}">
<div class="panel-body">
{% for form in forms %}
{% if form.pos.item != pos.item %}
{# Add-Ons #}
<legend>+ {{ form.pos.item }}</legend>
{% endif %}
{% bootstrap_form form layout="horizontal" %}
{% endfor %}
{% bootstrap_form form layout="horizontal" %}
</div>
</div>
</div>

View File

@@ -37,10 +37,7 @@
{% endif %}
<strong>{{ item.name }}</strong>
{% if item.description %}
<div class="product-description">
{{ item.description|localize|rich_text }}
</div>
{% endif %}
<p>{{ item.description|localize|rich_text }}</p>{% endif %}
</div>
<div class="col-md-2 col-xs-6 price">
{% if item.min_price != item.max_price or item.free_price %}
@@ -60,11 +57,6 @@
<div class="row-fluid product-row variation">
<div class="col-md-8 col-xs-12">
{{ var }}
{% if var.description %}
<div class="variation-description">
{{ var.description|localize|rich_text }}
</div>
{% endif %}
</div>
<div class="col-md-2 col-xs-6 price">
{% if item.free_price %}
@@ -124,10 +116,7 @@
{% endif %}
<strong>{{ item.name }}</strong>
{% if item.description %}
<div class="product-description">
{{ item.description|localize|rich_text }}
</div>
{% endif %}
<p class="description">{{ item.description|localize|rich_text }}</p>{% endif %}
</div>
<div class="col-md-2 col-xs-6 price">
{% if item.free_price %}

View File

@@ -15,10 +15,9 @@ import pretix.presale.views.waiting
event_patterns = [
url(r'^cart/add$', pretix.presale.views.cart.CartAdd.as_view(), name='event.cart.add'),
url(r'^cart/remove$', pretix.presale.views.cart.CartRemove.as_view(), name='event.cart.remove'),
url(r'^cart/clear$', pretix.presale.views.cart.CartClear.as_view(), name='event.cart.clear'),
url(r'^waitinglist', pretix.presale.views.waiting.WaitingView.as_view(), name='event.waitinglist'),
url(r'^checkout/start$', pretix.presale.views.checkout.CheckoutView.as_view(), name='event.checkout.start'),
url(r'^redeem/?$', pretix.presale.views.cart.RedeemView.as_view(),
url(r'^redeem$', pretix.presale.views.cart.RedeemView.as_view(),
name='event.redeem'),
url(r'^checkout/(?P<step>[^/]+)/$', pretix.presale.views.checkout.CheckoutView.as_view(),
name='event.checkout'),

View File

@@ -29,45 +29,30 @@ class CartMixin:
cartpos = queryset.order_by(
'item', 'variation'
).select_related(
'item', 'variation', 'addon_to'
'item', 'variation'
).prefetch_related(
*prefetch
)
else:
cartpos = self.positions
lcp = list(cartpos)
has_addons = {cp.addon_to.pk for cp in lcp if cp.addon_to}
# Group items of the same variation
# We do this by list manipulations instead of a GROUP BY query, as
# Django is unable to join related models in a .values() query
def keyfunc(pos):
if isinstance(pos, OrderPosition):
if pos.addon_to:
i = pos.addon_to.positionid
else:
i = pos.positionid
i = pos.positionid
else:
if pos.addon_to:
i = pos.addon_to.pk
else:
i = pos.pk
has_attendee_data = pos.item.admission and (
self.request.event.settings.attendee_names_asked
or self.request.event.settings.attendee_emails_asked
)
addon_penalty = 1 if pos.addon_to else 0
if downloads or pos.pk in has_addons or pos.addon_to:
return i, addon_penalty, pos.pk, 0, 0, 0, 0,
if answers and (has_attendee_data or pos.item.questions.all()):
return i, addon_penalty, pos.pk, 0, 0, 0, 0,
return 0, addon_penalty, 0, pos.item_id, pos.variation_id, pos.price, (pos.voucher_id or 0)
i = pos.pk
if downloads:
return i, pos.pk, 0, 0, 0, 0,
if answers and ((pos.item.admission and self.request.event.settings.attendee_names_asked)
or pos.item.questions.all()):
return i, pos.pk, 0, 0, 0, 0,
return 0, 0, pos.item_id, pos.variation_id, pos.price, (pos.voucher_id or 0)
positions = []
for k, g in groupby(sorted(lcp, key=keyfunc), key=keyfunc):
for k, g in groupby(sorted(list(cartpos), key=keyfunc), key=keyfunc):
g = list(g)
group = g[0]
group.count = len(g)
@@ -109,7 +94,7 @@ class CartMixin:
'payment_fee_tax_rate': payment_fee_tax_rate,
'answers': answers,
'minutes_left': minutes_left,
'first_expiry': first_expiry,
'first_expiry': first_expiry
}
def get_payment_fee(self, total):

View File

@@ -10,7 +10,7 @@ from django.views.generic import TemplateView, View
from pretix.base.decimal import round_decimal
from pretix.base.models import CartPosition, Quota, Voucher
from pretix.base.services.cart import (
CartError, add_items_to_cart, clear_cart, remove_cart_position,
CartError, add_items_to_cart, remove_items_from_cart,
)
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.views import EventViewMixin
@@ -105,18 +105,19 @@ class CartActionMixin:
class CartRemove(EventViewMixin, CartActionMixin, AsyncAction, View):
task = remove_cart_position
task = remove_items_from_cart
known_errortypes = ['CartError']
def get_success_message(self, value):
if CartPosition.objects.filter(cart_id=self.request.session.session_key).exists():
return _('Your cart has been updated.')
else:
return _('Your cart is now empty.')
return _('Your cart is empty.')
def post(self, request, *args, **kwargs):
if 'id' in request.POST:
return self.do(self.request.event.id, request.POST.get('id'), self.request.session.session_key, translation.get_language())
items = self._items_from_post_data()
if items:
return self.do(self.request.event.id, items, self.request.session.session_key, translation.get_language())
else:
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
return JsonResponse({
@@ -126,17 +127,6 @@ class CartRemove(EventViewMixin, CartActionMixin, AsyncAction, View):
return redirect(self.get_error_url())
class CartClear(EventViewMixin, CartActionMixin, AsyncAction, View):
task = clear_cart
known_errortypes = ['CartError']
def get_success_message(self, value):
return _('Your cart is now empty.')
def post(self, request, *args, **kwargs):
return self.do(self.request.event.id, self.request.session.session_key, translation.get_language())
class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
task = add_items_to_cart
known_errortypes = ['CartError']
@@ -171,7 +161,6 @@ class RedeemView(EventViewMixin, TemplateView):
Q(active=True)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& ~Q(category__is_addon=True)
)
vouchq = Q(hide_without_voucher=False)

Some files were not shown because too many files have changed in this diff Show More