forked from CGM_Public/pretix_original
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9099a38bc8 | ||
|
|
7cb66879ba | ||
|
|
29808ad92d |
@@ -294,10 +294,6 @@ Example::
|
||||
setting is not provided, pretix will generate a random secret on the first start
|
||||
and will store it in the filesystem for later usage.
|
||||
|
||||
``secret_fallback0`` ... ``secret_fallback9``
|
||||
Prior versions of the secret to be used by Django for signing and verification purposes that will still
|
||||
be accepted but no longer be used for new signing.
|
||||
|
||||
``debug``
|
||||
Whether or not to run in debug mode. Default is ``False``.
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ Endpoints
|
||||
"results": [
|
||||
{
|
||||
"identifier": "web",
|
||||
"label": {
|
||||
"name": {
|
||||
"en": "Online shop"
|
||||
},
|
||||
"type": "web",
|
||||
@@ -88,7 +88,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"identifier": "web",
|
||||
"label": {
|
||||
"name": {
|
||||
"en": "Online shop"
|
||||
},
|
||||
"type": "web",
|
||||
@@ -116,7 +116,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"identifier": "api.custom",
|
||||
"label": {
|
||||
"name": {
|
||||
"en": "Custom integration"
|
||||
},
|
||||
"type": "api",
|
||||
@@ -133,7 +133,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"identifier": "api.custom",
|
||||
"label": {
|
||||
"name": {
|
||||
"en": "Custom integration"
|
||||
},
|
||||
"type": "api",
|
||||
@@ -178,7 +178,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"identifier": "web",
|
||||
"label": {
|
||||
"name": {
|
||||
"en": "Online shop"
|
||||
},
|
||||
"type": "web",
|
||||
|
||||
@@ -14,7 +14,7 @@ Core
|
||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
|
||||
item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter,
|
||||
register_ticket_secret_generators, gift_card_transaction_display,
|
||||
register_text_placeholders, register_mail_placeholders, device_info_updated
|
||||
register_text_placeholders, register_mail_placeholders
|
||||
|
||||
Order events
|
||||
""""""""""""
|
||||
|
||||
@@ -29,8 +29,8 @@ item_assignments list of objects Products this l
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
Layout endpoints
|
||||
----------------
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
|
||||
|
||||
@@ -268,75 +268,5 @@ Layout endpoints
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
||||
|
||||
Ticket rendering endpoint
|
||||
-----------------------------
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/ticketpdfrenderer/render_batch/
|
||||
|
||||
With this API call, you can instruct the system to render a set of tickets into one combined PDF file. To specify
|
||||
which tickets to render, you need to submit a list of "parts". For every part, the following fields are supported:
|
||||
|
||||
* ``orderposition`` (``integer``, required): The ID of the order position to render.
|
||||
* ``override_channel`` (``string``, optional): The sales channel ID to be used for layout selection instead of the
|
||||
original channel of the order.
|
||||
* ``override_layout`` (``integer``, optional): The ticket layout ID to be used instead of the auto-selected one.
|
||||
|
||||
If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
|
||||
The body points you to the download URL of the result. Running a ``GET`` request on that result URL will
|
||||
yield one of the following status codes:
|
||||
|
||||
* ``200 OK`` – The export succeeded. The body will be your resulting file. Might be large!
|
||||
* ``409 Conflict`` – Your export is still running. The body will be JSON with the structure ``{"status": "running"}``. ``status`` can be ``waiting`` before the task is actually being processed. Please retry, but wait at least one second before you do.
|
||||
* ``410 Gone`` – Running the export has failed permanently. The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
|
||||
* ``404 Not Found`` – The export does not exist / is expired.
|
||||
|
||||
.. warning:: This endpoint is considered **experimental**. It might change at any time without prior notice.
|
||||
|
||||
.. note:: To avoid performance issues, a maximum number of 1000 parts is currently allowed.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/ticketpdfrenderer/render_batch/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parts": [
|
||||
{
|
||||
"orderposition": 55412
|
||||
},
|
||||
{
|
||||
"orderposition": 55412,
|
||||
"override_channel": "web"
|
||||
},
|
||||
{
|
||||
"orderposition": 55412,
|
||||
"override_layout": 56
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"download": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/ticketpdfrenderer/download/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 202: no error
|
||||
:statuscode 400: Invalid input options
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
|
||||
|
||||
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/pdf-layout.schema.json
|
||||
|
||||
+4
-4
@@ -80,18 +80,18 @@ dependencies = [
|
||||
"psycopg2-binary",
|
||||
"pycountry",
|
||||
"pycparser==2.22",
|
||||
"pycryptodome==3.21.*",
|
||||
"pycryptodome==3.20.*",
|
||||
"pypdf==5.0.*",
|
||||
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
||||
"python-dateutil==2.9.*",
|
||||
"pytz",
|
||||
"pytz-deprecation-shim==0.1.*",
|
||||
"pyuca",
|
||||
"qrcode==8.0",
|
||||
"redis==5.1.*",
|
||||
"qrcode==7.4.*",
|
||||
"redis==5.0.*",
|
||||
"reportlab==4.2.*",
|
||||
"requests==2.31.*",
|
||||
"sentry-sdk==2.15.*",
|
||||
"sentry-sdk==2.14.*",
|
||||
"sepaxml==2.6.*",
|
||||
"slimit",
|
||||
"stripe==7.9.*",
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
# 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/>.
|
||||
#
|
||||
__version__ = "2024.10.0.dev0"
|
||||
__version__ = "2024.9.0.dev0"
|
||||
|
||||
@@ -88,20 +88,16 @@ class SalesChannelMigrationMixin:
|
||||
}
|
||||
|
||||
if data.get("all_sales_channels") and set(data["sales_channels"]) != all_channels:
|
||||
raise ValidationError({
|
||||
"limit_sales_channels": [
|
||||
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||
"the list of all sales channels."
|
||||
]
|
||||
})
|
||||
raise ValidationError(
|
||||
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||
"the list of all sales channels."
|
||||
)
|
||||
|
||||
if data.get("limit_sales_channels") and set(data["sales_channels"]) != set(data["limit_sales_channels"]):
|
||||
raise ValidationError({
|
||||
"limit_sales_channels": [
|
||||
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||
"the same list."
|
||||
]
|
||||
})
|
||||
raise ValidationError(
|
||||
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||
"the same list."
|
||||
)
|
||||
|
||||
if data["sales_channels"] == all_channels:
|
||||
data["all_sales_channels"] = True
|
||||
|
||||
@@ -200,11 +200,6 @@ class UpdateView(APIView):
|
||||
device.save()
|
||||
device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device)
|
||||
|
||||
from ...base.signals import device_info_updated
|
||||
device_info_updated.send(
|
||||
sender=Device, old_device=request.auth, new_device=device
|
||||
)
|
||||
|
||||
serializer = DeviceSerializer(device)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ class Command(BaseCommand):
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--tasks', action='store', type=str, help='Only execute the tasks with this name '
|
||||
'(dotted path, comma separation)')
|
||||
parser.add_argument('--list-tasks', action='store_true', help='Only list all tasks')
|
||||
parser.add_argument('--exclude', action='store', type=str, help='Exclude the tasks with this name '
|
||||
'(dotted path, comma separation)')
|
||||
|
||||
@@ -62,9 +61,6 @@ class Command(BaseCommand):
|
||||
|
||||
for receiver in periodic_task._live_receivers(self):
|
||||
name = f'{receiver.__module__}.{receiver.__name__}'
|
||||
if options['list_tasks']:
|
||||
print(name)
|
||||
continue
|
||||
if options.get('tasks'):
|
||||
if name not in options.get('tasks').split(','):
|
||||
continue
|
||||
|
||||
@@ -571,23 +571,13 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
|
||||
def get_session_auth_hash(self):
|
||||
"""
|
||||
Return an HMAC that needs to be the same throughout the session, used e.g. for forced
|
||||
logout after every password change.
|
||||
"""
|
||||
return self._get_session_auth_hash(secret=settings.SECRET_KEY)
|
||||
|
||||
def get_session_auth_fallback_hash(self):
|
||||
for fallback_secret in settings.SECRET_KEY_FALLBACKS:
|
||||
yield self._get_session_auth_hash(secret=fallback_secret)
|
||||
|
||||
def _get_session_auth_hash(self, secret):
|
||||
"""
|
||||
Return an HMAC that needs to
|
||||
"""
|
||||
key_salt = "pretix.base.models.User.get_session_auth_hash"
|
||||
payload = self.password
|
||||
payload += self.email
|
||||
payload += self.session_token
|
||||
return salted_hmac(key_salt, payload, secret=secret).hexdigest()
|
||||
return salted_hmac(key_salt, payload).hexdigest()
|
||||
|
||||
def update_session_token(self):
|
||||
self.session_token = generate_session_token()
|
||||
|
||||
@@ -219,24 +219,13 @@ class Customer(LoggedModel):
|
||||
return is_password_usable(self.password)
|
||||
|
||||
def get_session_auth_hash(self):
|
||||
"""
|
||||
Return an HMAC that needs to be the same throughout the session, used e.g. for forced
|
||||
logout after every password change.
|
||||
"""
|
||||
return self._get_session_auth_hash(secret=settings.SECRET_KEY)
|
||||
|
||||
def get_session_auth_fallback_hash(self):
|
||||
for fallback_secret in settings.SECRET_KEY_FALLBACKS:
|
||||
yield self._get_session_auth_hash(secret=fallback_secret)
|
||||
|
||||
def _get_session_auth_hash(self, secret):
|
||||
"""
|
||||
Return an HMAC of the password field.
|
||||
"""
|
||||
key_salt = "pretix.base.models.customers.Customer.get_session_auth_hash"
|
||||
payload = self.password
|
||||
payload += self.email
|
||||
return salted_hmac(key_salt, payload, secret=secret).hexdigest()
|
||||
return salted_hmac(key_salt, payload).hexdigest()
|
||||
|
||||
def get_email_context(self):
|
||||
from pretix.base.settings import get_name_parts_localized
|
||||
|
||||
@@ -40,7 +40,6 @@ import json
|
||||
import logging
|
||||
import operator
|
||||
import string
|
||||
import warnings
|
||||
from collections import Counter
|
||||
from datetime import datetime, time, timedelta
|
||||
from decimal import Decimal
|
||||
@@ -382,28 +381,8 @@ class Order(LockModel, LoggedModel):
|
||||
self.event.cache.delete('complain_testmode_orders')
|
||||
self.delete()
|
||||
|
||||
def email_confirm_secret(self):
|
||||
return self.tagged_secret("email_confirm", 9)
|
||||
|
||||
def email_confirm_hash(self):
|
||||
warnings.warn('Use email_confirm_secret() instead of email_confirm_hash().',
|
||||
DeprecationWarning)
|
||||
return self.email_confirm_secret()
|
||||
|
||||
def check_email_confirm_secret(self, received_secret):
|
||||
return (
|
||||
hmac.compare_digest(
|
||||
self.tagged_secret("email_confirm", 9),
|
||||
received_secret[:9].lower()
|
||||
) or any(
|
||||
# TODO: remove this clause after a while (compatibility with old secrets currently in flight)
|
||||
hmac.compare_digest(
|
||||
hashlib.sha256(sk.encode() + self.secret.encode()).hexdigest()[:9],
|
||||
received_secret
|
||||
)
|
||||
for sk in [settings.SECRET_KEY, *settings.SECRET_KEY_FALLBACKS]
|
||||
)
|
||||
)
|
||||
return hashlib.sha256(settings.SECRET_KEY.encode() + self.secret.encode()).hexdigest()[:9]
|
||||
|
||||
def get_extended_status_display(self):
|
||||
# Changes in this method should to be replicated in pretixcontrol/orders/fragment_order_status.html
|
||||
|
||||
@@ -304,24 +304,10 @@ class TaxRule(LoggedModel):
|
||||
subtract_from_gross = Decimal('0.00')
|
||||
rate = adjust_rate
|
||||
|
||||
def _limit_subtract(base_price, subtract_from_gross):
|
||||
if not subtract_from_gross:
|
||||
return base_price
|
||||
if base_price >= Decimal('0.00'):
|
||||
# For positive prices, make sure they don't go negative because of bundles
|
||||
return max(Decimal('0.00'), base_price - subtract_from_gross)
|
||||
else:
|
||||
# If the price is already negative, we don't really care any more
|
||||
return base_price - subtract_from_gross
|
||||
|
||||
if rate == Decimal('0.00'):
|
||||
gross = _limit_subtract(base_price, subtract_from_gross)
|
||||
return TaxedPrice(
|
||||
net=gross,
|
||||
gross=gross,
|
||||
tax=Decimal('0.00'),
|
||||
rate=rate,
|
||||
name=self.name,
|
||||
net=base_price - subtract_from_gross, gross=base_price - subtract_from_gross, tax=Decimal('0.00'),
|
||||
rate=rate, name=self.name
|
||||
)
|
||||
|
||||
if base_price_is == 'auto':
|
||||
@@ -331,14 +317,19 @@ class TaxRule(LoggedModel):
|
||||
base_price_is = 'net'
|
||||
|
||||
if base_price_is == 'gross':
|
||||
gross = _limit_subtract(base_price, subtract_from_gross)
|
||||
if base_price >= Decimal('0.00'):
|
||||
# For positive prices, make sure they don't go negative because of bundles
|
||||
gross = max(Decimal('0.00'), base_price - subtract_from_gross)
|
||||
else:
|
||||
# If the price is already negative, we don't really care any more
|
||||
gross = base_price - subtract_from_gross
|
||||
net = round_decimal(gross - (gross * (1 - 100 / (100 + rate))),
|
||||
currency)
|
||||
elif base_price_is == 'net':
|
||||
net = base_price
|
||||
gross = round_decimal((net * (1 + rate / 100)), currency)
|
||||
if subtract_from_gross:
|
||||
gross = _limit_subtract(gross, subtract_from_gross)
|
||||
gross -= subtract_from_gross
|
||||
net = round_decimal(gross - (gross * (1 - 100 / (100 + rate))),
|
||||
currency)
|
||||
else:
|
||||
|
||||
@@ -1182,11 +1182,10 @@ def process_exit_all(sender, **kwargs):
|
||||
positions = cl.positions_inside_query(ignore_status=True, at_time=cl.exit_all_at)
|
||||
for p in positions:
|
||||
with scope(organizer=cl.event.organizer):
|
||||
ci, created = Checkin.objects.get_or_create(
|
||||
ci = Checkin.objects.create(
|
||||
position=p, list=cl, auto_checked_in=True, type=Checkin.TYPE_EXIT, datetime=cl.exit_all_at
|
||||
)
|
||||
if created:
|
||||
checkin_created.send(cl.event, checkin=ci)
|
||||
checkin_created.send(cl.event, checkin=ci)
|
||||
d = cl.exit_all_at.astimezone(cl.event.timezone)
|
||||
if cl.event.settings.get(f'autocheckin_dst_hack_{cl.pk}'): # move time back if yesterday was DST switch
|
||||
d -= timedelta(hours=1)
|
||||
|
||||
@@ -301,7 +301,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
order.event, 'presale:event.order.open', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
'hash': order.email_confirm_secret()
|
||||
'hash': order.email_confirm_hash()
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -262,7 +262,7 @@ def base_placeholders(sender, **kwargs):
|
||||
'presale:event.order.open', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
'hash': order.email_confirm_secret()
|
||||
'hash': order.email_confirm_hash()
|
||||
}
|
||||
), lambda event: build_absolute_uri(
|
||||
event,
|
||||
@@ -443,7 +443,7 @@ def base_placeholders(sender, **kwargs):
|
||||
'organizer': event.organizer.slug,
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
'hash': order.email_confirm_secret(),
|
||||
'hash': order.email_confirm_hash(),
|
||||
}),
|
||||
)
|
||||
for order in orders
|
||||
|
||||
@@ -838,12 +838,3 @@ is given as the first argument.
|
||||
|
||||
The ``sender`` keyword argument will contain the organizer.
|
||||
"""
|
||||
|
||||
device_info_updated = django.dispatch.Signal()
|
||||
"""
|
||||
Arguments: ``old_device``, ``new_device``
|
||||
|
||||
This signal is sent out each time the information for a Device is modified.
|
||||
Both the original and updated versions of the Device are included to allow
|
||||
receivers to see what has been updated.
|
||||
"""
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
<div class="order-button">
|
||||
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_secret order=order.code secret=order.secret %}" class="button">
|
||||
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_hash order=order.code secret=order.secret %}" class="button">
|
||||
{% trans "View order details" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -103,7 +103,7 @@ def timeline_for_event(event, subevent=None):
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=rd.datetime(ev),
|
||||
description=pgettext_lazy('timeline', 'Customers can no longer modify their order information'),
|
||||
description=pgettext_lazy('timeline', 'Customers can no longer modify their orders'),
|
||||
edit_url=ev_edit_url
|
||||
))
|
||||
|
||||
@@ -159,18 +159,6 @@ def timeline_for_event(event, subevent=None):
|
||||
})
|
||||
))
|
||||
|
||||
rd = event.settings.get('change_allow_user_until', as_type=RelativeDateWrapper)
|
||||
if rd and event.settings.change_allow_user_until:
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=rd.datetime(ev),
|
||||
description=pgettext_lazy('timeline', 'Customers can no longer make changes to their orders'),
|
||||
edit_url=reverse('control:event.settings.cancel', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug
|
||||
})
|
||||
))
|
||||
|
||||
rd = event.settings.get('waiting_list_auto_disable', as_type=RelativeDateWrapper)
|
||||
if rd and event.settings.waiting_list_enabled:
|
||||
tl.append(TimelineEvent(
|
||||
|
||||
@@ -239,14 +239,11 @@ class VoucherForm(I18nModelForm):
|
||||
self.instance.event, self.instance.quota, self.instance.item, self.instance.variation
|
||||
)
|
||||
Voucher.clean_voucher_code(data, self.instance.event, self.instance.pk)
|
||||
if 'seat' in self.fields:
|
||||
if data.get('seat'):
|
||||
self.instance.seat = Voucher.clean_seat_id(
|
||||
data, self.instance.item, self.instance.quota, self.instance.event, self.instance.pk
|
||||
)
|
||||
self.instance.item = self.instance.seat.product
|
||||
else:
|
||||
self.instance.seat = None
|
||||
if 'seat' in self.fields and data.get('seat'):
|
||||
self.instance.seat = Voucher.clean_seat_id(
|
||||
data, self.instance.item, self.instance.quota, self.instance.event, self.instance.pk
|
||||
)
|
||||
self.instance.item = self.instance.seat.product
|
||||
|
||||
voucher_form_validation.send(sender=self.instance.event, form=self, data=data)
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
{% bootstrap_field form.eu_reverse_charge layout="control" %}
|
||||
{% bootstrap_field form.home_country layout="control" %}
|
||||
{% bootstrap_field form.keep_gross_if_rate_changes layout="control" %}
|
||||
<h3>{% trans "Custom rules" %}</h3>
|
||||
<h3>{% trans "Custom taxation rules" %}</h3>
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
These settings are intended for professional users with very specific taxation situations.
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
{% endif %}
|
||||
{{ i.default_price|money:request.event.currency }}
|
||||
{% if i.original_price %}<strike class="text-muted">{{ i.original_price|money:request.event.currency }}</strike>{% endif %}
|
||||
{% if i.tax_rule %}
|
||||
{% if i.tax_rule and i.default_price %}
|
||||
<br/>
|
||||
<small class="text-muted">
|
||||
{% if not i.tax_rule.price_includes_tax %}
|
||||
|
||||
@@ -21,11 +21,6 @@
|
||||
{% trans "The waiting list is no longer active for this event. The waiting list no longer affects quotas and no longer notifies waiting users." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if request.event.settings.hide_sold_out %}
|
||||
<div class="alert alert-warning">
|
||||
{% trans "According to your event settings, sold out products are hidden from customers. This way, customers will not be able to discovere the waiting list." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
{% if 'can_change_orders' in request.eventpermset %}
|
||||
<form method="post" class="col-md-6"
|
||||
|
||||
@@ -94,9 +94,7 @@ def process_login(request, user, keep_logged_in):
|
||||
pretix_successful_logins.inc(1)
|
||||
handle_login_source(user, request)
|
||||
auth_login(request, user)
|
||||
t = int(time.time())
|
||||
request.session['pretix_auth_login_time'] = t
|
||||
request.session['pretix_auth_last_used'] = t
|
||||
request.session['pretix_auth_login_time'] = int(time.time())
|
||||
if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None):
|
||||
return redirect_to_url(next_url)
|
||||
return redirect('control:index')
|
||||
|
||||
@@ -39,7 +39,7 @@ from django.contrib import messages
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import transaction
|
||||
from django.db.models import Exists, Max, OuterRef, Prefetch, Q, Subquery
|
||||
from django.http import Http404, HttpResponseNotAllowed, HttpResponseRedirect
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
@@ -193,9 +193,6 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, CheckInList
|
||||
class CheckInListBulkRevertConfirmView(CheckInListQueryMixin, EventPermissionRequiredMixin, TemplateView):
|
||||
template_name = "pretixcontrol/checkin/bulk_revert_confirm.html"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return HttpResponseNotAllowed(permitted_methods=["POST"])
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.list = get_object_or_404(self.request.event.checkin_lists.all(), pk=kwargs.get("list"))
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,6 @@ AES
|
||||
Absenderadresse
|
||||
Absenderinformation
|
||||
Absendername
|
||||
ABGEKÜNDIGT
|
||||
Admin
|
||||
Adminbereich
|
||||
Affirm
|
||||
@@ -26,7 +25,6 @@ Ausgangsscans
|
||||
ausgeklappt
|
||||
ausgecheckt
|
||||
auswahl
|
||||
Auth
|
||||
Authentication
|
||||
Authenticator
|
||||
Authentifizierungsmechanismus
|
||||
@@ -38,7 +36,6 @@ Bancontact
|
||||
BankID
|
||||
Banking
|
||||
barcodes
|
||||
Baskisch
|
||||
Bcc
|
||||
BCC
|
||||
Beispielevent
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,6 @@ AES
|
||||
Absenderadresse
|
||||
Absenderinformation
|
||||
Absendername
|
||||
ABGEKÜNDIGT
|
||||
Admin
|
||||
Adminbereich
|
||||
Affirm
|
||||
@@ -26,7 +25,6 @@ Ausgangsscans
|
||||
ausgeklappt
|
||||
ausgecheckt
|
||||
auswahl
|
||||
Auth
|
||||
Authentication
|
||||
Authenticator
|
||||
Authentifizierungsmechanismus
|
||||
@@ -38,7 +36,6 @@ Bancontact
|
||||
BankID
|
||||
Banking
|
||||
barcodes
|
||||
Baskisch
|
||||
Bcc
|
||||
BCC
|
||||
Beispielevent
|
||||
|
||||
+701
-751
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-09-26 11:23+0000\n"
|
||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1532
-1109
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-09-26 11:23+0000\n"
|
||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
||||
"PO-Revision-Date: 2024-09-06 08:47+0000\n"
|
||||
"Last-Translator: Albizuri <oier@puntu.eus>\n"
|
||||
"Language-Team: Basque <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -557,14 +557,17 @@ msgid "absent"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:171
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:72
|
||||
msgid "Check-in QR"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:543
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:387
|
||||
msgid "The PDF background file could not be loaded for the following reason:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:894
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:656
|
||||
msgid "Group of objects"
|
||||
msgstr ""
|
||||
|
||||
@@ -577,35 +580,44 @@ msgid "Text box"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:903
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:663
|
||||
msgid "Barcode area"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:905
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:665
|
||||
msgid "Image area"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:907
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:667
|
||||
msgid "Powered by pretix"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:909
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:669
|
||||
msgid "Object"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:913
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:673
|
||||
msgid "Ticket design"
|
||||
msgstr "Sarrera diseinua"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1250
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:972
|
||||
msgid "Saving failed."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1319
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1370
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1041
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1091
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1353
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1074
|
||||
msgid "Do you really want to leave the editor without saving your changes?"
|
||||
msgstr ""
|
||||
|
||||
@@ -622,14 +634,17 @@ msgid "Unknown error."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:318
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:313
|
||||
msgid "Your color has great contrast and is very easy to read!"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:322
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:317
|
||||
msgid "Your color has decent contrast and is probably good-enough to read!"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:326
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:321
|
||||
msgid ""
|
||||
"Your color has bad contrast for text on white background, please choose a "
|
||||
"darker shade."
|
||||
@@ -637,38 +652,48 @@ msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:475
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||
msgid "Search query"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:493
|
||||
msgid "All"
|
||||
msgstr "Guztiak"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:494
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:498
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
@@ -706,8 +731,7 @@ msgstr "Saskia iraungita"
|
||||
msgid "The items in your cart are reserved for you for one minute."
|
||||
msgid_plural "The items in your cart are reserved for you for {num} minutes."
|
||||
msgstr[0] "Zure saskiko produktuak minutu -ez erreserbatuta daude zuretzat."
|
||||
msgstr[1] ""
|
||||
"Zure saskiko produktuak {num} minutuz erreserbatuta daude zuretzat."
|
||||
msgstr[1] "Zure saskiko produktuak {num} minutuz erreserbatuta daude zuretzat."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:203
|
||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||
@@ -1075,3 +1099,7 @@ msgstr ""
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:89
|
||||
msgid "December"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:661
|
||||
msgid "Text object"
|
||||
msgstr ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1338
-1705
File diff suppressed because it is too large
Load Diff
@@ -8,16 +8,16 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
||||
"PO-Revision-Date: 2024-10-01 22:52+0000\n"
|
||||
"Last-Translator: Patrick Chilton <chpatrick@gmail.com>\n"
|
||||
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/"
|
||||
"pretix-js/hu/>\n"
|
||||
"PO-Revision-Date: 2020-01-24 08:00+0000\n"
|
||||
"Last-Translator: Prokaj Miklós <mixolid0@gmail.com>\n"
|
||||
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
"js/hu/>\n"
|
||||
"Language: hu\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.7.2\n"
|
||||
"X-Generator: Weblate 3.5.1\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -134,6 +134,9 @@ msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "Continue"
|
||||
msgid "Continue"
|
||||
msgstr "Folytatás"
|
||||
|
||||
@@ -170,7 +173,7 @@ msgstr "Kapcsolatfelvétel Stripe-pal…"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:72
|
||||
msgid "Total"
|
||||
msgstr "Összeg"
|
||||
msgstr "Teljes"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:291
|
||||
msgid "Contacting your bank …"
|
||||
@@ -238,7 +241,7 @@ msgstr ""
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:44
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:45
|
||||
msgid "Canceled"
|
||||
msgstr "Lemondva"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
|
||||
msgid "Confirmed"
|
||||
@@ -246,7 +249,7 @@ msgstr ""
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
|
||||
msgid "Approval pending"
|
||||
msgstr "Engedélyre vár"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
||||
#, fuzzy
|
||||
@@ -257,7 +260,7 @@ msgstr "Beváltás"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
||||
msgid "Cancel"
|
||||
msgstr "Lemondás"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
||||
@@ -270,7 +273,7 @@ msgstr ""
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
|
||||
msgid "Additional information required"
|
||||
msgstr "Több információ szükséges"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
|
||||
msgid "Valid ticket"
|
||||
@@ -286,7 +289,7 @@ msgstr ""
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
|
||||
msgid "Information required"
|
||||
msgstr "Információ szükséges"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
|
||||
#, fuzzy
|
||||
@@ -468,7 +471,7 @@ msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
|
||||
msgid "Product"
|
||||
msgstr "Termék"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
|
||||
#, fuzzy
|
||||
@@ -727,12 +730,12 @@ msgstr[0] "(még egy időpont)"
|
||||
msgstr[1] "(még {num} időpont)"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:43
|
||||
#, fuzzy
|
||||
#| msgid "The items in your cart are no longer reserved for you."
|
||||
msgid ""
|
||||
"The items in your cart are no longer reserved for you. You can still "
|
||||
"complete your order as long as they’re available."
|
||||
msgstr ""
|
||||
"A kosárba helyezett tételek tovább már nincsenek lefoglalva. Még "
|
||||
"megpróbálhatod befejezni a rendelést, ha még elérhetőek."
|
||||
msgstr "A kosárba helyezett termékek tovább nincsenek tovább foglalva."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:45
|
||||
msgid "Cart expired"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,6 @@ acceptor
|
||||
analytics
|
||||
anonymize
|
||||
anonymized
|
||||
Auth
|
||||
authenticator
|
||||
automatical
|
||||
availabilities
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -31,8 +31,6 @@
|
||||
# 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.
|
||||
from datetime import datetime
|
||||
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
@@ -47,7 +45,6 @@ from pretix.helpers.format import format_map
|
||||
|
||||
@app.task(base=ProfiledEventTask, acks_late=True)
|
||||
def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict, objects: list, items: list,
|
||||
subevent: int, subevents_from: datetime, subevents_to: datetime,
|
||||
recipients: str, filter_checkins: bool, not_checked_in: bool, checkin_lists: list,
|
||||
attachments: list = None, attach_tickets: bool = False,
|
||||
attach_ical: bool = False) -> None:
|
||||
@@ -79,7 +76,7 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
|
||||
list_id__in=checkin_lists or []
|
||||
)
|
||||
),
|
||||
).prefetch_related('addons', 'subevent'):
|
||||
).prefetch_related('addons'):
|
||||
if p.addon_to_id is not None:
|
||||
continue
|
||||
|
||||
@@ -102,15 +99,6 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
|
||||
if p.attendee_email == o.email and send_to_order:
|
||||
continue
|
||||
|
||||
if subevent and p.subevent_id != subevent:
|
||||
continue
|
||||
|
||||
if subevents_from and p.subevent.date_from < subevents_from:
|
||||
continue
|
||||
|
||||
if subevents_to and p.subevent.date_from >= subevents_to:
|
||||
continue
|
||||
|
||||
try:
|
||||
with language(o.locale, event.settings.region):
|
||||
email_context = get_email_context(event=event, order=o, invoice_address=ia, position=p)
|
||||
|
||||
@@ -429,9 +429,6 @@ class OrderSendView(BaseSenderView):
|
||||
kwargs.update({
|
||||
'recipients': form.cleaned_data['recipients'],
|
||||
'items': [i.pk for i in form.cleaned_data.get('items')],
|
||||
'subevent': form.cleaned_data['subevent'].pk if form.cleaned_data.get('subevent') else None,
|
||||
'subevents_from': form.cleaned_data.get('subevents_from'),
|
||||
'subevents_to': form.cleaned_data.get('subevents_to'),
|
||||
'not_checked_in': form.cleaned_data.get('not_checked_in'),
|
||||
'checkin_lists': [i.pk for i in form.cleaned_data.get('checkin_lists')],
|
||||
'filter_checkins': form.cleaned_data.get('filter_checkins'),
|
||||
|
||||
@@ -19,33 +19,21 @@
|
||||
# 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 datetime import timedelta
|
||||
|
||||
from celery.result import AsyncResult
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.db.models import QuerySet
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import serializers, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework import serializers, viewsets
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.order import CompatibleJSONField
|
||||
|
||||
from ...api.serializers.fields import UploadedFileField
|
||||
from ...base.models import CachedFile, OrderPosition, SalesChannel
|
||||
from ...base.models import SalesChannel
|
||||
from ...base.pdf import PdfLayoutValidator
|
||||
from ...helpers.http import ChunkBasedFileResponse
|
||||
from ...multidomain.utils import static_absolute
|
||||
from .models import TicketLayout, TicketLayoutItem
|
||||
from .tasks import bulk_render
|
||||
|
||||
|
||||
class ItemAssignmentSerializer(I18nAwareModelSerializer):
|
||||
@@ -168,123 +156,3 @@ class TicketLayoutItemViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
**super().get_serializer_context(),
|
||||
'event': self.request.event,
|
||||
}
|
||||
|
||||
|
||||
with scopes_disabled():
|
||||
class RenderJobPartSerializer(serializers.Serializer):
|
||||
orderposition = serializers.PrimaryKeyRelatedField(
|
||||
queryset=OrderPosition.objects.none(),
|
||||
required=True,
|
||||
allow_null=False,
|
||||
)
|
||||
override_layout = serializers.PrimaryKeyRelatedField(
|
||||
queryset=TicketLayout.objects.none(),
|
||||
required=False,
|
||||
allow_null=True,
|
||||
)
|
||||
override_channel = serializers.SlugRelatedField(
|
||||
queryset=SalesChannel.objects.none(),
|
||||
slug_field='identifier',
|
||||
required=False,
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
|
||||
class RenderJobSerializer(serializers.Serializer):
|
||||
parts = RenderJobPartSerializer(many=True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['parts'].child.fields['orderposition'].queryset = OrderPosition.objects.filter(order__event=self.context['event'])
|
||||
self.fields['parts'].child.fields['override_layout'].queryset = self.context['event'].ticket_layouts.all()
|
||||
self.fields['parts'].child.fields['override_channel'].queryset = self.context['event'].organizer.sales_channels.all()
|
||||
|
||||
def validate(self, attrs):
|
||||
if len(attrs["parts"]) > 1000:
|
||||
raise ValidationError({"parts": ["Please do not submit more than 1000 parts."]})
|
||||
return super().validate(attrs)
|
||||
|
||||
|
||||
class TicketRendererViewSet(viewsets.ViewSet):
|
||||
permission = 'can_view_orders'
|
||||
|
||||
def get_serializer_kwargs(self):
|
||||
return {}
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
raise Http404()
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
raise Http404()
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
raise Http404()
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
raise Http404()
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
raise Http404()
|
||||
|
||||
@action(detail=False, methods=['GET'], url_name='download', url_path='download/(?P<asyncid>[^/]+)/(?P<cfid>[^/]+)')
|
||||
def download(self, *args, **kwargs):
|
||||
cf = get_object_or_404(CachedFile, id=kwargs['cfid'])
|
||||
if cf.file:
|
||||
resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename).encode("ascii", "ignore")
|
||||
return resp
|
||||
elif not settings.HAS_CELERY:
|
||||
return Response(
|
||||
{'status': 'failed', 'message': 'Unknown file ID or export failed'},
|
||||
status=status.HTTP_410_GONE
|
||||
)
|
||||
|
||||
res = AsyncResult(kwargs['asyncid'])
|
||||
if res.failed():
|
||||
if isinstance(res.info, dict) and res.info['exc_type'] == 'ExportError':
|
||||
msg = res.info['exc_message']
|
||||
else:
|
||||
msg = 'Internal error'
|
||||
return Response(
|
||||
{'status': 'failed', 'message': msg},
|
||||
status=status.HTTP_410_GONE
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
'status': 'running' if res.state in ('PROGRESS', 'STARTED', 'SUCCESS') else 'waiting',
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT
|
||||
)
|
||||
|
||||
@action(detail=False, methods=['POST'])
|
||||
def render_batch(self, *args, **kwargs):
|
||||
serializer = RenderJobSerializer(data=self.request.data, context={
|
||||
"event": self.request.event,
|
||||
})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
cf = CachedFile(web_download=False)
|
||||
cf.date = now()
|
||||
cf.expires = now() + timedelta(hours=24)
|
||||
cf.save()
|
||||
async_result = bulk_render.apply_async(args=(
|
||||
self.request.event.id,
|
||||
str(cf.id),
|
||||
[
|
||||
{
|
||||
"orderposition": r["orderposition"].id,
|
||||
"override_layout": r["override_layout"].id if r.get("override_layout") else None,
|
||||
"override_channel": r["override_channel"].id if r.get("override_channel") else None,
|
||||
} for r in serializer.validated_data["parts"]
|
||||
]
|
||||
))
|
||||
|
||||
url_kwargs = {
|
||||
'asyncid': str(async_result.id),
|
||||
'cfid': str(cf.id),
|
||||
}
|
||||
url_kwargs.update(self.kwargs)
|
||||
return Response({
|
||||
'download': reverse('api-v1:ticketpdfrenderer-download', kwargs=url_kwargs, request=self.request)
|
||||
}, status=status.HTTP_202_ACCEPTED)
|
||||
|
||||
@@ -19,26 +19,18 @@
|
||||
# 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/>.
|
||||
#
|
||||
import json
|
||||
import logging
|
||||
from io import BytesIO
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db.models import Prefetch, prefetch_related_objects
|
||||
from pypdf import PdfWriter
|
||||
|
||||
from pretix.base.models import (
|
||||
CachedFile, Checkin, Event, EventMetaValue, ItemMetaValue,
|
||||
ItemVariationMetaValue, OrderPosition, SalesChannel, SubEventMetaValue,
|
||||
cachedfile_name,
|
||||
CachedFile, Event, OrderPosition, cachedfile_name,
|
||||
)
|
||||
from pretix.base.services.orders import OrderError
|
||||
from pretix.base.services.tasks import EventTask
|
||||
from pretix.celery_app import app
|
||||
|
||||
from ...base.i18n import language
|
||||
from ...base.services.export import ExportError
|
||||
from .models import TicketLayout
|
||||
from .ticketoutput import PdfTicketOutput
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -54,93 +46,3 @@ def tickets_create_pdf(event: Event, fileid: int, position: int, channel) -> int
|
||||
file.file.save(cachedfile_name(file, file.filename), ContentFile(data))
|
||||
file.save()
|
||||
return file.pk
|
||||
|
||||
|
||||
@app.task(base=EventTask, throws=(OrderError, ExportError,))
|
||||
def bulk_render(event: Event, fileid: int, parts: list) -> int:
|
||||
file = CachedFile.objects.get(id=fileid)
|
||||
|
||||
channels = SalesChannel.objects.in_bulk([p["override_channel"] for p in parts if p.get("override_channel")])
|
||||
layouts = TicketLayout.objects.in_bulk([p["override_layout"] for p in parts if p.get("override_layout")])
|
||||
|
||||
positions = OrderPosition.objects.all()
|
||||
prefetch_related_objects([event.organizer], 'meta_properties')
|
||||
prefetch_related_objects(
|
||||
[event],
|
||||
Prefetch('meta_values', queryset=EventMetaValue.objects.select_related('property'),
|
||||
to_attr='meta_values_cached'),
|
||||
'questions',
|
||||
'item_meta_properties',
|
||||
)
|
||||
positions = positions.prefetch_related(
|
||||
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
|
||||
Prefetch('item', queryset=event.items.prefetch_related(
|
||||
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
|
||||
to_attr='meta_values_cached')
|
||||
)),
|
||||
'variation',
|
||||
'answers', 'answers__options', 'answers__question',
|
||||
'item__category',
|
||||
'addon_to__answers', 'addon_to__answers__options', 'addon_to__answers__question',
|
||||
Prefetch('addons', positions.select_related('item', 'variation')),
|
||||
Prefetch('subevent', queryset=event.subevents.prefetch_related(
|
||||
Prefetch('meta_values', to_attr='meta_values_cached',
|
||||
queryset=SubEventMetaValue.objects.select_related('property'))
|
||||
)),
|
||||
'linked_media',
|
||||
Prefetch('order', event.orders.select_related('invoice_address').prefetch_related(
|
||||
Prefetch(
|
||||
'positions',
|
||||
positions.prefetch_related(
|
||||
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||
Prefetch('item', queryset=event.items.prefetch_related(
|
||||
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
|
||||
to_attr='meta_values_cached')
|
||||
)),
|
||||
Prefetch('variation', queryset=event.items.prefetch_related(
|
||||
Prefetch('meta_values', ItemVariationMetaValue.objects.select_related('property'),
|
||||
to_attr='meta_values_cached')
|
||||
)),
|
||||
'answers', 'answers__options', 'answers__question',
|
||||
'item__category',
|
||||
Prefetch('subevent', queryset=event.subevents.prefetch_related(
|
||||
Prefetch('meta_values', to_attr='meta_values_cached',
|
||||
queryset=SubEventMetaValue.objects.select_related('property'))
|
||||
)),
|
||||
Prefetch('addons', positions.select_related('item', 'variation', 'seat'))
|
||||
).select_related('addon_to', 'seat', 'addon_to__seat')
|
||||
)
|
||||
))
|
||||
).select_related(
|
||||
'addon_to', 'seat', 'addon_to__seat'
|
||||
)
|
||||
positions = positions.in_bulk([p["orderposition"] for p in parts])
|
||||
|
||||
merger = PdfWriter()
|
||||
for part in parts:
|
||||
p = positions[part["orderposition"]]
|
||||
p.order.event = event # performance optimization
|
||||
with (language(p.order.locale)):
|
||||
kwargs = {}
|
||||
if part.get("override_channel"):
|
||||
kwargs["override_channel"] = channels[part["override_channel"]].identifier
|
||||
if part.get("override_layout"):
|
||||
l = layouts[part["override_layout"]]
|
||||
kwargs["override_layout"] = json.loads(l.layout)
|
||||
kwargs["override_background"] = l.background
|
||||
prov = PdfTicketOutput(
|
||||
event,
|
||||
**kwargs,
|
||||
)
|
||||
filename, ctype, data = prov.generate(p)
|
||||
merger.append(ContentFile(data))
|
||||
|
||||
outbuffer = BytesIO()
|
||||
merger.write(outbuffer)
|
||||
merger.close()
|
||||
outbuffer.seek(0)
|
||||
|
||||
file.type = "application/pdf"
|
||||
file.file.save(cachedfile_name(file, file.filename), ContentFile(outbuffer.getvalue()))
|
||||
file.save()
|
||||
return file.pk
|
||||
|
||||
@@ -23,7 +23,7 @@ from django.urls import re_path
|
||||
|
||||
from pretix.api.urls import event_router
|
||||
from pretix.plugins.ticketoutputpdf.api import (
|
||||
TicketLayoutItemViewSet, TicketLayoutViewSet, TicketRendererViewSet,
|
||||
TicketLayoutItemViewSet, TicketLayoutViewSet,
|
||||
)
|
||||
from pretix.plugins.ticketoutputpdf.views import (
|
||||
LayoutCreate, LayoutDelete, LayoutEditorView, LayoutGetDefault,
|
||||
@@ -48,4 +48,3 @@ urlpatterns = [
|
||||
]
|
||||
event_router.register('ticketlayouts', TicketLayoutViewSet)
|
||||
event_router.register('ticketlayoutitems', TicketLayoutItemViewSet)
|
||||
event_router.register('ticketpdfrenderer', TicketRendererViewSet, basename='ticketpdfrenderer')
|
||||
|
||||
@@ -100,23 +100,10 @@ def get_customer(request):
|
||||
request._cached_customer = None
|
||||
else:
|
||||
session_hash = session.get(hash_session_key)
|
||||
session_auth_hash = customer.get_session_auth_hash()
|
||||
session_hash_verified = session_hash and constant_time_compare(
|
||||
session_hash,
|
||||
session_auth_hash,
|
||||
customer.get_session_auth_hash()
|
||||
)
|
||||
if not session_hash_verified:
|
||||
# If the current secret does not verify the session, try
|
||||
# with the fallback secrets and stop when a matching one is
|
||||
# found.
|
||||
if session_hash and any(
|
||||
constant_time_compare(session_hash, fallback_auth_hash)
|
||||
for fallback_auth_hash in customer.get_session_auth_fallback_hash()
|
||||
):
|
||||
request.session.cycle_key()
|
||||
request.session[hash_session_key] = session_auth_hash
|
||||
session_hash_verified = True
|
||||
|
||||
if session_hash_verified:
|
||||
request._cached_customer = customer
|
||||
else:
|
||||
|
||||
@@ -263,8 +263,6 @@ def get_grouped_items(event, *, channel: SalesChannel, subevent=None, voucher=No
|
||||
|
||||
quotas_to_compute = []
|
||||
for item in items:
|
||||
assert item.event_id == event.pk
|
||||
item.event = event # save a database query if this is looked up
|
||||
if item.has_variations:
|
||||
for v in item.available_variations:
|
||||
for q in v._subevent_quotas:
|
||||
|
||||
@@ -156,10 +156,11 @@ class OrderOpen(EventViewMixin, OrderDetailMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
if not self.order:
|
||||
raise Http404(_('Unknown order code or not authorized to access this order.'))
|
||||
if self.order.check_email_confirm_secret(kwargs.get('hash')) and not self.order.email_known_to_work:
|
||||
self.order.log_action('pretix.event.order.contact.confirmed')
|
||||
self.order.email_known_to_work = True
|
||||
self.order.save(update_fields=['email_known_to_work'])
|
||||
if kwargs.get('hash') == self.order.email_confirm_hash():
|
||||
if not self.order.email_known_to_work:
|
||||
self.order.log_action('pretix.event.order.contact.confirmed')
|
||||
self.order.email_known_to_work = True
|
||||
self.order.save(update_fields=['email_known_to_work'])
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
|
||||
|
||||
@@ -94,13 +94,6 @@ else:
|
||||
pass # os.chown is not available on Windows
|
||||
f.write(SECRET_KEY)
|
||||
|
||||
|
||||
SECRET_KEY_FALLBACKS = []
|
||||
for i in range(10):
|
||||
if config.has_option('django', f'secret_fallback{i}'):
|
||||
SECRET_KEY_FALLBACKS.append(config.get('django', f'secret_fallback{i}'))
|
||||
|
||||
|
||||
# Adjustable settings
|
||||
|
||||
debug_fallback = "runserver" in sys.argv or "runserver_plus" in sys.argv
|
||||
|
||||
+47
-7
@@ -11,7 +11,7 @@
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"rollup": "^2.79.1",
|
||||
"rollup-plugin-vue": "^5.0.1",
|
||||
"vue": "^2.7.16",
|
||||
@@ -1734,13 +1734,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-node-resolve": {
|
||||
"version": "15.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz",
|
||||
"integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==",
|
||||
"version": "15.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
|
||||
"integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==",
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^5.0.1",
|
||||
"@types/resolve": "1.20.2",
|
||||
"deepmerge": "^4.2.2",
|
||||
"is-builtin-module": "^3.2.1",
|
||||
"is-module": "^1.0.0",
|
||||
"resolve": "^1.22.1"
|
||||
},
|
||||
@@ -2101,6 +2102,17 @@
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/builtin-modules": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
|
||||
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
||||
@@ -2767,6 +2779,20 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-builtin-module": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
|
||||
"integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
|
||||
"dependencies": {
|
||||
"builtin-modules": "^3.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
|
||||
@@ -5136,13 +5162,14 @@
|
||||
}
|
||||
},
|
||||
"@rollup/plugin-node-resolve": {
|
||||
"version": "15.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz",
|
||||
"integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==",
|
||||
"version": "15.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
|
||||
"integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==",
|
||||
"requires": {
|
||||
"@rollup/pluginutils": "^5.0.1",
|
||||
"@types/resolve": "1.20.2",
|
||||
"deepmerge": "^4.2.2",
|
||||
"is-builtin-module": "^3.2.1",
|
||||
"is-module": "^1.0.0",
|
||||
"resolve": "^1.22.1"
|
||||
}
|
||||
@@ -5390,6 +5417,11 @@
|
||||
"update-browserslist-db": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"builtin-modules": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
|
||||
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw=="
|
||||
},
|
||||
"call-bind": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
||||
@@ -5888,6 +5920,14 @@
|
||||
"binary-extensions": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"is-builtin-module": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
|
||||
"integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
|
||||
"requires": {
|
||||
"builtin-modules": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"is-core-module": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"vue": "^2.7.16",
|
||||
"rollup": "^2.79.1",
|
||||
"rollup-plugin-vue": "^5.0.1",
|
||||
|
||||
@@ -807,19 +807,6 @@ def test_event_update_plugins_validation(token_client, organizer, event, item, m
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"all_sales_channels": False,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["bar"],
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.content.decode() == ('{"limit_sales_channels":["If \'limit_sales_channels\' is set, the legacy '
|
||||
'attribute \'sales_channels\' must not be set or set to the same list."]}')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_event_test_mode(token_client, organizer, event):
|
||||
|
||||
@@ -782,59 +782,3 @@ def test_custom_rules_country_rate_subtract_from_gross(event):
|
||||
rate=Decimal('100.00'),
|
||||
name='',
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_no_negative_due_to_subtract_from_gross(event):
|
||||
tr = TaxRule(
|
||||
event=event,
|
||||
rate=Decimal("19.00"),
|
||||
price_includes_tax=True,
|
||||
)
|
||||
assert tr.tax(Decimal('100.00'), subtract_from_gross=Decimal('200.00')).gross == Decimal("0.00")
|
||||
tr = TaxRule(
|
||||
event=event,
|
||||
rate=Decimal("0.00"),
|
||||
price_includes_tax=True,
|
||||
)
|
||||
assert tr.tax(Decimal('100.00'), subtract_from_gross=Decimal('200.00')).gross == Decimal("0.00")
|
||||
tr = TaxRule(
|
||||
event=event,
|
||||
rate=Decimal("19.00"),
|
||||
price_includes_tax=False,
|
||||
)
|
||||
assert tr.tax(Decimal('100.00'), subtract_from_gross=Decimal('200.00')).gross == Decimal("0.00")
|
||||
tr = TaxRule(
|
||||
event=event,
|
||||
rate=Decimal("19.00"),
|
||||
price_includes_tax=True,
|
||||
)
|
||||
assert tr.tax(Decimal('100.00'), subtract_from_gross=Decimal('200.00')).gross == Decimal("0.00")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_allow_negative(event):
|
||||
tr = TaxRule(
|
||||
event=event,
|
||||
rate=Decimal("19.00"),
|
||||
price_includes_tax=True,
|
||||
)
|
||||
assert tr.tax(Decimal('-100.00')).gross == Decimal("-100.00")
|
||||
tr = TaxRule(
|
||||
event=event,
|
||||
rate=Decimal("0.00"),
|
||||
price_includes_tax=True,
|
||||
)
|
||||
assert tr.tax(Decimal('-100.00')).gross == Decimal("-100.00")
|
||||
tr = TaxRule(
|
||||
event=event,
|
||||
rate=Decimal("19.00"),
|
||||
price_includes_tax=False,
|
||||
)
|
||||
assert tr.tax(Decimal('-100.00')).gross == Decimal("-119.00")
|
||||
tr = TaxRule(
|
||||
event=event,
|
||||
rate=Decimal("19.00"),
|
||||
price_includes_tax=True,
|
||||
)
|
||||
assert tr.tax(Decimal('-100.00')).gross == Decimal("-100.00")
|
||||
|
||||
@@ -41,7 +41,7 @@ from django.contrib.auth.tokens import (
|
||||
PasswordResetTokenGenerator, default_token_generator,
|
||||
)
|
||||
from django.core import mail as djmail
|
||||
from django.test import RequestFactory, TestCase, override_settings
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils.timezone import now
|
||||
from django_otp.oath import TOTP
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
@@ -50,7 +50,6 @@ from webauthn.authentication.verify_authentication_response import (
|
||||
)
|
||||
|
||||
from pretix.base.models import Organizer, Team, U2FDevice, User
|
||||
from pretix.control.views.auth import process_login
|
||||
from pretix.helpers import security
|
||||
|
||||
|
||||
@@ -893,19 +892,6 @@ class SessionTimeOutTest(TestCase):
|
||||
response = self.client.get('/control/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_plugin_auth_updates_auth_last_used(self):
|
||||
session = self.client.session
|
||||
session['pretix_auth_long_session'] = True
|
||||
session['pretix_auth_login_time'] = int(time.time()) - 3600 * 5
|
||||
session['pretix_auth_last_used'] = int(time.time()) - 3600 * 3 - 60
|
||||
session.save()
|
||||
|
||||
request = RequestFactory().get("/")
|
||||
request.session = self.client.session
|
||||
process_login(request, self.user, keep_logged_in=True)
|
||||
|
||||
assert request.session['pretix_auth_last_used'] >= int(time.time()) - 60
|
||||
|
||||
def test_update_session_activity(self):
|
||||
t1 = int(time.time()) - 5
|
||||
session = self.client.session
|
||||
|
||||
@@ -406,79 +406,6 @@ def test_sendmail_attendee_product_filter(logged_in_client, sendmail_url, event,
|
||||
assert '/order/' not in djmail.outbox[0].body
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_sendmail_attendee_subevent_filter(logged_in_client, sendmail_url, event, item, order, pos):
|
||||
event.settings.attendee_emails_asked = True
|
||||
event.has_subevents = True
|
||||
event.save()
|
||||
with scopes_disabled():
|
||||
se1 = event.subevents.create(name='Subevent FOO', date_from=now())
|
||||
se2 = event.subevents.create(name='Bar', date_from=now())
|
||||
pos.attendee_email = 'attendee1@dummy.test'
|
||||
pos.subevent = se1
|
||||
pos.save()
|
||||
with scopes_disabled():
|
||||
order.positions.create(
|
||||
item=item, price=0, attendee_email='attendee2@dummy.test', subevent=se2
|
||||
)
|
||||
|
||||
djmail.outbox = []
|
||||
response = logged_in_client.post(sendmail_url + 'orders/',
|
||||
{'sendto': 'na',
|
||||
'action': 'send',
|
||||
'recipients': 'attendees',
|
||||
'items': item.pk,
|
||||
'subject_0': 'Test subject',
|
||||
'message_0': 'This is a test file for sending mails.',
|
||||
'subevent': se2.pk,
|
||||
},
|
||||
follow=True)
|
||||
assert response.status_code == 200
|
||||
assert 'alert-success' in response.rendered_content
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].to == ['attendee2@dummy.test']
|
||||
assert '/ticket/' in djmail.outbox[0].body
|
||||
assert '/order/' not in djmail.outbox[0].body
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_sendmail_attendee_subevent_range_filter(logged_in_client, sendmail_url, event, item, order, pos):
|
||||
event.settings.attendee_emails_asked = True
|
||||
event.has_subevents = True
|
||||
event.save()
|
||||
with scopes_disabled():
|
||||
se1 = event.subevents.create(name='Subevent FOO', date_from=datetime.datetime(2023, 7, 6, 1, 2, 3, tzinfo=event.timezone))
|
||||
se2 = event.subevents.create(name='Bar', date_from=datetime.datetime(2023, 8, 9, 1, 2, 3, tzinfo=event.timezone))
|
||||
pos.attendee_email = 'attendee1@dummy.test'
|
||||
pos.subevent = se1
|
||||
pos.save()
|
||||
with scopes_disabled():
|
||||
order.positions.create(
|
||||
item=item, price=0, attendee_email='attendee2@dummy.test', subevent=se2
|
||||
)
|
||||
|
||||
djmail.outbox = []
|
||||
response = logged_in_client.post(sendmail_url + 'orders/',
|
||||
{'sendto': 'na',
|
||||
'action': 'send',
|
||||
'recipients': 'attendees',
|
||||
'items': item.pk,
|
||||
'subject_0': 'Test subject',
|
||||
'message_0': 'This is a test file for sending mails.',
|
||||
'subevents_from_0': '2023-07-01',
|
||||
'subevents_from_1': '00:00:00',
|
||||
'subevents_to_0': '2023-08-01',
|
||||
'subevents_to_1': '00:00:00',
|
||||
},
|
||||
follow=True)
|
||||
assert response.status_code == 200
|
||||
assert 'alert-success' in response.rendered_content
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].to == ['attendee1@dummy.test']
|
||||
assert '/ticket/' in djmail.outbox[0].body
|
||||
assert '/order/' not in djmail.outbox[0].body
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_sendmail_attendee_checkin_filter(logged_in_client, sendmail_url, event, order, checkin_list, item, pos):
|
||||
event.settings.attendee_emails_asked = True
|
||||
|
||||
@@ -21,8 +21,6 @@
|
||||
#
|
||||
import copy
|
||||
import json
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
from django.core.files.base import ContentFile
|
||||
@@ -30,9 +28,7 @@ from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from pretix.base.models import (
|
||||
Event, Item, Order, OrderPosition, Organizer, Team,
|
||||
)
|
||||
from pretix.base.models import Event, Item, Organizer, Team
|
||||
from pretix.plugins.ticketoutputpdf.models import TicketLayoutItem
|
||||
|
||||
|
||||
@@ -43,35 +39,14 @@ def env():
|
||||
organizer=o, name='Dummy', slug='dummy',
|
||||
date_from=now(), plugins='pretix.plugins.banktransfer'
|
||||
)
|
||||
t = Team.objects.create(organizer=event.organizer, can_view_orders=True)
|
||||
t = Team.objects.create(organizer=event.organizer)
|
||||
t.limit_events.add(event)
|
||||
item1 = Item.objects.create(event=event, name="Ticket", default_price=23)
|
||||
tl = event.ticket_layouts.create(
|
||||
name="Foo",
|
||||
default=True,
|
||||
layout='[{"type": "poweredby", "left": "0", "bottom": "0", "size": "1.00", "content": "dark"}]',
|
||||
)
|
||||
tl = event.ticket_layouts.create(name="Foo", default=True, layout='[{"a": 2}]')
|
||||
TicketLayoutItem.objects.create(layout=tl, item=item1, sales_channel=o.sales_channels.get(identifier="web"))
|
||||
return event, tl, item1
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def position(env):
|
||||
item = env[0].items.create(name="Ticket", default_price=3, admission=True)
|
||||
order = Order.objects.create(
|
||||
code='FOO', event=env[0], email='dummy@dummy.test',
|
||||
status=Order.STATUS_PAID, locale='en',
|
||||
datetime=now() - timedelta(days=4),
|
||||
expires=now() - timedelta(hours=4) + timedelta(days=10),
|
||||
total=Decimal('23.00'),
|
||||
sales_channel=env[0].organizer.sales_channels.get(identifier="web"),
|
||||
)
|
||||
return OrderPosition.objects.create(
|
||||
order=order, item=item, variation=None,
|
||||
price=Decimal("23.00"), attendee_name_parts={"full_name": "Peter"}, positionid=1
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
return APIClient()
|
||||
@@ -90,7 +65,7 @@ RES_LAYOUT = {
|
||||
'name': 'Foo',
|
||||
'default': True,
|
||||
'item_assignments': [{'item': 1, 'sales_channel': 'web'}],
|
||||
'layout': [{"type": "poweredby", "left": "0", "bottom": "0", "size": "1.00", "content": "dark"}],
|
||||
'layout': [{'a': 2}],
|
||||
'background': 'http://example.com/static/pretixpresale/pdf/ticket_default_a4.pdf'
|
||||
}
|
||||
|
||||
@@ -238,92 +213,3 @@ def test_api_delete(env, token_client):
|
||||
)
|
||||
assert resp.status_code == 204
|
||||
assert not env[0].ticket_layouts.exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_renderer_batch_valid(env, token_client, position):
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/ticketpdfrenderer/render_batch/'.format(env[0].slug, env[0].slug),
|
||||
{
|
||||
"parts": [
|
||||
{
|
||||
"orderposition": position.pk,
|
||||
},
|
||||
{
|
||||
"orderposition": position.pk,
|
||||
"override_channel": "web",
|
||||
},
|
||||
{
|
||||
"orderposition": position.pk,
|
||||
"override_layout": env[1].pk,
|
||||
},
|
||||
]
|
||||
},
|
||||
format='json',
|
||||
)
|
||||
assert resp.status_code == 202
|
||||
assert "download" in resp.data
|
||||
resp = token_client.get("/" + resp.data["download"].split("/", 3)[3])
|
||||
assert resp.status_code == 200
|
||||
assert resp["Content-Type"] == "application/pdf"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_renderer_batch_invalid(env, token_client, position):
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/ticketpdfrenderer/render_batch/'.format(env[0].slug, env[0].slug),
|
||||
{
|
||||
"parts": [
|
||||
{
|
||||
"orderposition": -2,
|
||||
},
|
||||
]
|
||||
},
|
||||
format='json',
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {"parts": [{"orderposition": ["Invalid pk \"-2\" - object does not exist."]}]}
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/ticketpdfrenderer/render_batch/'.format(env[0].slug, env[0].slug),
|
||||
{
|
||||
"parts": [
|
||||
{
|
||||
"orderposition": position.pk,
|
||||
"override_layout": -2,
|
||||
},
|
||||
]
|
||||
},
|
||||
format='json',
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {"parts": [{"override_layout": ["Invalid pk \"-2\" - object does not exist."]}]}
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/ticketpdfrenderer/render_batch/'.format(env[0].slug, env[0].slug),
|
||||
{
|
||||
"parts": [
|
||||
{
|
||||
"orderposition": position.pk,
|
||||
"override_channel": "magic",
|
||||
},
|
||||
]
|
||||
},
|
||||
format='json',
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {"parts": [{"override_channel": ["Object with identifier=magic does not exist."]}]}
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/ticketpdfrenderer/render_batch/'.format(env[0].slug, env[0].slug),
|
||||
{
|
||||
"parts": [
|
||||
{
|
||||
"orderposition": position.pk,
|
||||
}
|
||||
] * 1002
|
||||
},
|
||||
format='json',
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {"parts": ["Please do not submit more than 1000 parts."]}
|
||||
|
||||
@@ -3654,31 +3654,6 @@ class CartBundleTest(CartTestMixin, TestCase):
|
||||
assert cp.price == Decimal('0.00')
|
||||
assert b.price == Decimal('1.50')
|
||||
|
||||
@classscope(attr='orga')
|
||||
def test_voucher_apply_multiple_reduce_beyond_designated_price_no_tax_rules(self):
|
||||
self.ticket.tax_rule = None
|
||||
self.ticket.save()
|
||||
self.trans.tax_rule = None
|
||||
self.trans.save()
|
||||
cp = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=21.5, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
b = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.trans, addon_to=cp,
|
||||
price=1.5, expires=now() + timedelta(minutes=10), is_bundled=True
|
||||
)
|
||||
v = Voucher.objects.create(
|
||||
event=self.event, price_mode='set', value=Decimal('0.00'), max_usages=100
|
||||
)
|
||||
|
||||
self.cm.apply_voucher(v.code)
|
||||
self.cm.commit()
|
||||
cp.refresh_from_db()
|
||||
b.refresh_from_db()
|
||||
assert cp.price == Decimal('0.00')
|
||||
assert b.price == Decimal('1.50')
|
||||
|
||||
@classscope(attr='orga')
|
||||
def test_voucher_apply_affect_bundled(self):
|
||||
cp = CartPosition.objects.create(
|
||||
|
||||
@@ -628,7 +628,7 @@ def test_change_email(env, client):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_change_pw(env, client, client2):
|
||||
def test_change_pw(env, client):
|
||||
with scopes_disabled():
|
||||
customer = env[0].customers.create(email='john@example.org', is_verified=True)
|
||||
customer.set_password('foo')
|
||||
@@ -640,12 +640,6 @@ def test_change_pw(env, client, client2):
|
||||
})
|
||||
assert r.status_code == 302
|
||||
|
||||
r = client2.post('/bigevents/account/login', {
|
||||
'email': 'john@example.org',
|
||||
'password': 'foo',
|
||||
})
|
||||
assert r.status_code == 302
|
||||
|
||||
r = client.post('/bigevents/account/password', {
|
||||
'password_current': 'invalid',
|
||||
'password': 'aYLBRNg4',
|
||||
@@ -664,13 +658,6 @@ def test_change_pw(env, client, client2):
|
||||
customer.refresh_from_db()
|
||||
assert customer.check_password('aYLBRNg4')
|
||||
|
||||
r = client.get('/bigevents/account/password')
|
||||
assert r.status_code == 200
|
||||
|
||||
# Client 2 got logged out
|
||||
r = client2.post('/bigevents/account/password')
|
||||
assert r.status_code == 302
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_login_per_org(env, client):
|
||||
|
||||
@@ -221,7 +221,7 @@ class OrdersTest(BaseOrdersTest):
|
||||
assert not self.order.email_known_to_work
|
||||
|
||||
response = self.client.get(
|
||||
'/%s/%s/order/%s/%s/open/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret, self.order.email_confirm_secret())
|
||||
'/%s/%s/order/%s/%s/open/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret, self.order.email_confirm_hash())
|
||||
)
|
||||
assert response.status_code == 302
|
||||
self.order.refresh_from_db()
|
||||
|
||||
Reference in New Issue
Block a user