Compare commits

...

16 Commits

Author SHA1 Message Date
Richard Schreiber
b56caaed91 Make customer identifier unique per organizer 2022-05-12 17:35:45 +02:00
Richard Schreiber
e1c6103dc4 Limit identifiers (Question, QuestionOption, Customer) to alphanum, dot, dash, and underscore 2022-05-12 17:24:17 +02:00
Richard Schreiber
5e88a3cfc3 PDF Editor: Fix CSS-selector for non-alphanum question identifiers (Z#2399663) 2022-05-12 14:01:00 +02:00
Iryna N
f9c71743d1 Translations: Update Ukrainian
Currently translated at 68.0% (3221 of 4732 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/uk/

powered by weblate
2022-05-12 10:00:16 +02:00
Raphael Michel
ed4bc87198 Show better error message if a gift card is used in apply_voucher 2022-05-11 17:06:20 +02:00
Richard Schreiber
351e06168e PDF Editor: set textfield to ??? to hint at unknown placeholder (Z#2399245) 2022-05-11 13:46:22 +02:00
Richard Schreiber
75dc134b45 PDF Editor: support Mac-like (CMD/metaKey) keyboard shortcuts 2022-05-11 13:44:50 +02:00
Richard Schreiber
53419b9e49 Fix check for mariadb 2022-05-11 13:41:45 +02:00
Raphael Michel
aca3e29bd2 Bump django-compressor to 3.1, get rid of annoying warnings (#2459) 2022-05-10 14:13:19 +02:00
Raphael Michel
2fcd6bb3f5 API: Support creating cart positions with vouchers (#2635) 2022-05-10 12:19:04 +02:00
Richard Schreiber
25313bf044 Add placeholder name_for_salutation to editor/PDF to improve handling of salutation „Mx“ (#2639)
* add name_for_salutation to editor/pdf

* improve order of fields

* add safe fallback for attendee_name_parts being None
2022-05-10 11:36:10 +02:00
Iryna Loik
7012605c9e Translations: Update Ukrainian
Currently translated at 63.6% (3012 of 4732 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/uk/

powered by weblate
2022-05-10 11:33:55 +02:00
Iryna N
5e8ce33470 Translations: Update Ukrainian
Currently translated at 84.5% (169 of 200 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/uk/

powered by weblate
2022-05-10 11:33:55 +02:00
Iryna N
4dfc037267 Translations: Update Ukrainian
Currently translated at 63.1% (2988 of 4732 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/uk/

powered by weblate
2022-05-10 11:33:55 +02:00
Raphael Michel
64ac69a81a Seating frame view: Allow to pass a voucher from query parameter 2022-05-10 11:31:27 +02:00
Richard Schreiber
40297b3d3f Localize salutation of invoice address in editor/PDFs 2022-05-10 11:22:54 +02:00
21 changed files with 944 additions and 330 deletions

View File

@@ -172,8 +172,6 @@ Cart position endpoints
* does not check or calculate prices but believes any prices you send * does not check or calculate prices but believes any prices you send
* does not support the redemption of vouchers
* does not prevent you from buying items that can only be bought with a voucher * does not prevent you from buying items that can only be bought with a voucher
* does not support file upload questions * does not support file upload questions
@@ -191,6 +189,7 @@ Cart position endpoints
* ``expires`` (optional) * ``expires`` (optional)
* ``includes_tax`` (optional, **deprecated**, do not use, will be removed) * ``includes_tax`` (optional, **deprecated**, do not use, will be removed)
* ``sales_channel`` (optional) * ``sales_channel`` (optional)
* ``voucher`` (optional, expect a voucher code)
* ``answers`` * ``answers``
* ``question`` * ``question``

View File

@@ -23,6 +23,7 @@ import os
from datetime import timedelta from datetime import timedelta
from django.core.files import File from django.core.files import File
from django.db.models import Q
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy from django.utils.translation import gettext_lazy
@@ -33,7 +34,7 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import ( from pretix.api.serializers.order import (
AnswerCreateSerializer, AnswerSerializer, InlineSeatSerializer, AnswerCreateSerializer, AnswerSerializer, InlineSeatSerializer,
) )
from pretix.base.models import Quota, Seat from pretix.base.models import Quota, Seat, Voucher
from pretix.base.models.orders import CartPosition from pretix.base.models.orders import CartPosition
@@ -61,11 +62,12 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
seat = serializers.CharField(required=False, allow_null=True) seat = serializers.CharField(required=False, allow_null=True)
sales_channel = serializers.CharField(required=False, default='sales_channel') sales_channel = serializers.CharField(required=False, default='sales_channel')
includes_tax = serializers.BooleanField(required=False, allow_null=True) includes_tax = serializers.BooleanField(required=False, allow_null=True)
voucher = serializers.CharField(required=False, allow_null=True)
class Meta: class Meta:
model = CartPosition model = CartPosition
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email', fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'subevent', 'expires', 'includes_tax', 'answers', 'seat', 'sales_channel') 'subevent', 'expires', 'includes_tax', 'answers', 'seat', 'sales_channel', 'voucher')
def create(self, validated_data): def create(self, validated_data):
answers_data = validated_data.pop('answers') answers_data = validated_data.pop('answers')
@@ -125,14 +127,46 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
raise ValidationError('The specified seat ID is not unique.') raise ValidationError('The specified seat ID is not unique.')
else: else:
validated_data['seat'] = seat validated_data['seat'] = seat
if not seat.is_available(
sales_channel=validated_data.get('sales_channel', 'web'),
distance_ignore_cart_id=validated_data['cart_id'],
):
raise ValidationError(gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
elif seated: elif seated:
raise ValidationError('The specified product requires to choose a seat.') raise ValidationError('The specified product requires to choose a seat.')
if validated_data.get('voucher'):
try:
voucher = self.context['event'].vouchers.get(code__iexact=validated_data.get('voucher'))
except Voucher.DoesNotExist:
raise ValidationError('The specified voucher does not exist.')
if voucher and not voucher.applies_to(validated_data.get('item'), validated_data.get('variation')):
raise ValidationError('The specified voucher is not valid for the given item and variation.')
if voucher and voucher.seat and voucher.seat != validated_data.get('seat'):
raise ValidationError('The specified voucher is not valid for this seat.')
if voucher and voucher.subevent_id and voucher.subevent_id != validated_data.get('subevent'):
raise ValidationError('The specified voucher is not valid for this subevent.')
if voucher.valid_until is not None and voucher.valid_until < now():
raise ValidationError('The specified voucher is expired.')
redeemed_in_carts = CartPosition.objects.filter(
Q(voucher=voucher) & Q(event=self.context['event']) & Q(expires__gte=now())
)
cart_count = redeemed_in_carts.count()
v_avail = voucher.max_usages - voucher.redeemed - cart_count
if v_avail < 1:
raise ValidationError('The specified voucher has already been used the maximum number of times.')
validated_data['voucher'] = voucher
if validated_data.get('seat'):
if not validated_data['seat'].is_available(
sales_channel=validated_data.get('sales_channel', 'web'),
distance_ignore_cart_id=validated_data['cart_id'],
ignore_voucher_id=validated_data['voucher'].pk if validated_data.get('voucher') else None,
):
raise ValidationError(
gettext_lazy('The selected seat "{seat}" is not available.').format(seat=validated_data['seat'].name))
validated_data.pop('sales_channel') validated_data.pop('sales_channel')
# todo: does this make sense? # todo: does this make sense?
validated_data['custom_price_input'] = validated_data['price'] validated_data['custom_price_input'] = validated_data['price']

View File

@@ -25,7 +25,7 @@ from django.db import transaction
from django.db.models import F, Q from django.db.models import F, Q
from django.utils.timezone import now from django.utils.timezone import now
from django_filters.rest_framework import ( from django_filters.rest_framework import (
BooleanFilter, DjangoFilterBackend, FilterSet, BooleanFilter, CharFilter, DjangoFilterBackend, FilterSet,
) )
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from rest_framework import status, viewsets from rest_framework import status, viewsets
@@ -40,6 +40,7 @@ from pretix.base.models import Voucher
with scopes_disabled(): with scopes_disabled():
class VoucherFilter(FilterSet): class VoucherFilter(FilterSet):
active = BooleanFilter(method='filter_active') active = BooleanFilter(method='filter_active')
code = CharFilter(lookup_expr='iexact')
class Meta: class Meta:
model = Voucher model = Voucher

View File

@@ -4,7 +4,6 @@
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db import migrations, models from django.db import migrations, models
from django_mysql.checks import mysql_connections from django_mysql.checks import mysql_connections
from django_mysql.utils import connection_is_mariadb
def set_attendee_name_parts(apps, schema_editor): def set_attendee_name_parts(apps, schema_editor):
@@ -31,7 +30,7 @@ def check_mysqlversion(apps, schema_editor):
conns = list(mysql_connections()) conns = list(mysql_connections())
found = 'Unknown version' found = 'Unknown version'
for alias, conn in conns: for alias, conn in conns:
if connection_is_mariadb(conn) and hasattr(conn, 'mysql_version'): if hasattr(conn, 'mysql_is_mariadb') and conn.mysql_is_mariadb and hasattr(conn, 'mysql_version'):
if conn.mysql_version >= (10, 2, 7): if conn.mysql_version >= (10, 2, 7):
any_conn_works = True any_conn_works = True
else: else:

View File

@@ -0,0 +1,22 @@
# Generated by Django 3.2.12 on 2022-05-12 15:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0214_customer_notes_ext_id'),
]
operations = [
migrations.AlterField(
model_name='customer',
name='identifier',
field=models.CharField(db_index=True, max_length=190),
),
migrations.AlterUniqueTogether(
name='customer',
unique_together={('organizer', 'email'), ('organizer', 'identifier')},
),
]

View File

@@ -24,6 +24,7 @@ from django.conf import settings
from django.contrib.auth.hashers import ( from django.contrib.auth.hashers import (
check_password, is_password_usable, make_password, check_password, is_password_usable, make_password,
) )
from django.core.validators import RegexValidator
from django.db import models from django.db import models
from django.db.models import F, Q from django.db.models import F, Q
from django.utils.crypto import get_random_string, salted_hmac from django.utils.crypto import get_random_string, salted_hmac
@@ -44,7 +45,18 @@ class Customer(LoggedModel):
""" """
id = models.BigAutoField(primary_key=True) id = models.BigAutoField(primary_key=True)
organizer = models.ForeignKey(Organizer, related_name='customers', on_delete=models.CASCADE) organizer = models.ForeignKey(Organizer, related_name='customers', on_delete=models.CASCADE)
identifier = models.CharField(max_length=190, db_index=True, unique=True) identifier = models.CharField(
max_length=190,
db_index=True,
help_text=_('You can enter any value here to make it easier to match the data with other sources. If you do '
'not input one, we will generate one automatically.'),
validators=[
RegexValidator(
regex=r"^[a-zA-Z0-9]([a-zA-Z0-9.\-_]*[a-zA-Z0-9])?$",
message=_("The identifier may only contain letters, numbers, dots, dashes, and underscores. It must start and end with a letter or number."),
),
],
)
email = models.EmailField(db_index=True, null=True, blank=False, verbose_name=_('E-mail'), max_length=190) email = models.EmailField(db_index=True, null=True, blank=False, verbose_name=_('E-mail'), max_length=190)
phone = PhoneNumberField(null=True, blank=True, verbose_name=_('Phone number')) phone = PhoneNumberField(null=True, blank=True, verbose_name=_('Phone number'))
password = models.CharField(verbose_name=_('Password'), max_length=128) password = models.CharField(verbose_name=_('Password'), max_length=128)
@@ -65,7 +77,7 @@ class Customer(LoggedModel):
objects = ScopedManager(organizer='organizer') objects = ScopedManager(organizer='organizer')
class Meta: class Meta:
unique_together = [['organizer', 'email']] unique_together = [['organizer', 'email'], ['organizer', 'identifier']]
ordering = ('email',) ordering = ('email',)
def get_email_field_name(self): def get_email_field_name(self):

View File

@@ -1243,7 +1243,13 @@ class Question(LoggedModel):
max_length=190, max_length=190,
verbose_name=_("Internal identifier"), verbose_name=_("Internal identifier"),
help_text=_('You can enter any value here to make it easier to match the data with other sources. If you do ' help_text=_('You can enter any value here to make it easier to match the data with other sources. If you do '
'not input one, we will generate one automatically.') 'not input one, we will generate one automatically.'),
validators=[
RegexValidator(
regex=r"^[a-zA-Z0-9.\-_]+$",
message=_("The identifier may only contain letters, numbers, dots, dashes, and underscores."),
),
],
) )
help_text = I18nTextField( help_text = I18nTextField(
verbose_name=_("Help text"), verbose_name=_("Help text"),
@@ -1461,7 +1467,17 @@ class Question(LoggedModel):
class QuestionOption(models.Model): class QuestionOption(models.Model):
question = models.ForeignKey('Question', related_name='options', on_delete=models.CASCADE) question = models.ForeignKey('Question', related_name='options', on_delete=models.CASCADE)
identifier = models.CharField(max_length=190) identifier = models.CharField(
max_length=190,
help_text=_('You can enter any value here to make it easier to match the data with other sources. If you do '
'not input one, we will generate one automatically.'),
validators=[
RegexValidator(
regex=r"^[a-zA-Z0-9.\-_]+$",
message=_("The identifier may only contain letters, numbers, dots, dashes, and underscores."),
),
],
)
answer = I18nCharField(verbose_name=_('Answer')) answer = I18nCharField(verbose_name=_('Answer'))
position = models.IntegerField(default=0) position = models.IntegerField(default=0)

View File

@@ -530,7 +530,10 @@ def _get_attendee_name_part(key, op, order, ev):
def _get_ia_name_part(key, op, order, ev): def _get_ia_name_part(key, op, order, ev):
return order.invoice_address.name_parts.get(key, '') if getattr(order, 'invoice_address', None) else '' value = order.invoice_address.name_parts.get(key, '') if getattr(order, 'invoice_address', None) else ''
if key == 'salutation' and value:
return pgettext('person_name_salutation', value)
return value
def get_images(event): def get_images(event):
@@ -546,6 +549,14 @@ def get_variables(event):
v = copy.copy(DEFAULT_VARIABLES) v = copy.copy(DEFAULT_VARIABLES)
scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme] scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
concatenation_for_salutation = scheme.get("concatenation_for_salutation", scheme["concatenation"])
v['attendee_name_for_salutation'] = {
'label': _("Attendee name for salutation"),
'editor_sample': _("Mr Doe"),
'evaluate': lambda op, order, ev: concatenation_for_salutation(op.attendee_name_parts or {})
}
for key, label, weight in scheme['fields']: for key, label, weight in scheme['fields']:
v['attendee_name_%s' % key] = { v['attendee_name_%s' % key] = {
'label': _("Attendee name: {part}").format(part=label), 'label': _("Attendee name: {part}").format(part=label),
@@ -563,6 +574,12 @@ def get_variables(event):
v['invoice_name']['editor_sample'] = scheme['concatenation'](scheme['sample']) v['invoice_name']['editor_sample'] = scheme['concatenation'](scheme['sample'])
v['attendee_name']['editor_sample'] = scheme['concatenation'](scheme['sample']) v['attendee_name']['editor_sample'] = scheme['concatenation'](scheme['sample'])
v['invoice_name_for_salutation'] = {
'label': _("Invoice address name for salutation"),
'editor_sample': _("Mr Doe"),
'evaluate': lambda op, order, ev: concatenation_for_salutation(order.invoice_address.name_parts if getattr(order, 'invoice_address', None) else {})
}
for key, label, weight in scheme['fields']: for key, label, weight in scheme['fields']:
v['invoice_name_%s' % key] = { v['invoice_name_%s' % key] = {
'label': _("Invoice address name: {part}").format(part=label), 'label': _("Invoice address name: {part}").format(part=label),

View File

@@ -447,7 +447,10 @@ class CartManager:
try: try:
voucher = self.event.vouchers.get(code__iexact=voucher_code.strip()) voucher = self.event.vouchers.get(code__iexact=voucher_code.strip())
except Voucher.DoesNotExist: except Voucher.DoesNotExist:
raise CartError(error_messages['voucher_invalid']) if self.event.organizer.accepted_gift_cards.filter(secret__iexact=voucher_code).exists():
raise CartError(error_messages['gift_card'])
else:
raise CartError(error_messages['voucher_invalid'])
voucher_use_diff = Counter() voucher_use_diff = Counter()
ops = [] ops = []

View File

View File

@@ -0,0 +1,36 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django.template.loaders.app_directories import Loader
from django.template.utils import get_app_template_dirs
class AppLoader(Loader):
def get_dirs(self):
ds = get_app_template_dirs('templates')
ignore_patterns = {
# Ignore templates of plugins we don't actually use as they cause trouble during
# static file compression
'/django_filters/',
'/django_otp/',
}
return [d for d in ds if not any(p in str(d) for p in ignore_patterns)]

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-28 16:44+0000\n" "POT-Creation-Date: 2022-04-28 16:44+0000\n"
"PO-Revision-Date: 2022-05-05 06:00+0000\n" "PO-Revision-Date: 2022-05-10 02:00+0000\n"
"Last-Translator: Iryna N <in380@nyu.edu>\n" "Last-Translator: Iryna N <in380@nyu.edu>\n"
"Language-Team: Ukrainian <https://translate.pretix.eu/projects/pretix/" "Language-Team: Ukrainian <https://translate.pretix.eu/projects/pretix/"
"pretix-js/uk/>\n" "pretix-js/uk/>\n"
@@ -88,7 +88,7 @@ msgstr "Так"
#: pretix/plugins/paypal/static/pretixplugins/paypal/pretix-paypal.js:47 #: pretix/plugins/paypal/static/pretixplugins/paypal/pretix-paypal.js:47
msgid "MyBank" msgid "MyBank"
msgstr "" msgstr "MyBank"
#: pretix/plugins/paypal/static/pretixplugins/paypal/pretix-paypal.js:48 #: pretix/plugins/paypal/static/pretixplugins/paypal/pretix-paypal.js:48
msgid "Przelewy24" msgid "Przelewy24"

View File

@@ -26,7 +26,7 @@ from urllib.parse import urljoin, urlsplit
import django_libsass import django_libsass
import sass import sass
from compressor.filters.cssmin import CSSCompressorFilter from compressor.filters.cssmin import CSSMinFilter
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.core.files.base import ContentFile, File from django.core.files.base import ContentFile, File
@@ -115,7 +115,7 @@ def compile_scss(object, file="main.scss", fonts=True):
include_paths=[sassdir], output_style='nested', include_paths=[sassdir], output_style='nested',
custom_functions=cf custom_functions=cf
) )
cssf = CSSCompressorFilter(css) cssf = CSSMinFilter(css)
css = cssf.output() css = cssf.output()
cache.set('sass_compile_{}_{}'.format(cp, srcchecksum), css, 600) cache.set('sass_compile_{}_{}'.format(cp, srcchecksum), css, 600)

View File

@@ -26,9 +26,9 @@
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="subevent" value="{{ subevent.id|default_if_none:"" }}"/> <input type="hidden" name="subevent" value="{{ subevent.id|default_if_none:"" }}"/>
{% if event.has_subevents %} {% if event.has_subevents %}
{% eventsignal event "pretix.presale.signals.render_seating_plan" request=request subevent=subevent %} {% eventsignal event "pretix.presale.signals.render_seating_plan" request=request subevent=subevent voucher=voucher %}
{% else %} {% else %}
{% eventsignal event "pretix.presale.signals.render_seating_plan" request=request %} {% eventsignal event "pretix.presale.signals.render_seating_plan" request=request voucher=voucher %}
{% endif %} {% endif %}
</form> </form>
{% include "pretixpresale/fragment_modals.html" %} {% include "pretixpresale/fragment_modals.html" %}

View File

@@ -700,6 +700,22 @@ class SeatingPlanView(EventViewMixin, TemplateView):
kwargs={'cart_namespace': kwargs.get('cart_namespace') or ''}) kwargs={'cart_namespace': kwargs.get('cart_namespace') or ''})
if context['cart_redirect'].startswith('https:'): if context['cart_redirect'].startswith('https:'):
context['cart_redirect'] = '/' + context['cart_redirect'].split('/', 3)[3] context['cart_redirect'] = '/' + context['cart_redirect'].split('/', 3)[3]
v = self.request.GET.get('voucher')
if v:
v = v.strip()
try:
voucher = self.request.event.vouchers.get(code__iexact=v)
if voucher.redeemed >= voucher.max_usages or voucher.valid_until is not None \
and voucher.valid_until < now() or voucher.item is not None \
and voucher.item.is_available() is False:
voucher = None
except Voucher.DoesNotExist:
voucher = None
else:
voucher = None
context['voucher'] = voucher
return context return context

View File

@@ -593,7 +593,7 @@ CSRF_FAILURE_VIEW = 'pretix.base.views.errors.csrf_failure'
template_loaders = ( template_loaders = (
'django.template.loaders.filesystem.Loader', 'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader', 'pretix.helpers.template_loaders.AppLoader',
) )
if not DEBUG: if not DEBUG:
template_loaders = ( template_loaders = (
@@ -648,6 +648,10 @@ COMPRESS_PRECOMPILERS = (
('text/vue', 'pretix.helpers.compressor.VueCompiler'), ('text/vue', 'pretix.helpers.compressor.VueCompiler'),
) )
COMPRESS_OFFLINE_CONTEXT = {
'basetpl': 'empty.html',
}
COMPRESS_ENABLED = COMPRESS_OFFLINE = not debug_fallback COMPRESS_ENABLED = COMPRESS_OFFLINE = not debug_fallback
COMPRESS_FILTERS = { COMPRESS_FILTERS = {
@@ -655,7 +659,10 @@ COMPRESS_FILTERS = {
# CssAbsoluteFilter is incredibly slow, especially when dealing with our _flags.scss # CssAbsoluteFilter is incredibly slow, especially when dealing with our _flags.scss
# However, we don't need it if we consequently use the static() function in Sass # However, we don't need it if we consequently use the static() function in Sass
# 'compressor.filters.css_default.CssAbsoluteFilter', # 'compressor.filters.css_default.CssAbsoluteFilter',
'compressor.filters.cssmin.CSSCompressorFilter', 'compressor.filters.cssmin.rCSSMinFilter',
),
'js': (
'compressor.filters.jsmin.JSMinFilter',
) )
} }

View File

@@ -286,7 +286,7 @@ var editor = {
} else if (key.startsWith('meta:')) { } else if (key.startsWith('meta:')) {
return key.substr(5); return key.substr(5);
} }
return $('#toolbox-content option[value='+key+'], #toolbox-content option[data-old-value='+key+']').attr('data-sample') || ''; return $('#toolbox-content option[value="'+key+'"], #toolbox-content option[data-old-value="'+key+'"]').attr('data-sample') || '???';
}, },
_load_page: function (page_number, dump) { _load_page: function (page_number, dump) {
@@ -835,32 +835,32 @@ var editor = {
editor._delete(); editor._delete();
break; break;
case 65: /* A */ case 65: /* A */
if (e.ctrlKey) { if (e.ctrlKey || e.metaKey) {
editor._selectAll(); editor._selectAll();
} }
break; break;
case 89: /* Y */ case 89: /* Y */
if (e.ctrlKey) { if (e.ctrlKey || e.metaKey) {
editor._redo(); editor._redo();
} }
break; break;
case 90: /* Z */ case 90: /* Z */
if (e.ctrlKey) { if (e.ctrlKey || e.metaKey) {
editor._undo(); editor._undo();
} }
break; break;
case 88: /* X */ case 88: /* X */
if (e.ctrlKey) { if (e.ctrlKey || e.metaKey) {
editor._cut(); editor._cut();
} }
break; break;
case 86: /* V */ case 86: /* V */
if (e.ctrlKey) { if (e.ctrlKey || e.metaKey) {
editor._paste(); editor._paste();
} }
break; break;
case 67: /* C */ case 67: /* C */
if (e.ctrlKey) { if (e.ctrlKey || e.metaKey) {
editor._copy(); editor._copy();
} }
break; break;

View File

@@ -165,13 +165,12 @@ setup(
'celery==4.4.*', 'celery==4.4.*',
'chardet==4.0.*', 'chardet==4.0.*',
'cryptography>=3.4.2', 'cryptography>=3.4.2',
'csscompressor',
'css-inline==0.7.*', 'css-inline==0.7.*',
'defusedcsv>=1.1.0', 'defusedcsv>=1.1.0',
'dj-static', 'dj-static',
'Django==3.2.*', 'Django==3.2.*',
'django-bootstrap3==15.0.*', 'django-bootstrap3==15.0.*',
'django-compressor==2.4.*', 'django-compressor==3.1.*',
'django-countries==7.2.*', 'django-countries==7.2.*',
'django-filter==21.1', 'django-filter==21.1',
'django-formset-js-improved==0.5.0.2', 'django-formset-js-improved==0.5.0.2',

View File

@@ -888,3 +888,180 @@ def test_cartpos_create_bulk_partial_seat_failure(token_client, organizer, event
assert CartPosition.objects.count() == 1 assert CartPosition.objects.count() == 1
cp1 = CartPosition.objects.get(pk=resp.data['results'][0]['data']['id']) cp1 = CartPosition.objects.get(pk=resp.data['results'][0]['data']['id'])
assert cp1.price == Decimal('23.00') assert cp1.price == Decimal('23.00')
@pytest.mark.django_db
def test_cartpos_create_with_voucher_by_code(token_client, organizer, event, item, quota, seat):
with scopes_disabled():
voucher = event.vouchers.create(code="FOOBAR", seat=seat)
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
res['item'] = item.pk
res['voucher'] = voucher.code
res['seat'] = seat.seat_guid
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
with scopes_disabled():
cp1 = CartPosition.objects.get(pk=resp.data['id'])
assert cp1.voucher == voucher
assert cp1.seat == seat
@pytest.mark.django_db
def test_cartpos_create_with_voucher_unknown(token_client, organizer, event, item, quota):
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
res['item'] = item.pk
res['voucher'] = 'TEST'
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == ['The specified voucher does not exist.']
@pytest.mark.django_db
def test_cartpos_create_with_voucher_invalid_item(token_client, organizer, event, item, quota):
with scopes_disabled():
item2 = event.items.create(name="item2")
voucher = event.vouchers.create(code="FOOBAR", item=item2)
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
res['item'] = item.pk
res['voucher'] = voucher.code
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == ['The specified voucher is not valid for the given item and variation.']
@pytest.mark.django_db
def test_cartpos_create_with_voucher_invalid_seat(token_client, organizer, event, item, quota, seat):
with scopes_disabled():
seat2 = event.seats.create(seat_number="A2", product=item, seat_guid="A2")
voucher = event.vouchers.create(code="FOOBAR", item=item, seat=seat2)
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
res['item'] = item.pk
res['voucher'] = voucher.code
res['seat'] = seat.seat_guid
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == ['The specified voucher is not valid for this seat.']
@pytest.mark.django_db
def test_cartpos_create_with_voucher_invalid_subevent(token_client, organizer, event, item, quota, subevent):
with scopes_disabled():
voucher = event.vouchers.create(code="FOOBAR", item=item, subevent=subevent)
quota.subevent = subevent
quota.save()
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
res['item'] = item.pk
res['voucher'] = voucher.code
res['subevent'] = subevent.pk
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == ['The specified voucher is not valid for this subevent.']
@pytest.mark.django_db
def test_cartpos_create_with_voucher_expired(token_client, organizer, event, item, quota):
with scopes_disabled():
voucher = event.vouchers.create(code="FOOBAR", item=item, valid_until=now() - datetime.timedelta(days=1))
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
res['item'] = item.pk
res['voucher'] = voucher.code
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == ['The specified voucher is expired.']
@pytest.mark.django_db
def test_cartpos_create_with_voucher_redeemed(token_client, organizer, event, item, quota):
with scopes_disabled():
voucher = event.vouchers.create(code="FOOBAR", item=item, redeemed=1)
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
res['item'] = item.pk
res['voucher'] = voucher.code
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == ['The specified voucher has already been used the maximum number of times.']
@pytest.mark.django_db
def test_cartpos_create_bulk_with_voucher(token_client, organizer, event, item, quota):
with scopes_disabled():
voucher = event.vouchers.create(code="FOOBAR", item=item, max_usages=3, redeemed=1)
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
res['item'] = item.pk
res['expires'] = (now() + datetime.timedelta(days=1)).isoformat()
res['voucher'] = voucher.code
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/cartpositions/bulk_create/'.format(
organizer.slug, event.slug
), format='json', data=[
res,
res
]
)
assert resp.status_code == 200
assert len(resp.data['results']) == 2
assert resp.data['results'][0]['success']
assert resp.data['results'][1]['success']
with scopes_disabled():
assert CartPosition.objects.count() == 2
cp1 = CartPosition.objects.get(pk=resp.data['results'][0]['data']['id'])
cp2 = CartPosition.objects.get(pk=resp.data['results'][1]['data']['id'])
assert cp1.voucher == voucher
assert cp2.voucher == voucher
@pytest.mark.django_db
def test_cartpos_create_bulk_with_voucher_redeemed(token_client, organizer, event, item, quota):
with scopes_disabled():
voucher = event.vouchers.create(code="FOOBAR", item=item, max_usages=3, redeemed=2)
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
res['item'] = item.pk
res['expires'] = (now() + datetime.timedelta(days=1)).isoformat()
res['voucher'] = voucher.code
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/cartpositions/bulk_create/'.format(
organizer.slug, event.slug
), format='json', data=[
res,
res
]
)
assert resp.status_code == 200
assert len(resp.data['results']) == 2
assert resp.data['results'][0]['success']
assert not resp.data['results'][1]['success']
assert resp.data['results'][1]['errors'] == {'non_field_errors': ['The specified voucher has already been used the maximum number of times.']}
with scopes_disabled():
assert CartPosition.objects.count() == 1
cp1 = CartPosition.objects.get(pk=resp.data['results'][0]['data']['id'])
assert cp1.voucher == voucher

View File

@@ -2026,6 +2026,31 @@ def test_order_create_with_seat_consumed_from_cart(token_client, organizer, even
assert p.seat == seat assert p.seat == seat
@pytest.mark.django_db
def test_order_create_with_voucher_consumed_from_cart(token_client, organizer, event, item, quota, question):
with scopes_disabled():
voucher = event.vouchers.create(code="FOOBAR", item=item, max_usages=3, redeemed=2)
CartPosition.objects.create(
event=event, cart_id='aaa', item=item, voucher=voucher,
price=21.5, expires=now() + datetime.timedelta(minutes=10),
)
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['positions'][0]['item'] = item.pk
res['positions'][0]['voucher'] = voucher.code
res['positions'][0]['answers'][0]['question'] = question.pk
res['consume_carts'] = ['aaa']
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
with scopes_disabled():
o = Order.objects.get(code=resp.data['code'])
p = o.positions.first()
assert p.voucher == voucher
@pytest.mark.django_db @pytest.mark.django_db
def test_order_create_send_no_emails(token_client, organizer, event, item, quota, question): def test_order_create_send_no_emails(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD) res = copy.deepcopy(ORDER_CREATE_PAYLOAD)