Compare commits

...

32 Commits

Author SHA1 Message Date
Raphael Michel
50f44b4d4d Silence ResponseError from redis 2022-06-05 20:41:03 +02:00
Raphael Michel
d4825d00fb Fix copying variations when copying items 2022-05-20 16:09:57 +02:00
Raphael Michel
591f5a75ef Fix error handling on add-ons selection step (#2659)
Co-authored-by: Richard Schreiber <wiffbi@gmail.com>
2022-05-19 13:27:28 +02:00
Dennis Lichtenthäler
15407732ea Translations: Update German
Currently translated at 100.0% (4732 of 4732 strings)

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

powered by weblate
2022-05-19 09:58:18 +02:00
Ola Ola
82534f49da Translations: Update Ukrainian
Currently translated at 76.3% (3615 of 4732 strings)

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

powered by weblate
2022-05-19 09:58:18 +02:00
Raphael Michel
6c7f76fe96 Orders API: Allow downloading tickets for pending orders (#2657)
Co-authored-by: Richard Schreiber <wiffbi@gmail.com>
2022-05-19 09:58:06 +02:00
Raphael Michel
08590f9d98 Explicitly store whether checkins were offline (#2617) 2022-05-17 14:32:14 +02:00
Raphael Michel
074252a9c0 SecretKeySettingsWidget: Fix issue during form validation 2022-05-17 13:56:38 +02:00
Raphael Michel
615f7ed2cf Translations: Update Ukrainian
Currently translated at 76.3% (3612 of 4732 strings)

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

powered by weblate
2022-05-13 17:13:05 +02:00
Iryna Loik
e3a4435356 Translations: Update Ukrainian
Currently translated at 76.3% (3612 of 4732 strings)

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

powered by weblate
2022-05-13 17:13:05 +02:00
Jonathan Berger
e8ec2a8d1f Translations: Update French
Currently translated at 47.6% (2254 of 4732 strings)

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

powered by weblate
2022-05-13 17:13:05 +02:00
hmontheline
17cac62c31 Translations: Update French
Currently translated at 47.6% (2254 of 4732 strings)

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

powered by weblate
2022-05-13 17:13:05 +02:00
Jonathan Berger
73beabedea Translations: Update French
Currently translated at 47.4% (2244 of 4732 strings)

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

powered by weblate
2022-05-13 17:13:05 +02:00
Iryna Loik
93f0e818e4 Translations: Update Ukrainian
Currently translated at 69.9% (3309 of 4732 strings)

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

powered by weblate
2022-05-13 17:13:05 +02:00
Iryna N
54ff5967fc Translations: Update Ukrainian
Currently translated at 69.9% (3309 of 4732 strings)

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

powered by weblate
2022-05-13 17:13:05 +02:00
Richard Schreiber
41b18b9419 Make customer identifier unique per organizer (#2647) 2022-05-13 16:40:34 +02:00
Raphael Michel
750a2511d5 Fix incorrect escaping of QR code secrets 2022-05-13 16:36:48 +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
38 changed files with 1614 additions and 760 deletions

View File

@@ -172,8 +172,6 @@ Cart position endpoints
* 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 support file upload questions
@@ -191,6 +189,7 @@ Cart position endpoints
* ``expires`` (optional)
* ``includes_tax`` (optional, **deprecated**, do not use, will be removed)
* ``sales_channel`` (optional)
* ``voucher`` (optional, expect a voucher code)
* ``answers``
* ``question``

View File

@@ -609,13 +609,17 @@ Fetching individual orders
Order ticket download
---------------------
.. versionchanged:: 4.10
The API now supports ticket downloads for pending orders if allowed by the event settings.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/download/(output)/
Download tickets for an order, identified by its order code. Depending on the chosen output, the response might
be a ZIP file, PDF file or something else. The order details response contains a list of output options for this
particular order.
Tickets can be only downloaded if the order is paid and if ticket downloads are active. Note that in some cases the
Tickets can only be downloaded if ticket downloads are active and depending on event settings the order is either paid or pending. Note that in some cases the
ticket file might not yet have been created. In that case, you will receive a status code :http:statuscode:`409` and
you are expected to retry the request after a short period of waiting.
@@ -1635,6 +1639,10 @@ Fetching individual positions
Order position ticket download
------------------------------
.. versionchanged:: 4.10
The API now supports ticket downloads for pending orders if allowed by the event settings.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/download/(output)/
Download tickets for one order position, identified by its internal ID.
@@ -1646,7 +1654,7 @@ Order position ticket download
The referenced URL can provide a download or a regular, human-viewable website - so it is advised to open this URL
in a webbrowser and leave it up to the user to handle the result.
Tickets can be only downloaded if the order is paid and if ticket downloads are active. Also, depending on event
Tickets can only be downloaded if ticket downloads are active and depending on event settings the order is either paid or pending. Also, depending on event
configuration downloads might be only unavailable for add-on products or non-admission products.
Note that in some cases the ticket file might not yet have been created. In that case, you will receive a status
code :http:statuscode:`409` and you are expected to retry the request after a short period of waiting.

View File

@@ -23,6 +23,7 @@ import os
from datetime import timedelta
from django.core.files import File
from django.db.models import Q
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import gettext_lazy
@@ -33,7 +34,7 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import (
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
@@ -61,11 +62,12 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
seat = serializers.CharField(required=False, allow_null=True)
sales_channel = serializers.CharField(required=False, default='sales_channel')
includes_tax = serializers.BooleanField(required=False, allow_null=True)
voucher = serializers.CharField(required=False, allow_null=True)
class Meta:
model = CartPosition
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):
answers_data = validated_data.pop('answers')
@@ -125,14 +127,46 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
raise ValidationError('The specified seat ID is not unique.')
else:
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:
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')
# todo: does this make sense?
validated_data['custom_price_input'] = validated_data['price']

View File

@@ -157,6 +157,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
list=self.get_object(),
successful=False,
forced=True,
force_sent=True,
device=self.request.auth if isinstance(self.request.auth, Device) else None,
gate=self.request.auth.gate if isinstance(self.request.auth, Device) else None,
**kwargs,

View File

@@ -261,8 +261,11 @@ class OrderViewSet(viewsets.ModelViewSet):
provider = self._get_output_provider(output)
order = self.get_object()
if order.status != Order.STATUS_PAID:
raise PermissionDenied("Downloads are not available for unpaid orders.")
if order.status in (Order.STATUS_CANCELED, Order.STATUS_EXPIRED):
raise PermissionDenied("Downloads are not available for canceled or expired orders.")
if order.status == Order.STATUS_PENDING and not request.event.settings.ticket_download_pending:
raise PermissionDenied("Downloads are not available for pending orders.")
ct = CachedCombinedTicket.objects.filter(
order=order, provider=provider.identifier, file__isnull=False
@@ -1119,8 +1122,11 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
provider = self._get_output_provider(output)
pos = self.get_object()
if pos.order.status != Order.STATUS_PAID:
raise PermissionDenied("Downloads are not available for unpaid orders.")
if pos.order.status in (Order.STATUS_CANCELED, Order.STATUS_EXPIRED):
raise PermissionDenied("Downloads are not available for canceled or expired orders.")
if pos.order.status == Order.STATUS_PENDING and not request.event.settings.ticket_download_pending:
raise PermissionDenied("Downloads are not available for pending orders.")
if not pos.generate_ticket:
raise PermissionDenied("Downloads are not enabled for this product.")

View File

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

View File

@@ -196,10 +196,16 @@ class SecretKeySettingsWidget(forms.TextInput):
attrs.update({
'autocomplete': 'new-password' # see https://bugs.chromium.org/p/chromium/issues/detail?id=370363#c7
})
self.__reflect_value = False
super().__init__(attrs)
def value_from_datadict(self, data, files, name):
value = super().value_from_datadict(data, files, name)
self.__reflect_value = value and value != SECRET_REDACTED
return value
def get_context(self, name, value, attrs):
if value:
if value and not self.__reflect_value:
value = SECRET_REDACTED
return super().get_context(name, value, attrs)

View File

@@ -4,7 +4,6 @@
from django.core.exceptions import ImproperlyConfigured
from django.db import migrations, models
from django_mysql.checks import mysql_connections
from django_mysql.utils import connection_is_mariadb
def set_attendee_name_parts(apps, schema_editor):
@@ -31,7 +30,7 @@ def check_mysqlversion(apps, schema_editor):
conns = list(mysql_connections())
found = 'Unknown version'
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):
any_conn_works = True
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

@@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-04-29 13:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0215_customer_organizer_identifier_unique'),
]
operations = [
migrations.AddField(
model_name='checkin',
name='force_sent',
field=models.BooleanField(default=False, null=True),
),
]

View File

@@ -326,7 +326,13 @@ class Checkin(models.Model):
type = models.CharField(max_length=100, choices=CHECKIN_TYPES, default=TYPE_ENTRY)
nonce = models.CharField(max_length=190, null=True, blank=True)
# Whether or not the scan was made offline
force_sent = models.BooleanField(default=False, null=True, blank=True)
# Whether the scan was made offline AND would have not been possible online
forced = models.BooleanField(default=False)
device = models.ForeignKey(
'pretixbase.Device', related_name='checkins', on_delete=models.PROTECT, null=True, blank=True
)

View File

@@ -24,6 +24,7 @@ from django.conf import settings
from django.contrib.auth.hashers import (
check_password, is_password_usable, make_password,
)
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import F, Q
from django.utils.crypto import get_random_string, salted_hmac
@@ -44,7 +45,18 @@ class Customer(LoggedModel):
"""
id = models.BigAutoField(primary_key=True)
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)
phone = PhoneNumberField(null=True, blank=True, verbose_name=_('Phone number'))
password = models.CharField(verbose_name=_('Password'), max_length=128)
@@ -65,7 +77,7 @@ class Customer(LoggedModel):
objects = ScopedManager(organizer='organizer')
class Meta:
unique_together = [['organizer', 'email']]
unique_together = [['organizer', 'email'], ['organizer', 'identifier']]
ordering = ('email',)
def get_email_field_name(self):

View File

@@ -1243,7 +1243,13 @@ class Question(LoggedModel):
max_length=190,
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 '
'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(
verbose_name=_("Help text"),
@@ -1461,7 +1467,17 @@ class Question(LoggedModel):
class QuestionOption(models.Model):
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'))
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):
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):
@@ -546,6 +549,14 @@ def get_variables(event):
v = copy.copy(DEFAULT_VARIABLES)
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']:
v['attendee_name_%s' % key] = {
'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['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']:
v['invoice_name_%s' % key] = {
'label': _("Invoice address name: {part}").format(part=label),

View File

@@ -447,7 +447,10 @@ class CartManager:
try:
voucher = self.event.vouchers.get(code__iexact=voucher_code.strip())
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()
ops = []

View File

@@ -796,6 +796,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
gate=device.gate if device else None,
nonce=nonce,
forced=force and (not entry_allowed or from_revoked_secret),
force_sent=force,
raw_barcode=raw_barcode,
)
op.order.log_action('pretix.event.checkin', data={

View File

View File

@@ -36,6 +36,7 @@ from django.utils import timezone, translation
from django.utils.timezone import get_current_timezone
from django.utils.translation import get_language, gettext as _
from django.views.generic import FormView
from redis import ResponseError
from pretix.base.models import User
from pretix.base.services.tasks import ProfiledEventTask
@@ -68,6 +69,11 @@ class AsyncMixin:
res.get(timeout=timeout, propagate=False)
except celery.exceptions.TimeoutError:
pass
except ResponseError:
# There is a long-standing concurrency issue in either celery or redis-py that hasn't been fixed
# yet. Instead of crashing, we can ignore it and the client will retry their request and hopefully
# it is fixed next time.
logger.warning('Ignored ResponseError in AsyncResult.get()')
except ConnectionError:
# Redis probably just restarted, let's just report not ready and retry next time
data = self._ajax_response_data()

View File

@@ -32,6 +32,7 @@
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import copy
import os
from decimal import Decimal
from urllib.parse import urlencode
@@ -423,9 +424,10 @@ class ItemCreateForm(I18nModelForm):
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():
ItemVariation.objects.create(item=instance, value=variation.value, active=variation.active,
position=variation.position, default_price=variation.default_price,
description=variation.description, original_price=variation.original_price)
v = copy.copy(variation)
v.pk = None
v.item = instance
v.save()
else:
ItemVariation.objects.create(
item=instance, value=__('Standard')

View File

@@ -80,13 +80,17 @@
{% elif c.forced and c.successful %}
<span class="fa fa-fw fa-warning" data-toggle="tooltip"
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}"></span>
{% elif c.forced and not c.successful %}
<br>
<small class="text-muted">{% trans "Failed in offline mode" %}</small>
{% elif c.force_sent %}
<span class="fa fa-fw fa-cloud-upload" data-toggle="tooltip"
title="{% blocktrans trimmed with date=c.created|date:'SHORT_DATETIME_FORMAT' %}Offline scan. Upload time: {{ date }}{% endblocktrans %}"></span>
{% elif c.auto_checked_in %}
<span class="fa fa-fw fa-magic" data-toggle="tooltip"
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}"></span>
{% endif %}
{% if c.forced and not c.successful %}
<br>
<small class="text-muted">{% trans "Failed in offline mode" %}</small>
{% endif %}
</td>
<td>
{% if c.type == "exit" %}<span class="fa fa-fw fa-sign-out"></span>{% endif %}

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

View File

@@ -5,8 +5,8 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-28 16:44+0000\n"
"PO-Revision-Date: 2022-04-28 18:04+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"PO-Revision-Date: 2022-05-16 19:00+0000\n"
"Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/de/"
">\n"
"Language: de\n"
@@ -24436,7 +24436,7 @@ msgid ""
"methods such as giropay, iDEAL, Alipay,and many more."
msgstr ""
"Akzeptieren Sie Zahlungen über Stripe, einen weltweit beliebten "
"Zahlungsdienstleister. PayPal unterstützt Zahlungen per Kreditkarte sowie "
"Zahlungsdienstleister. Stripe unterstützt Zahlungen per Kreditkarte sowie "
"viele lokale Zahlungsarten wie z.B. giropay, iDEAL, Alipay, und viele mehr."
#: pretix/plugins/stripe/forms.py:40

View File

@@ -4,8 +4,8 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-28 16:44+0000\n"
"PO-Revision-Date: 2022-05-09 15:43+0000\n"
"Last-Translator: hmontheline <Helias.mackay@goetheanum.ch>\n"
"PO-Revision-Date: 2022-05-13 14:36+0000\n"
"Last-Translator: Jonathan Berger <drskullster@gmail.com>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
">\n"
"Language: fr\n"
@@ -1291,10 +1291,8 @@ msgstr "Date de commande"
#: pretix/base/exporters/orderlist.py:444
#: pretix/base/exporters/orderlist.py:547
#: pretix/plugins/checkinlists/exporters.py:469
#, fuzzy
#| msgid "Order date"
msgid "Order time"
msgstr "Date de commande"
msgstr "Heure de commande"
#: pretix/base/exporters/orderlist.py:280
#, fuzzy
@@ -1542,60 +1540,44 @@ msgstr "Tickets"
#: pretix/base/exporters/orderlist.py:577 pretix/base/orderimport.py:671
#: pretix/plugins/checkinlists/exporters.py:472
#, fuzzy
#| msgid "Client ID"
msgid "Seat ID"
msgstr "Numéro de client"
msgstr "Numéro de siège"
#: pretix/base/exporters/orderlist.py:578
#: pretix/plugins/checkinlists/exporters.py:473
#, fuzzy
#| msgid "Team name"
msgid "Seat name"
msgstr "Nom de l'équipe"
msgstr "Nom du siège"
#: pretix/base/exporters/orderlist.py:579
#: pretix/plugins/checkinlists/exporters.py:474
#, fuzzy
#| msgid "Team name"
msgid "Seat zone"
msgstr "Nom de l'équipe"
msgstr "Zone du siège"
#: pretix/base/exporters/orderlist.py:580
#: pretix/plugins/checkinlists/exporters.py:475
#, fuzzy
#| msgid "Client ID"
msgid "Seat row"
msgstr "Numéro de client"
msgstr "Rangée du siège"
#: pretix/base/exporters/orderlist.py:581
#: pretix/plugins/checkinlists/exporters.py:476
#, fuzzy
#| msgid "Team name"
msgid "Seat number"
msgstr "Nom de l'équipe"
msgstr "Numéro de siège"
#: pretix/base/exporters/orderlist.py:582
#, fuzzy
#| msgid "Order code"
msgid "Order comment"
msgstr "Code de commande"
msgstr "Commentaire de commande"
#: pretix/base/exporters/orderlist.py:750
msgid "Order payments and refunds"
msgstr "Commandes et remboursements"
#: pretix/base/exporters/orderlist.py:758
#, fuzzy
#| msgid "Payment date"
msgid "Payment states"
msgstr "Date de paiement"
msgstr "États de paiement"
#: pretix/base/exporters/orderlist.py:766
#, fuzzy
#| msgid "Check-in status"
msgid "Refund states"
msgstr "Statut d'enregistrement"
msgstr "Etats des remboursements"
#: pretix/base/exporters/orderlist.py:791
#: pretix/base/exporters/orderlist.py:930
@@ -1802,10 +1784,8 @@ msgstr "Remises par cartes-cadeaux"
#: pretix/base/exporters/orderlist.py:969
#: pretix/control/templates/pretixcontrol/giftcards/payment.html:10
#, fuzzy
#| msgid "Voucher details"
msgid "Issuer"
msgstr "Détails du bon"
msgstr "Emetteur"
#: pretix/base/exporters/orderlist.py:997 pretix/control/navigation.py:523
#: pretix/control/templates/pretixcontrol/organizers/edit.html:84
@@ -2039,10 +2019,8 @@ msgid "Please do not use special characters in names."
msgstr "Veuillez entrer votre nom."
#: pretix/base/forms/questions.py:257
#, fuzzy
#| msgid "Please enter your name."
msgid "Please enter a shorter name."
msgstr "Veuillez entrer votre nom."
msgstr "Veuillez entrer un nom plus court."
#: pretix/base/forms/questions.py:274
#, fuzzy

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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"
"Language-Team: Ukrainian <https://translate.pretix.eu/projects/pretix/"
"pretix-js/uk/>\n"
@@ -88,7 +88,7 @@ msgstr "Так"
#: pretix/plugins/paypal/static/pretixplugins/paypal/pretix-paypal.js:47
msgid "MyBank"
msgstr ""
msgstr "MyBank"
#: pretix/plugins/paypal/static/pretixplugins/paypal/pretix-paypal.js:48
msgid "Przelewy24"

View File

@@ -618,6 +618,7 @@ class CheckinLogList(ListExporter):
_('Product'),
_('Name'),
_('Device'),
_('Offline'),
_('Offline override'),
_('Automatically checked in'),
_('Gate'),
@@ -664,6 +665,7 @@ class CheckinLogList(ListExporter):
str(ci.position.item) if ci.position else (str(ci.raw_item) if ci.raw_item else ''),
(ci.position.attendee_name or ia.name) if ci.position else '',
str(ci.device) if ci.device else '',
_('Yes') if ci.force_sent is True else (_('No') if ci.force_sent is False else '?'),
_('Yes') if ci.forced else _('No'),
_('Yes') if ci.auto_checked_in else _('No'),
str(ci.gate or ''),

View File

@@ -26,7 +26,7 @@ from urllib.parse import urljoin, urlsplit
import django_libsass
import sass
from compressor.filters.cssmin import CSSCompressorFilter
from compressor.filters.cssmin import CSSMinFilter
from django.conf import settings
from django.core.cache import cache
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',
custom_functions=cf
)
cssf = CSSCompressorFilter(css)
cssf = CSSMinFilter(css)
css = cssf.output()
cache.set('sass_compile_{}_{}'.format(cp, srcchecksum), css, 600)

View File

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

View File

@@ -700,6 +700,22 @@ class SeatingPlanView(EventViewMixin, TemplateView):
kwargs={'cart_namespace': kwargs.get('cart_namespace') or ''})
if context['cart_redirect'].startswith('https:'):
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

View File

@@ -593,7 +593,7 @@ CSRF_FAILURE_VIEW = 'pretix.base.views.errors.csrf_failure'
template_loaders = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
'pretix.helpers.template_loaders.AppLoader',
)
if not DEBUG:
template_loaders = (
@@ -648,6 +648,10 @@ COMPRESS_PRECOMPILERS = (
('text/vue', 'pretix.helpers.compressor.VueCompiler'),
)
COMPRESS_OFFLINE_CONTEXT = {
'basetpl': 'empty.html',
}
COMPRESS_ENABLED = COMPRESS_OFFLINE = not debug_fallback
COMPRESS_FILTERS = {
@@ -655,7 +659,10 @@ COMPRESS_FILTERS = {
# 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
# 'compressor.filters.css_default.CssAbsoluteFilter',
'compressor.filters.cssmin.CSSCompressorFilter',
'compressor.filters.cssmin.rCSSMinFilter',
),
'js': (
'compressor.filters.jsmin.JSMinFilter',
)
}

View File

@@ -70,6 +70,7 @@ function async_task_check_error(jqXHR, textStatus, errorThrown) {
jqXHR.responseText.indexOf("<body"),
jqXHR.responseText.indexOf("</body")
));
setup_basics($("body"));
form_handlers($("body"));
setup_collapsible_details($("body"));
window.setTimeout(function () { $(window).scrollTop(0) }, 200)
@@ -152,6 +153,7 @@ function async_task_error(jqXHR, textStatus, errorThrown) {
if (respdom.filter('#page-wrapper') && $('#page-wrapper').length) {
$("#page-wrapper").html(respdom.find("#page-wrapper").html());
setup_basics($("#page-wrapper"));
form_handlers($("#page-wrapper"));
setup_collapsible_details($("#page-wrapper"));
$(document).trigger("pretix:bind-forms");
@@ -161,6 +163,7 @@ function async_task_error(jqXHR, textStatus, errorThrown) {
jqXHR.responseText.indexOf("<body"),
jqXHR.responseText.indexOf("</body")
));
setup_basics($("body"));
form_handlers($("body"));
setup_collapsible_details($("body"));
$(document).trigger("pretix:bind-forms");

View File

@@ -286,7 +286,7 @@ var editor = {
} else if (key.startsWith('meta:')) {
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) {
@@ -835,32 +835,32 @@ var editor = {
editor._delete();
break;
case 65: /* A */
if (e.ctrlKey) {
if (e.ctrlKey || e.metaKey) {
editor._selectAll();
}
break;
case 89: /* Y */
if (e.ctrlKey) {
if (e.ctrlKey || e.metaKey) {
editor._redo();
}
break;
case 90: /* Z */
if (e.ctrlKey) {
if (e.ctrlKey || e.metaKey) {
editor._undo();
}
break;
case 88: /* X */
if (e.ctrlKey) {
if (e.ctrlKey || e.metaKey) {
editor._cut();
}
break;
case 86: /* V */
if (e.ctrlKey) {
if (e.ctrlKey || e.metaKey) {
editor._paste();
}
break;
case 67: /* C */
if (e.ctrlKey) {
if (e.ctrlKey || e.metaKey) {
editor._copy();
}
break;

View File

@@ -918,7 +918,7 @@ $(function () {
height: 196
}
);
$div.append($(this).attr("data-qrcode").slice(0, 10) + "…<br>");
$div.append($("<div>").text($(this).attr("data-qrcode").slice(0, 10)).get(0).innerHTML + "…<br>");
$div.append(gettext("Click to close"));
$div.slideDown(200);
$div.click(function (e) {

View File

@@ -174,6 +174,63 @@ var form_handlers = function (el) {
}
};
function setup_basics(el) {
el.find("input[data-toggle=radiocollapse]").change(function () {
$($(this).attr("data-parent")).find(".collapse.in").collapse('hide');
$($(this).attr("data-target")).collapse('show');
});
el.find(".js-only").removeClass("js-only");
el.find(".js-hidden").hide();
el.find("div.collapsed").removeClass("collapsed").addClass("collapse");
el.find(".has-error, .alert-danger").each(function () {
$(this).closest("div.panel-collapse").collapse("show");
});
el.find(".has-error").first().each(function(){
if ($(this).is(':input')) this.focus();
else $(":input", this).get(0).focus();
});
el.find(".alert-danger").first().each(function() {
var content = $("<ul></ul>").click(function(e) {
var input = $(e.target.hash).get(0);
if (input) input.focus();
input.scrollIntoView({block: "center"});
e.preventDefault();
});
$(".has-error").each(function() {
var target = target = $(":input", this);
var desc = $("#" + target.attr("aria-describedby").split(' ', 1)[0]);
// multi-input fields have a role=group with aria-labelledby
var label = this.hasAttribute("aria-labelledby") ? $("#" + this.getAttribute("aria-labelledby")) : $("[for="+target.attr("id")+"]");
var $li = $("<li>");
$li.text(": " + desc.text())
$li.prepend($("<a>").attr("href", "#" + target.attr("id")).text(label.get(0).childNodes[0].nodeValue))
content.append($li);
});
$(this).append(content);
});
el.find("[data-click-to-load]").on("click", function(e) {
var target = document.getElementById(this.getAttribute("data-click-to-load"));
target.src = this.href;
target.focus();
e.preventDefault();
});
el.find('[data-toggle="tooltip"]').tooltip();
// AddOns
el.find('.addon-variation-description').hide();
el.find('.toggle-variation-description').click(function () {
$(this).parent().find('.addon-variation-description').slideToggle();
});
el.find('input[type=radio][description]').change(function () {
if ($(this).prop("checked")) {
$(this).parent().parent().find('.addon-variation-description').stop().slideDown();
}
});
}
$(function () {
"use strict";
@@ -191,48 +248,7 @@ $(function () {
if (!$input.prop("checked")) $input.prop('checked', true).trigger("change");
});
$("input[data-toggle=radiocollapse]").change(function () {
$($(this).attr("data-parent")).find(".collapse.in").collapse('hide');
$($(this).attr("data-target")).collapse('show');
});
$(".js-only").removeClass("js-only");
$(".js-hidden").hide();
$("div.collapsed").removeClass("collapsed").addClass("collapse");
$(".has-error, .alert-danger").each(function () {
$(this).closest("div.panel-collapse").collapse("show");
});
$(".has-error").first().each(function(){
if ($(this).is(':input')) this.focus();
else $(":input", this).get(0).focus();
});
$(".alert-danger").first().each(function() {
var content = $("<ul></ul>").click(function(e) {
var input = $(e.target.hash).get(0);
if (input) input.focus();
input.scrollIntoView({block: "center"});
e.preventDefault();
});
$(".has-error").each(function() {
var target = target = $(":input", this);
var desc = $("#" + target.attr("aria-describedby").split(' ', 1)[0]);
// multi-input fields have a role=group with aria-labelledby
var label = this.hasAttribute("aria-labelledby") ? $("#" + this.getAttribute("aria-labelledby")) : $("[for="+target.attr("id")+"]");
var $li = $("<li>");
$li.text(": " + desc.text())
$li.prepend($("<a>").attr("href", "#" + target.attr("id")).text(label.get(0).childNodes[0].nodeValue))
content.append($li);
});
$(this).append(content);
});
$("[data-click-to-load]").on("click", function(e) {
var target = document.getElementById(this.getAttribute("data-click-to-load"));
target.src = this.href;
target.focus();
e.preventDefault();
});
setup_basics($("body"));
$(".overlay-remove").on("click", function() {
$(this).closest(".contains-overlay").find(".overlay").fadeOut();
});
@@ -244,21 +260,8 @@ $(function () {
$("#voucher-toggle").slideUp();
});
$('[data-toggle="tooltip"]').tooltip();
$("#ajaxerr").on("click", ".ajaxerr-close", ajaxErrDialog.hide);
// AddOns
$('.addon-variation-description').hide();
$('.toggle-variation-description').click(function () {
$(this).parent().find('.addon-variation-description').slideToggle();
});
$('input[type=radio][description]').change(function () {
if ($(this).prop("checked")) {
$(this).parent().parent().find('.addon-variation-description').stop().slideDown();
}
});
// Copy answers
$(".js-copy-answers").click(function (e) {
e.preventDefault();

View File

@@ -165,13 +165,12 @@ setup(
'celery==4.4.*',
'chardet==4.0.*',
'cryptography>=3.4.2',
'csscompressor',
'css-inline==0.7.*',
'defusedcsv>=1.1.0',
'dj-static',
'Django==3.2.*',
'django-bootstrap3==15.0.*',
'django-compressor==2.4.*',
'django-compressor==3.1.*',
'django-countries==7.2.*',
'django-filter==21.1',
'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
cp1 = CartPosition.objects.get(pk=resp.data['results'][0]['data']['id'])
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

@@ -814,6 +814,7 @@ def test_forced_flag_set_if_required(token_client, organizer, clist, event, orde
), {'force': True}, format='json')
with scopes_disabled():
assert not p.checkins.order_by('pk').last().forced
assert p.checkins.order_by('pk').last().force_sent
assert resp.status_code == 201
assert resp.data['status'] == 'ok'
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
@@ -821,6 +822,7 @@ def test_forced_flag_set_if_required(token_client, organizer, clist, event, orde
), {'force': True}, format='json')
with scopes_disabled():
assert p.checkins.order_by('pk').last().forced
assert p.checkins.order_by('pk').last().force_sent
assert resp.status_code == 201
assert resp.data['status'] == 'ok'
@@ -1184,6 +1186,7 @@ def test_redeem_unknown_revoked_force(token_client, organizer, clist, event, ord
assert resp.data["status"] == "ok"
with scopes_disabled():
assert Checkin.objects.last().forced
assert Checkin.objects.last().force_sent
@pytest.mark.django_db

View File

@@ -2026,6 +2026,31 @@ def test_order_create_with_seat_consumed_from_cart(token_client, organizer, even
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
def test_order_create_send_no_emails(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)