forked from CGM_Public/pretix_original
Compare commits
32 Commits
add-name-f
...
redis-resp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50f44b4d4d | ||
|
|
d4825d00fb | ||
|
|
591f5a75ef | ||
|
|
15407732ea | ||
|
|
82534f49da | ||
|
|
6c7f76fe96 | ||
|
|
08590f9d98 | ||
|
|
074252a9c0 | ||
|
|
615f7ed2cf | ||
|
|
e3a4435356 | ||
|
|
e8ec2a8d1f | ||
|
|
17cac62c31 | ||
|
|
73beabedea | ||
|
|
93f0e818e4 | ||
|
|
54ff5967fc | ||
|
|
41b18b9419 | ||
|
|
750a2511d5 | ||
|
|
e1c6103dc4 | ||
|
|
5e88a3cfc3 | ||
|
|
f9c71743d1 | ||
|
|
ed4bc87198 | ||
|
|
351e06168e | ||
|
|
75dc134b45 | ||
|
|
53419b9e49 | ||
|
|
aca3e29bd2 | ||
|
|
2fcd6bb3f5 | ||
|
|
25313bf044 | ||
|
|
7012605c9e | ||
|
|
5e8ce33470 | ||
|
|
4dfc037267 | ||
|
|
64ac69a81a | ||
|
|
40297b3d3f |
@@ -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``
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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')},
|
||||
),
|
||||
]
|
||||
18
src/pretix/base/migrations/0216_checkin_forced_sent.py
Normal file
18
src/pretix/base/migrations/0216_checkin_forced_sent.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
|
||||
@@ -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={
|
||||
|
||||
0
src/pretix/base/templates/empty.html
Normal file
0
src/pretix/base/templates/empty.html
Normal 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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
36
src/pretix/helpers/template_loaders.py
Normal file
36
src/pretix/helpers/template_loaders.py
Normal 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)]
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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 ''),
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user