mirror of
https://github.com/pretix/pretix.git
synced 2025-12-07 22:42:26 +00:00
Compare commits
46 Commits
fix-pdf-bg
...
fix-pdf-im
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b876d0a8e | ||
|
|
393a218df5 | ||
|
|
f247eb0568 | ||
|
|
b35a388685 | ||
|
|
6dbbfe3b04 | ||
|
|
b2c49461bc | ||
|
|
23dcdf1fd1 | ||
|
|
1f80e9ef82 | ||
|
|
0969abb460 | ||
|
|
7b5789b110 | ||
|
|
f3b5996b82 | ||
|
|
5dcab59174 | ||
|
|
a2e38bb415 | ||
|
|
0510814aae | ||
|
|
dee2818f5d | ||
|
|
0d7809c36b | ||
|
|
4c494b5265 | ||
|
|
9e85e8c60a | ||
|
|
ab8c71fab8 | ||
|
|
1fa8ea3a12 | ||
|
|
f584d3d5af | ||
|
|
46ae911ade | ||
|
|
85db5698a6 | ||
|
|
09a17b57ce | ||
|
|
826962d6e2 | ||
|
|
f77e79bb38 | ||
|
|
d21e832204 | ||
|
|
119d4f0e04 | ||
|
|
feab6acfbd | ||
|
|
d85a6074ec | ||
|
|
6c813ea299 | ||
|
|
8a903f21ae | ||
|
|
a7f7c64cce | ||
|
|
82969daf37 | ||
|
|
8e9d0fb723 | ||
|
|
ef3d44e581 | ||
|
|
f9055fce9f | ||
|
|
cff0e86fd9 | ||
|
|
f0913fc720 | ||
|
|
23a9f60171 | ||
|
|
faf41c805c | ||
|
|
41cded095c | ||
|
|
90fb034897 | ||
|
|
f4203b7408 | ||
|
|
8a9f14db03 | ||
|
|
a2adf2825a |
@@ -152,6 +152,10 @@ Example::
|
||||
password=abcd
|
||||
host=localhost
|
||||
port=3306
|
||||
sslmode=require
|
||||
sslrootcert=/etc/pretix/postgresql-ca.crt
|
||||
sslcert=/etc/pretix/postgresql-client-crt.crt
|
||||
sslkey=/etc/pretix/postgresql-client-key.key
|
||||
|
||||
``backend``
|
||||
One of ``sqlite3`` and ``postgresql``.
|
||||
@@ -163,6 +167,11 @@ Example::
|
||||
``user``, ``password``, ``host``, ``port``
|
||||
Connection details for the database connection. Empty by default.
|
||||
|
||||
``sslmode``, ``sslrootcert``
|
||||
Connection TLS details for the PostgreSQL database connection. Possible values of ``sslmode`` are ``disable``, ``allow``, ``prefer``, ``require``, ``verify-ca``, and ``verify-full``. ``sslrootcert`` should be the accessible path of the ca certificate. Both values are empty by default.
|
||||
|
||||
``sslcert``, ``sslkey``
|
||||
Connection mTLS details for the PostgreSQL database connection. It's also necessary to specify ``sslmode`` and ``sslrootcert`` parameters, please check the correct values from the TLS part. ``sslcert`` should be the accessible path of the client certificate. ``sslkey`` should be the accessible path of the client key. All values are empty by default.
|
||||
.. _`config-replica`:
|
||||
|
||||
Database replica settings
|
||||
@@ -324,6 +333,10 @@ to speed up various operations::
|
||||
["sentinel_host_3", 26379]
|
||||
]
|
||||
password=password
|
||||
ssl_cert_reqs=required
|
||||
ssl_ca_certs=/etc/pretix/redis-ca.pem
|
||||
ssl_keyfile=/etc/pretix/redis-client-crt.pem
|
||||
ssl_certfile=/etc/pretix/redis-client-key.key
|
||||
|
||||
``location``
|
||||
The location of redis, as a URL of the form ``redis://[:password]@localhost:6379/0``
|
||||
@@ -347,6 +360,22 @@ to speed up various operations::
|
||||
If your redis setup doesn't require a password or you already specified it in the location you can omit this option.
|
||||
If this is set it will be passed to redis as the connection option PASSWORD.
|
||||
|
||||
``ssl_cert_reqs``
|
||||
If this is set it will be passed to redis as the connection option ``SSL_CERT_REQS``.
|
||||
Possible values are ``none``, ``optional``, and ``required``.
|
||||
|
||||
``ssl_ca_certs``
|
||||
If your redis setup doesn't require TLS you can omit this option.
|
||||
If this is set it will be passed to redis as the connection option ``SSL_CA_CERTS``. Possible value is the ca path.
|
||||
|
||||
``ssl_keyfile``
|
||||
If your redis setup doesn't require mTLS you can omit this option.
|
||||
If this is set it will be passed to redis as the connection option ``SSL_KEYFILE``. Possible value is the keyfile path.
|
||||
|
||||
``ssl_certfile``
|
||||
If your redis setup doesn't require mTLS you can omit this option.
|
||||
If this is set it will be passed to redis as the connection option ``SSL_CERTFILE``. Possible value is the certfile path.
|
||||
|
||||
If redis is not configured, pretix will store sessions and locks in the database. If memcached
|
||||
is configured, memcached will be used for caching instead of redis.
|
||||
|
||||
@@ -396,6 +425,8 @@ The two ``transport_options`` entries can be omitted in most cases.
|
||||
If they are present they need to be a valid JSON dictionary.
|
||||
For possible entries in that dictionary see the `Celery documentation`_.
|
||||
|
||||
It is possible the use Redis with TLS/mTLS for the broker or the backend. To do so, it is necessary to specify the TLS identifier ``rediss``, the ssl mode ``ssl_cert_reqs`` and optionally specify the CA (TLS) ``ssl_ca_certs``, cert ``ssl_certfile`` and key ``ssl_keyfile`` (mTLS) path as encoded string. the following uri describes the format and possible parameters ``rediss://0.0.0.0:6379/1?ssl_cert_reqs=required&ssl_ca_certs=%2Fetc%2Fpretix%2Fredis-ca.pem&ssl_certfile=%2Fetc%2Fpretix%2Fredis-client-crt.pem&ssl_keyfile=%2Fetc%2Fpretix%2Fredis-client-key.key``
|
||||
|
||||
To use redis with sentinels set the broker or backend to ``sentinel://sentinel_host_1:26379;sentinel_host_2:26379/0``
|
||||
and the respective transport_options to ``{"master_name":"mymaster"}``.
|
||||
If your redis instances behind the sentinel have a password use ``sentinel://:my_password@sentinel_host_1:26379;sentinel_host_2:26379/0``.
|
||||
|
||||
@@ -61,7 +61,7 @@ Backend
|
||||
item_formsets, order_search_filter_q, order_search_forms
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events, orderposition_blocked_display
|
||||
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events, orderposition_blocked_display, customer_created, customer_signed_in
|
||||
|
||||
Vouchers
|
||||
""""""""
|
||||
|
||||
@@ -70,6 +70,8 @@ The provider class
|
||||
|
||||
.. autoattribute:: settings_form_fields
|
||||
|
||||
.. autoattribute:: walletqueries
|
||||
|
||||
.. automethod:: settings_form_clean
|
||||
|
||||
.. automethod:: settings_content_render
|
||||
|
||||
@@ -945,6 +945,7 @@ with scopes_disabled():
|
||||
| Q(addon_to__attendee_email__icontains=value)
|
||||
| Q(order__code__istartswith=value)
|
||||
| Q(order__invoice_address__name_cached__icontains=value)
|
||||
| Q(order__invoice_address__company__icontains=value)
|
||||
| Q(order__email__icontains=value)
|
||||
| Q(pk__in=matching_media)
|
||||
)
|
||||
|
||||
@@ -140,7 +140,7 @@ class BaseExporter:
|
||||
"""
|
||||
return {}
|
||||
|
||||
def render(self, form_data: dict) -> Tuple[str, str, bytes]:
|
||||
def render(self, form_data: dict) -> Tuple[str, str, Optional[bytes]]:
|
||||
"""
|
||||
Render the exported file and return a tuple consisting of a filename, a file type
|
||||
and file content.
|
||||
|
||||
@@ -26,7 +26,7 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
from django.conf import settings
|
||||
from django.http import Http404, HttpRequest, HttpResponse
|
||||
from django.middleware.common import CommonMiddleware
|
||||
from django.urls import get_script_prefix
|
||||
from django.urls import get_script_prefix, resolve
|
||||
from django.utils import timezone, translation
|
||||
from django.utils.cache import patch_vary_headers
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
@@ -230,6 +230,8 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
)
|
||||
|
||||
def process_response(self, request, resp):
|
||||
url = resolve(request.path_info)
|
||||
|
||||
if settings.DEBUG and resp.status_code >= 400:
|
||||
# Don't use CSP on debug error page as it breaks of Django's fancy error
|
||||
# pages
|
||||
@@ -249,20 +251,26 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
|
||||
h = {
|
||||
'default-src': ["{static}"],
|
||||
'script-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
||||
'script-src': ['{static}'],
|
||||
'object-src': ["'none'"],
|
||||
'frame-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
||||
'frame-src': ['{static}'],
|
||||
'style-src': ["{static}", "{media}"],
|
||||
'connect-src': ["{dynamic}", "{media}", "https://checkout.stripe.com"],
|
||||
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"] + img_src,
|
||||
'connect-src': ["{dynamic}", "{media}"],
|
||||
'img-src': ["{static}", "{media}", "data:"] + img_src,
|
||||
'font-src': ["{static}"],
|
||||
'media-src': ["{static}", "data:"],
|
||||
# form-action is not only used to match on form actions, but also on URLs
|
||||
# form-actions redirect to. In the context of e.g. payment providers or
|
||||
# single-sign-on this can be nearly anything so we cannot really restrict
|
||||
# single-sign-on this can be nearly anything, so we cannot really restrict
|
||||
# this. However, we'll restrict it to HTTPS.
|
||||
'form-action': ["{dynamic}", "https:"] + (['http:'] if settings.SITE_URL.startswith('http://') else []),
|
||||
}
|
||||
# Only include pay.google.com for wallet detection purposes on the Payment selection page
|
||||
if (
|
||||
url.url_name == "event.order.pay.change" or
|
||||
(url.url_name == "event.checkout" and url.kwargs['step'] == "payment")
|
||||
):
|
||||
h['script-src'].append('https://pay.google.com')
|
||||
if settings.LOG_CSP:
|
||||
h['report-uri'] = ["/csp_report/"]
|
||||
if 'Content-Security-Policy' in resp:
|
||||
|
||||
@@ -78,6 +78,16 @@ from pretix.presale.views.cart import cart_session, get_or_create_cart_id
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WalletQueries:
|
||||
APPLEPAY = 'applepay'
|
||||
GOOGLEPAY = 'googlepay'
|
||||
|
||||
WALLETS = (
|
||||
(APPLEPAY, pgettext_lazy('payment', 'Apple Pay')),
|
||||
(GOOGLEPAY, pgettext_lazy('payment', 'Google Pay')),
|
||||
)
|
||||
|
||||
|
||||
class PaymentProviderForm(Form):
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
@@ -436,6 +446,19 @@ class BasePaymentProvider:
|
||||
d['_restrict_to_sales_channels']._as_type = list
|
||||
return d
|
||||
|
||||
@property
|
||||
def walletqueries(self):
|
||||
"""
|
||||
.. warning:: This property is considered **experimental**. It might change or get removed at any time without
|
||||
prior notice.
|
||||
|
||||
A list of wallet payment methods that should be dynamically joined to the public name of the payment method,
|
||||
if they are available to the user.
|
||||
The detection is made on a best effort basis with no guarantees of correctness and actual availability.
|
||||
Wallets that pretix can check for are exposed through ``pretix.base.payment.WalletQueries``.
|
||||
"""
|
||||
return []
|
||||
|
||||
def settings_form_clean(self, cleaned_data):
|
||||
"""
|
||||
Overriding this method allows you to inject custom validation into the settings form.
|
||||
|
||||
@@ -48,6 +48,7 @@ from functools import partial
|
||||
from io import BytesIO
|
||||
|
||||
import jsonschema
|
||||
import reportlab.rl_config
|
||||
from bidi.algorithm import get_display
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles import finders
|
||||
@@ -60,7 +61,8 @@ from django.utils.html import conditional_escape
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _, pgettext
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from pypdf import PdfReader
|
||||
from pypdf import PdfReader, PdfWriter, Transformation
|
||||
from pypdf.generic import RectangleObject
|
||||
from reportlab.graphics import renderPDF
|
||||
from reportlab.graphics.barcode.qr import QrCodeWidget
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
@@ -85,6 +87,9 @@ from pretix.presale.style import get_fonts
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if not settings.DEBUG:
|
||||
reportlab.rl_config.shapeChecking = 0
|
||||
|
||||
|
||||
DEFAULT_VARIABLES = OrderedDict((
|
||||
("secret", {
|
||||
@@ -861,7 +866,7 @@ class Renderer:
|
||||
|
||||
if image_file:
|
||||
try:
|
||||
ir = ThumbnailingImageReader(image_file)
|
||||
ir = ThumbnailingImageReader(image_file.path)
|
||||
ir.resize(float(o['width']) * mm, float(o['height']) * mm, 300)
|
||||
canvas.drawImage(
|
||||
image=ir,
|
||||
@@ -992,7 +997,10 @@ class Renderer:
|
||||
elif o['type'] == "poweredby":
|
||||
self._draw_poweredby(canvas, op, o)
|
||||
if self.bg_pdf:
|
||||
page_size = (self.bg_pdf.pages[0].mediabox[2], self.bg_pdf.pages[0].mediabox[3])
|
||||
page_size = (
|
||||
self.bg_pdf.pages[0].mediabox[2] - self.bg_pdf.pages[0].mediabox[0],
|
||||
self.bg_pdf.pages[0].mediabox[3] - self.bg_pdf.pages[0].mediabox[1]
|
||||
)
|
||||
if self.bg_pdf.pages[0].get('/Rotate') in (90, 270):
|
||||
# swap dimensions due to pdf being rotated
|
||||
page_size = page_size[::-1]
|
||||
@@ -1020,14 +1028,12 @@ class Renderer:
|
||||
with open(os.path.join(d, 'out.pdf'), 'rb') as f:
|
||||
return BytesIO(f.read())
|
||||
else:
|
||||
from pypdf import PdfReader, PdfWriter, Transformation
|
||||
from pypdf.generic import RectangleObject
|
||||
buffer.seek(0)
|
||||
new_pdf = PdfReader(buffer)
|
||||
output = PdfWriter()
|
||||
|
||||
for i, page in enumerate(new_pdf.pages):
|
||||
bg_page = copy.copy(self.bg_pdf.pages[i])
|
||||
bg_page = copy.deepcopy(self.bg_pdf.pages[i])
|
||||
bg_rotation = bg_page.get('/Rotate')
|
||||
if bg_rotation:
|
||||
# /Rotate is clockwise, transformation.rotate is counter-clockwise
|
||||
@@ -1064,6 +1070,56 @@ class Renderer:
|
||||
return outbuffer
|
||||
|
||||
|
||||
def merge_background(fg_pdf, bg_pdf, out_file, compress):
|
||||
if settings.PDFTK:
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
fg_filename = os.path.join(d, 'fg.pdf')
|
||||
bg_filename = os.path.join(d, 'bg.pdf')
|
||||
fg_pdf.write(fg_filename)
|
||||
bg_pdf.write(bg_filename)
|
||||
pdftk_cmd = [
|
||||
settings.PDFTK,
|
||||
fg_filename,
|
||||
'multibackground',
|
||||
bg_filename,
|
||||
'output',
|
||||
'-',
|
||||
]
|
||||
if compress:
|
||||
pdftk_cmd.append('compress')
|
||||
subprocess.run(pdftk_cmd, check=True, stdout=out_file)
|
||||
else:
|
||||
output = PdfWriter()
|
||||
for i, page in enumerate(fg_pdf.pages):
|
||||
bg_page = copy.deepcopy(bg_pdf.pages[i])
|
||||
bg_rotation = bg_page.get('/Rotate')
|
||||
if bg_rotation:
|
||||
# /Rotate is clockwise, transformation.rotate is counter-clockwise
|
||||
t = Transformation().rotate(bg_rotation)
|
||||
w = float(page.mediabox.getWidth())
|
||||
h = float(page.mediabox.getHeight())
|
||||
if bg_rotation in (90, 270):
|
||||
# offset due to rotation base
|
||||
if bg_rotation == 90:
|
||||
t = t.translate(h, 0)
|
||||
else:
|
||||
t = t.translate(0, w)
|
||||
# rotate mediabox as well
|
||||
page.mediabox = RectangleObject((
|
||||
page.mediabox.left.as_numeric(),
|
||||
page.mediabox.bottom.as_numeric(),
|
||||
page.mediabox.top.as_numeric(),
|
||||
page.mediabox.right.as_numeric(),
|
||||
))
|
||||
page.trimbox = page.mediabox
|
||||
elif bg_rotation == 180:
|
||||
t = t.translate(w, h)
|
||||
page.add_transformation(t)
|
||||
bg_page.merge_page(page)
|
||||
output.add_page(bg_page)
|
||||
output.write(out_file)
|
||||
|
||||
|
||||
@deconstructible
|
||||
class PdfLayoutValidator:
|
||||
def __call__(self, value):
|
||||
|
||||
@@ -86,8 +86,8 @@ def _build_time(t=None, value=None, ev=None, now_dt=None):
|
||||
return ev.date_admission or ev.date_from
|
||||
|
||||
|
||||
def _logic_annotate_for_graphic_explain(rules, ev, rule_data):
|
||||
logic_environment = _get_logic_environment(ev)
|
||||
def _logic_annotate_for_graphic_explain(rules, ev, rule_data, now_dt):
|
||||
logic_environment = _get_logic_environment(ev, now_dt)
|
||||
event = ev if isinstance(ev, Event) else ev.event
|
||||
|
||||
def _evaluate_inners(r):
|
||||
@@ -152,7 +152,7 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
||||
get in before 17:00". In the middle of the night it would switch to "You can only get in after 09:00".
|
||||
"""
|
||||
now_dt = now_dt or now()
|
||||
logic_environment = _get_logic_environment(ev)
|
||||
logic_environment = _get_logic_environment(ev, now_dt)
|
||||
_var_values = {'False': False, 'True': True}
|
||||
_var_explanations = {}
|
||||
|
||||
@@ -229,7 +229,7 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
||||
for vname, data in _var_explanations.items():
|
||||
var, operator, rhs = data['var'], data['operator'], data['rhs']
|
||||
if var == 'now':
|
||||
compare_to = _build_time(*rhs[0]['buildTime'], ev=ev).astimezone(ev.timezone)
|
||||
compare_to = _build_time(*rhs[0]['buildTime'], ev=ev, now_dt=now_dt).astimezone(ev.timezone)
|
||||
tolerance = timedelta(minutes=float(rhs[1])) if len(rhs) > 1 and rhs[1] else timedelta(seconds=0)
|
||||
if operator == 'isBefore':
|
||||
compare_to += tolerance
|
||||
@@ -337,7 +337,7 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
||||
return ', '.join(var_texts[v] for v in paths_with_min_weight[0] if not _var_values[v])
|
||||
|
||||
|
||||
def _get_logic_environment(ev):
|
||||
def _get_logic_environment(ev, now_dt):
|
||||
# Every change to our supported JSON logic must be done
|
||||
# * in pretix.base.services.checkin
|
||||
# * in pretix.base.models.checkin
|
||||
@@ -354,7 +354,7 @@ def _get_logic_environment(ev):
|
||||
logic.add_operation('objectList', lambda *objs: list(objs))
|
||||
logic.add_operation('lookup', lambda model, pk, str: int(pk))
|
||||
logic.add_operation('inList', lambda a, b: a in b)
|
||||
logic.add_operation('buildTime', partial(_build_time, ev=ev))
|
||||
logic.add_operation('buildTime', partial(_build_time, ev=ev, now_dt=now_dt))
|
||||
logic.add_operation('isBefore', is_before)
|
||||
logic.add_operation('isAfter', lambda t1, t2, tol=None: is_before(t2, t1, tol))
|
||||
return logic
|
||||
@@ -861,7 +861,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
|
||||
if type == Checkin.TYPE_ENTRY and clist.rules:
|
||||
rule_data = LazyRuleVars(op, clist, dt)
|
||||
logic = _get_logic_environment(op.subevent or clist.event)
|
||||
logic = _get_logic_environment(op.subevent or clist.event, now_dt=dt)
|
||||
if not logic.apply(clist.rules, rule_data):
|
||||
if force:
|
||||
force_used = True
|
||||
|
||||
@@ -26,7 +26,7 @@ from typing import Any, Dict, Union
|
||||
from celery.exceptions import MaxRetriesExceededError
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import connection, transaction
|
||||
from django.db import close_old_connections, connection, transaction
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now, override
|
||||
from django.utils.translation import gettext
|
||||
@@ -86,9 +86,12 @@ def export(self, event: Event, fileid: str, provider: str, form_data: Dict[str,
|
||||
gettext('Your export did not contain any data.')
|
||||
)
|
||||
file.filename, file.type, data = d
|
||||
|
||||
close_old_connections() # This task can run very long, we might need a new DB connection
|
||||
|
||||
f = ContentFile(data)
|
||||
file.file.save(cachedfile_name(file, file.filename), f)
|
||||
return file.pk
|
||||
return str(file.pk)
|
||||
|
||||
|
||||
@app.task(base=ProfiledOrganizerUserTask, throws=(ExportError,), bind=True)
|
||||
@@ -154,9 +157,12 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
|
||||
gettext('Your export did not contain any data.')
|
||||
)
|
||||
file.filename, file.type, data = d
|
||||
|
||||
close_old_connections() # This task can run very long, we might need a new DB connection
|
||||
|
||||
f = ContentFile(data)
|
||||
file.file.save(cachedfile_name(file, file.filename), f)
|
||||
return file.pk
|
||||
return str(file.pk)
|
||||
|
||||
|
||||
def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter, config_url, retry_func, has_permission):
|
||||
@@ -214,6 +220,11 @@ def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter,
|
||||
raise ExportError(
|
||||
gettext('Your exported data exceeded the size limit for scheduled exports.')
|
||||
)
|
||||
|
||||
conn = transaction.get_connection()
|
||||
if not conn.in_atomic_block: # atomic execution only happens during tests or with celery always_eager on
|
||||
close_old_connections() # This task can run very long, we might need a new DB connection
|
||||
|
||||
f = ContentFile(data)
|
||||
file.file.save(cachedfile_name(file, file.filename), f)
|
||||
except ExportEmptyError as e:
|
||||
|
||||
@@ -787,3 +787,23 @@ return a dictionary mapping names of attributes in the settings store to DRF ser
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
customer_created = GlobalSignal()
|
||||
"""
|
||||
Arguments: ``customer``
|
||||
|
||||
This signal is sent out every time a customer account is created. The ``customer``
|
||||
object is given as the first argument.
|
||||
|
||||
The ``sender`` keyword argument will contain the organizer.
|
||||
"""
|
||||
|
||||
customer_signed_in = GlobalSignal()
|
||||
"""
|
||||
Arguments: ``customer``
|
||||
|
||||
This signal is sent out every time a customer signs in. The ``customer`` object
|
||||
is given as the first argument.
|
||||
|
||||
The ``sender`` keyword argument will contain the organizer.
|
||||
"""
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% if spf_warning %}
|
||||
<div class="alert alert-warning">
|
||||
<div class="alert alert-danger">
|
||||
<p>
|
||||
{{ spf_warning }}
|
||||
</p>
|
||||
@@ -70,10 +70,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
{% if spf_warning %}
|
||||
<div class="form-group submit-group">
|
||||
<a href="" class="btn btn-default btn-save">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
placeholder="{% trans "Prefix (optional)" %}">
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control input-xs"
|
||||
id="voucher-bulk-codes-num"
|
||||
id="voucher-bulk-codes-num" max="100000"
|
||||
placeholder="{% trans "Number" context "number_of_things" %}">
|
||||
<div class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" id="voucher-bulk-codes-generate"
|
||||
|
||||
@@ -212,11 +212,21 @@
|
||||
<span class="label label-warning">{% trans "Voucher assigned" %}</span>
|
||||
{% endif %}
|
||||
{% elif e.availability.0 == 100 %}
|
||||
<span class="label label-warning">
|
||||
{% blocktrans with num=e.availability.1 %}
|
||||
Waiting, product {{ num }}x available
|
||||
{% endblocktrans %}
|
||||
</span>
|
||||
{% if e.availability.1|default_if_none:"none" == "none" %}
|
||||
<span class="label label-danger" data-toggle="tooltip"
|
||||
title="{% trans "For safety reasons, the waiting list does not run if the quota is set to unlimited." %}">
|
||||
<span class="fa fa-ban" aria-hidden="true"></span>
|
||||
{% blocktrans trimmed %}
|
||||
Quota unlimited
|
||||
{% endblocktrans %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="label label-warning">
|
||||
{% blocktrans with num=e.availability.1 %}
|
||||
Waiting, product {{ num }}x available
|
||||
{% endblocktrans %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="label label-danger">{% trans "Waiting, product unavailable" %}</span>
|
||||
{% endif %}
|
||||
@@ -226,7 +236,7 @@
|
||||
<a href="{% url "control:event.voucher" organizer=request.event.organizer.slug event=request.event.slug voucher=e.voucher.pk %}">
|
||||
{{ e.voucher }}
|
||||
</a>
|
||||
{% elif not e.voucher and e.availability.0 == 100 %}
|
||||
{% elif not e.voucher and e.availability.0 == 100 and e.availability.1|default_if_none:"none" != "none" %}
|
||||
<button name="assign" value="{{ e.pk }}" class="btn btn-default btn-xs">
|
||||
{% trans "Send a voucher" %}
|
||||
</button>
|
||||
|
||||
@@ -530,7 +530,8 @@ class CheckInListSimulator(EventPermissionRequiredMixin, FormView):
|
||||
and (self.result["status"] in ("ok", "incomplete") or self.result["reason"] == "rules"):
|
||||
op = OrderPosition.objects.get(pk=self.result["position"]["id"])
|
||||
rule_data = LazyRuleVars(op, self.list, form.cleaned_data["datetime"])
|
||||
rule_graph = _logic_annotate_for_graphic_explain(self.list.rules, op.subevent or self.list.event, rule_data)
|
||||
rule_graph = _logic_annotate_for_graphic_explain(self.list.rules, op.subevent or self.list.event, rule_data,
|
||||
form.cleaned_data["datetime"])
|
||||
self.result["rule_graph"] = rule_graph
|
||||
|
||||
if self.result.get("questions"):
|
||||
|
||||
@@ -192,8 +192,8 @@ class MailSettingsSetupView(TemplateView):
|
||||
spf_record = get_spf_record(hostname)
|
||||
if not spf_record:
|
||||
spf_warning = _(
|
||||
'We could not find an SPF record set for the domain you are trying to use. You can still '
|
||||
'proceed, but it will increase the chance of emails going to spam or being rejected. We '
|
||||
'We could not find an SPF record set for the domain you are trying to use. This means that '
|
||||
'there is a very high change most of the emails will be rejected or markes as spam. We '
|
||||
'strongly recommend setting an SPF record on the domain. You can do so through the DNS '
|
||||
'settings at the provider you registered your domain with.'
|
||||
)
|
||||
@@ -205,7 +205,8 @@ class MailSettingsSetupView(TemplateView):
|
||||
'this system in the SPF record.'
|
||||
)
|
||||
|
||||
if settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED:
|
||||
verification = settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED and not spf_warning
|
||||
if verification:
|
||||
if 'verification' in self.request.POST:
|
||||
messages.error(request, _('The verification code was incorrect, please try again.'))
|
||||
else:
|
||||
@@ -230,7 +231,7 @@ class MailSettingsSetupView(TemplateView):
|
||||
context={
|
||||
'basetpl': self.basetpl,
|
||||
'object': self.object,
|
||||
'verification': settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED,
|
||||
'verification': verification,
|
||||
'spf_warning': spf_warning,
|
||||
'spf_record': spf_record,
|
||||
'spf_key': settings.MAIL_CUSTOM_SENDER_SPF_STRING,
|
||||
|
||||
@@ -569,6 +569,8 @@ class VoucherRNG(EventPermissionRequiredMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
try:
|
||||
num = int(request.GET.get('num', '5'))
|
||||
if num > 100_000:
|
||||
return HttpResponseBadRequest()
|
||||
except ValueError: # NOQA
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ class WaitingListQuerySetMixin:
|
||||
return self.request.POST
|
||||
return self.request.GET
|
||||
|
||||
def get_queryset(self):
|
||||
def get_queryset(self, force_filtered=False):
|
||||
qs = WaitingListEntry.objects.filter(
|
||||
event=self.request.event
|
||||
).select_related('item', 'variation', 'voucher').prefetch_related(
|
||||
@@ -135,6 +135,8 @@ class WaitingListQuerySetMixin:
|
||||
qs = qs.filter(
|
||||
id__in=self.request_data.getlist('entry')
|
||||
)
|
||||
elif force_filtered and '__ALL' not in self.request_data:
|
||||
qs = qs.none()
|
||||
|
||||
return qs
|
||||
|
||||
@@ -158,7 +160,7 @@ class WaitingListActionView(EventPermissionRequiredMixin, WaitingListQuerySetMix
|
||||
'forbidden': self.get_queryset().filter(voucher__isnull=False),
|
||||
})
|
||||
elif request.POST.get('action') == 'delete_confirm':
|
||||
for obj in self.get_queryset():
|
||||
for obj in self.get_queryset(force_filtered=True):
|
||||
if not obj.voucher_id:
|
||||
obj.log_action('pretix.event.orders.waitinglist.deleted', user=self.request.user)
|
||||
obj.delete()
|
||||
|
||||
@@ -8,16 +8,16 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-06-27 12:51+0000\n"
|
||||
"PO-Revision-Date: 2023-03-07 03:00+0000\n"
|
||||
"Last-Translator: alemao8 <alevizosfotis@gmail.com>\n"
|
||||
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix/el/"
|
||||
">\n"
|
||||
"PO-Revision-Date: 2023-07-11 11:38+0000\n"
|
||||
"Last-Translator: hara metaxa <metaxahara@gmail.com>\n"
|
||||
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix/el/>"
|
||||
"\n"
|
||||
"Language: el\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 4.15.2\n"
|
||||
"X-Generator: Weblate 4.17\n"
|
||||
|
||||
#: pretix/_base_settings.py:78
|
||||
msgid "English"
|
||||
@@ -31444,17 +31444,13 @@ msgid "Cart expired"
|
||||
msgstr "Το καλάθι έληξε"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_base.html:37
|
||||
#, fuzzy
|
||||
#| msgid "Show information"
|
||||
msgid "Show full cart"
|
||||
msgstr "Εμφάνιση πληροφοριών"
|
||||
msgstr "Εμφάνιση καλαθιού"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_base.html:49
|
||||
#: pretix/presale/templates/pretixpresale/event/index.html:78
|
||||
#, fuzzy
|
||||
#| msgid "This file is from a different event."
|
||||
msgid "Add tickets for a different date"
|
||||
msgstr "Αυτό το αρχείο προέρχεται από διαφορετική εκδήλωση."
|
||||
msgstr "Προσθέστε εισιτήρια για διαφορετική ημερομηνία"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:7
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:9
|
||||
@@ -31849,10 +31845,8 @@ msgstr "από %(minprice)s"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:97
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_product_list.html:89
|
||||
#, fuzzy
|
||||
#| msgid "Show variants"
|
||||
msgid "Hide variants"
|
||||
msgstr "Εμφάνιση παραλλαγών"
|
||||
msgstr "Απόκρυψη παραλλαγών"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:99
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_product_list.html:91
|
||||
@@ -32197,12 +32191,12 @@ msgstr ""
|
||||
"Τα στοιχεία του καλαθιού σας είναι στη διάθεσή σας για %(minutes)s λεπτά."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:486
|
||||
#, 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 "Τα αντικείμενα στο καλάθι σας δεν είναι πλέον αποκλειστικά για εσάς."
|
||||
msgstr ""
|
||||
"Τα αντικείμενα στο καλάθι σας δεν είναι πλέον διαθέσιμα. Μπορείτε να "
|
||||
"επαναλάβετε την διαδικασία εφόσον είναι διαθέσιμα."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:490
|
||||
msgid "Overview of your ordered products."
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,8 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-06-27 12:51+0000\n"
|
||||
"PO-Revision-Date: 2023-06-27 07:11+0000\n"
|
||||
"Last-Translator: Jonathan Berger <drskullster@gmail.com>\n"
|
||||
"PO-Revision-Date: 2023-07-19 17:00+0000\n"
|
||||
"Last-Translator: Ronan LE MEILLAT <ronan.le_meillat@highcanfly.club>\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
|
||||
">\n"
|
||||
"Language: fr\n"
|
||||
@@ -528,28 +528,20 @@ msgid "Test-Mode of shop has been deactivated"
|
||||
msgstr "Le mode de test de la boutique a été désactivé"
|
||||
|
||||
#: pretix/api/webhooks.py:339
|
||||
#, fuzzy
|
||||
#| msgid "Waiting list entry"
|
||||
msgid "Waiting list entry added"
|
||||
msgstr "Saisie de la liste d'attente"
|
||||
msgstr "Ajout d'une inscription sur la liste d'attente"
|
||||
|
||||
#: pretix/api/webhooks.py:343
|
||||
#, fuzzy
|
||||
#| msgid "Waiting list entry"
|
||||
msgid "Waiting list entry changed"
|
||||
msgstr "Saisie de la liste d'attente"
|
||||
msgstr "Changement d'une inscription sur la liste d'attente"
|
||||
|
||||
#: pretix/api/webhooks.py:347
|
||||
#, fuzzy
|
||||
#| msgid "Waiting list entry"
|
||||
msgid "Waiting list entry deleted"
|
||||
msgstr "Saisie de la liste d'attente"
|
||||
msgstr "Suppression d'une inscription de la liste d'attente"
|
||||
|
||||
#: pretix/api/webhooks.py:351
|
||||
#, fuzzy
|
||||
#| msgid "Waiting list entries"
|
||||
msgid "Waiting list entry received voucher"
|
||||
msgstr "Entrées de liste d'attente"
|
||||
msgstr "Bon d’inscription sur la liste d’attente reçu"
|
||||
|
||||
#: pretix/base/addressvalidation.py:100 pretix/base/addressvalidation.py:103
|
||||
#: pretix/base/addressvalidation.py:108 pretix/base/forms/questions.py:938
|
||||
@@ -3406,7 +3398,7 @@ msgstr "Interdit par une règle personnalisée"
|
||||
|
||||
#: pretix/base/models/checkin.py:347
|
||||
msgid "Ticket code revoked/changed"
|
||||
msgstr "Code du billet révoqué/changé"
|
||||
msgstr "Code du billet révoqué/modifié"
|
||||
|
||||
#: pretix/base/models/checkin.py:348
|
||||
msgid "Information required"
|
||||
@@ -3426,7 +3418,7 @@ msgstr "Le code du billet est ambigu dans la liste"
|
||||
|
||||
#: pretix/base/models/checkin.py:352
|
||||
msgid "Server error"
|
||||
msgstr "Erreur du serveur"
|
||||
msgstr "Erreur côté serveur"
|
||||
|
||||
#: pretix/base/models/checkin.py:353
|
||||
msgid "Ticket blocked"
|
||||
@@ -3932,11 +3924,11 @@ msgstr ""
|
||||
|
||||
#: pretix/base/models/exports.py:64
|
||||
msgid "Additional recipients (Cc)"
|
||||
msgstr "Destinataires supplémentaires (Cc)"
|
||||
msgstr "Destinataires supplémentaires (copie)"
|
||||
|
||||
#: pretix/base/models/exports.py:69
|
||||
msgid "Additional recipients (Bcc)"
|
||||
msgstr "Destinataires supplémentaires (Bcc)"
|
||||
msgstr "Destinataires supplémentaires (copie cachée)"
|
||||
|
||||
#: pretix/base/models/exports.py:74 pretix/control/forms/event.py:1045
|
||||
#: pretix/control/forms/event.py:1107 pretix/control/forms/event.py:1119
|
||||
@@ -4224,9 +4216,9 @@ msgid ""
|
||||
"product as an add-on product, but only for fixed bundles!"
|
||||
msgstr ""
|
||||
"Si cette option est définie, le produit ne sera vendu que dans le cadre de "
|
||||
"produits groupés. <strong>Cochez</strong> cette option si vous souhaitez "
|
||||
"utiliser ce produit pour des offres groupées, pas en tant que produit "
|
||||
"complémentaire."
|
||||
"produits groupés. <strong>Ne cochez pas</strong> cette option si vous "
|
||||
"souhaitez utiliser ce produit pour des offres groupées, pas en tant que "
|
||||
"produit complémentaire."
|
||||
|
||||
#: pretix/base/models/items.py:527
|
||||
msgid ""
|
||||
@@ -5530,12 +5522,8 @@ msgid "Seat {number}"
|
||||
msgstr "Siège {number}"
|
||||
|
||||
#: pretix/base/models/tax.py:157
|
||||
#, fuzzy
|
||||
#| msgid "Your layout file is not a valid layout. Error message: {}"
|
||||
msgid "Your set of rules is not valid. Error message: {}"
|
||||
msgstr ""
|
||||
"Votre fichier de mise en page n’est pas une mise en page valide. Message "
|
||||
"d’erreur : {}"
|
||||
msgstr "Votre ensemble de règles n'est pas valide. Message d'erreur : {}"
|
||||
|
||||
#: pretix/base/models/tax.py:168
|
||||
msgid "Official name"
|
||||
@@ -6673,9 +6661,8 @@ msgid "Attendee country"
|
||||
msgstr "Pays du participant"
|
||||
|
||||
#: pretix/base/pdf.py:211
|
||||
#, fuzzy
|
||||
msgid "Pseudonymization ID (lead scanning)"
|
||||
msgstr "ID pseudonyme"
|
||||
msgstr "ID de pseudonymisation (balayage principal)"
|
||||
|
||||
#: pretix/base/pdf.py:217 pretix/base/pdf.py:222
|
||||
msgid "Sample event name"
|
||||
@@ -8048,11 +8035,8 @@ msgstr ""
|
||||
"réessayer."
|
||||
|
||||
#: pretix/base/services/shredder.py:177
|
||||
#, fuzzy
|
||||
#| msgctxt "paypal"
|
||||
#| msgid "Capture completed."
|
||||
msgid "Data shredding completed"
|
||||
msgstr "Capture terminée."
|
||||
msgstr "Déchiquetage de données terminé"
|
||||
|
||||
#: pretix/base/services/stats.py:210
|
||||
msgid "Uncategorized"
|
||||
@@ -8762,7 +8746,7 @@ msgstr ""
|
||||
|
||||
#: pretix/base/settings.py:934
|
||||
msgid "Accept late payments"
|
||||
msgstr "Accepter les paiements en retard"
|
||||
msgstr "Accepter les retards de paiement"
|
||||
|
||||
#: pretix/base/settings.py:935
|
||||
msgid ""
|
||||
@@ -11459,6 +11443,23 @@ msgid ""
|
||||
"\n"
|
||||
"Your pretix team\n"
|
||||
msgstr ""
|
||||
"Bonjour\n"
|
||||
"\n"
|
||||
"Nous confirmons par la présente que le travail de déchiquetage de données "
|
||||
"suivant a été effectué :\n"
|
||||
"\n"
|
||||
"Organisateur : %(organizer)s\n"
|
||||
"\n"
|
||||
"Evénement : %(event)s\n"
|
||||
"\n"
|
||||
"Sélection des données : %(shredders)s\n"
|
||||
"\n"
|
||||
"Heure de début : %(start_time)s (les nouvelles données ajoutées après cette "
|
||||
"heure n’ont peut-être pas été supprimées)\n"
|
||||
"\n"
|
||||
"Sinceres salutations\n"
|
||||
"\n"
|
||||
"Votre équipe pretix\n"
|
||||
|
||||
#: pretix/base/templates/pretixbase/forms/widgets/portrait_image.html:10
|
||||
msgid "Upload photo"
|
||||
@@ -12069,16 +12070,16 @@ msgstr "Saisie de texte libre"
|
||||
|
||||
#: pretix/control/forms/event.py:666
|
||||
msgid "Do not ask"
|
||||
msgstr "Ne pas demandez"
|
||||
msgstr "Ne pas demander"
|
||||
|
||||
#: pretix/control/forms/event.py:667
|
||||
msgid "Ask, but do not require input"
|
||||
msgstr "Demandez, mais n’avez pas besoin d’entrée"
|
||||
msgstr "Demander, mais ne pas exiger de saisie"
|
||||
|
||||
#: pretix/control/forms/event.py:668
|
||||
#: pretix/control/templates/pretixcontrol/event/settings.html:74
|
||||
msgid "Ask and require input"
|
||||
msgstr "Demandez et requérez la saise"
|
||||
msgstr "Demander et exiger la saisie"
|
||||
|
||||
#: pretix/control/forms/event.py:740
|
||||
msgid ""
|
||||
@@ -13015,7 +13016,7 @@ msgstr "Appareils révoqués"
|
||||
|
||||
#: pretix/control/forms/global_settings.py:59
|
||||
msgid "Additional footer text"
|
||||
msgstr "Texte de pied de page supplémentaire"
|
||||
msgstr "Texte supplémentaire en bas de page"
|
||||
|
||||
#: pretix/control/forms/global_settings.py:60
|
||||
msgid "Will be included as additional text in the footer, site-wide."
|
||||
@@ -14829,11 +14830,11 @@ msgstr "Un événement a été supprimé."
|
||||
|
||||
#: pretix/control/logdisplay.py:375
|
||||
msgid "A removal process for personal data has been started."
|
||||
msgstr ""
|
||||
msgstr "Un processus de suppression des données personnelles a été lancé."
|
||||
|
||||
#: pretix/control/logdisplay.py:376
|
||||
msgid "A removal process for personal data has been completed."
|
||||
msgstr ""
|
||||
msgstr "Un processus de suppression des données personnelles a été achevé."
|
||||
|
||||
#: pretix/control/logdisplay.py:377
|
||||
msgid "The order details have been changed."
|
||||
@@ -15889,7 +15890,7 @@ msgstr "Tous les utilisateurs"
|
||||
#: pretix/control/templates/pretixcontrol/user/staff_session_list.html:5
|
||||
#: pretix/control/templates/pretixcontrol/user/staff_session_list.html:7
|
||||
msgid "Admin sessions"
|
||||
msgstr "Sessions d'administration"
|
||||
msgstr "Sessions Admin"
|
||||
|
||||
#: pretix/control/navigation.py:427
|
||||
#: pretix/control/templates/pretixcontrol/global_settings_base.html:5
|
||||
@@ -18380,7 +18381,7 @@ msgstr ""
|
||||
"Vous pouvez ici définir un ensemble de propriétés de métadonnées (c’est-à-"
|
||||
"dire des variables) que vous pouvez définir ultérieurement pour vos articles "
|
||||
"et réutiliser dans des endroits tels que les mises en page de tickets. C’est "
|
||||
"un gain de temps utile si vous créez beaucoup, beaucoup d’éléments."
|
||||
"un gain de temps précieux si vous créez beaucoup d'articles."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/settings.html:389
|
||||
#: pretix/control/templates/pretixcontrol/event/settings.html:417
|
||||
@@ -19012,7 +19013,7 @@ msgstr "Type de produit"
|
||||
#: pretix/control/templates/pretixcontrol/item/create.html:25
|
||||
#: pretix/control/templates/pretixcontrol/item/index.html:33
|
||||
msgid "Admission product"
|
||||
msgstr "Produit d’admission"
|
||||
msgstr "Produit d'admission"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/item/create.html:27
|
||||
#: pretix/control/templates/pretixcontrol/item/index.html:35
|
||||
@@ -19275,7 +19276,7 @@ msgstr "Nouvelle variante"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/item/include_variations.html:215
|
||||
msgid "Add a new variation"
|
||||
msgstr "Ajouter une nouvelle variante"
|
||||
msgstr "Ajouter une nouvelle variation"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/item/index.html:153
|
||||
msgid "Availability"
|
||||
@@ -19565,7 +19566,7 @@ msgstr "Billet d’entrée personnalisé"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/index.html:87
|
||||
msgid "Admission ticket without personalization"
|
||||
msgstr "Billet d’entrée sans personnalisation"
|
||||
msgstr "Billet d'entrée sans personnalisation"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/index.html:95
|
||||
msgid "Product with variations"
|
||||
@@ -21598,9 +21599,8 @@ msgstr ""
|
||||
"événements."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html:79
|
||||
#, fuzzy
|
||||
msgid "Lifetime spending"
|
||||
msgstr "Paiement en attente"
|
||||
msgstr "Dépenses à vie"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html:101
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer_anonymize.html:39
|
||||
@@ -23036,6 +23036,9 @@ msgid ""
|
||||
"Depending on the amount of data in your event, the following step may take a "
|
||||
"while to complete. We will inform you via email once it has been completed."
|
||||
msgstr ""
|
||||
"Selon la quantité de données de votre événement, l’étape suivante peut "
|
||||
"prendre un certain temps. Nous vous informerons par e-mail une fois qu’il "
|
||||
"aura été complété."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/shredder/index.html:11
|
||||
msgid ""
|
||||
@@ -23628,7 +23631,7 @@ msgstr "On"
|
||||
#: pretix/control/templates/pretixcontrol/user/notifications.html:71
|
||||
#: pretix/control/templates/pretixcontrol/user/settings.html:24
|
||||
msgid "Off"
|
||||
msgstr "Off"
|
||||
msgstr "Désactivé"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/user/notifications.html:75
|
||||
msgid "You have no permission to receive this notification"
|
||||
@@ -26569,8 +26572,8 @@ msgid ""
|
||||
"We will assign you a personal reference code to use after you completed the "
|
||||
"order."
|
||||
msgstr ""
|
||||
"Nous vous assignerons un code de référence personnel à utiliser après que "
|
||||
"vous ayez complété la commande."
|
||||
"Nous vous assignerons un code de référence personnel à utiliser après avoir "
|
||||
"complété la commande."
|
||||
|
||||
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_confirm.html:35
|
||||
#, python-format
|
||||
@@ -27096,9 +27099,8 @@ msgstr ""
|
||||
"liste d’enregistrement."
|
||||
|
||||
#: pretix/plugins/checkinlists/exporters.py:479
|
||||
#, fuzzy
|
||||
msgid "Checked out"
|
||||
msgstr "Paiement"
|
||||
msgstr "Passé en caisse"
|
||||
|
||||
#: pretix/plugins/checkinlists/exporters.py:479
|
||||
#: pretix/plugins/checkinlists/exporters.py:670
|
||||
@@ -27385,11 +27387,11 @@ msgid ""
|
||||
"payment methods world-wide."
|
||||
msgstr ""
|
||||
"Acceptez les paiements avec votre compte PayPal. En plus des paiements "
|
||||
"PayPal réguliers, vous pouvez désormais également proposer des paiements "
|
||||
"dans une variété de méthodes de paiement locales telles que giropay, SOFORT, "
|
||||
"iDEAL et bien d’autres à vos clients - ils n’ont même pas besoin d’un compte "
|
||||
"PayPal. PayPal est l’une des méthodes de paiement les plus populaires au "
|
||||
"monde."
|
||||
"PayPal standards, vous pouvez désormais également proposer des paiements "
|
||||
"dans une variété de méthodes de paiement locales à vos clients telles que "
|
||||
"giropay, SOFORT, iDEAL et bien d’autres - ils n’ont même pas besoin d’un "
|
||||
"compte PayPal. PayPal est l’une des méthodes de paiement les plus populaires "
|
||||
"au monde."
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py:95
|
||||
msgid "PayPal Merchant ID"
|
||||
@@ -28329,10 +28331,9 @@ msgid "Create a new rule"
|
||||
msgstr "Créer une nouvelle règle"
|
||||
|
||||
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/rule_list.html:33
|
||||
#, fuzzy
|
||||
msgctxt "subevent"
|
||||
msgid "Sent / Total dates"
|
||||
msgstr "date de début d'événement"
|
||||
msgstr "Envoyés / Dates total"
|
||||
|
||||
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/rule_list.html:52
|
||||
msgid "Next execution:"
|
||||
|
||||
@@ -7,7 +7,7 @@ msgstr ""
|
||||
"Project-Id-Version: French\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-06-27 12:51+0000\n"
|
||||
"PO-Revision-Date: 2023-06-17 09:09+0000\n"
|
||||
"PO-Revision-Date: 2023-07-19 17:00+0000\n"
|
||||
"Last-Translator: Ronan LE MEILLAT <ronan.le_meillat@highcanfly.club>\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
"fr/>\n"
|
||||
@@ -281,7 +281,7 @@ msgstr "Entrée non autorisée"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
||||
msgid "Ticket code revoked/changed"
|
||||
msgstr "Code du billet révoqué/changé"
|
||||
msgstr "Code du billet révoqué/modifié"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
||||
msgid "Ticket blocked"
|
||||
|
||||
28368
src/pretix/locale/id/LC_MESSAGES/django.po
Normal file
28368
src/pretix/locale/id/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,10 +7,10 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-06-27 12:51+0000\n"
|
||||
"PO-Revision-Date: 2023-06-07 15:04+0000\n"
|
||||
"Last-Translator: Thomas Vranken <thvranken@gmail.com>\n"
|
||||
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/"
|
||||
">\n"
|
||||
"PO-Revision-Date: 2023-07-16 22:00+0000\n"
|
||||
"Last-Translator: Freek Engelbarts <freekengelbarts@gmail.com>\n"
|
||||
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/>"
|
||||
"\n"
|
||||
"Language: nl\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -40,7 +40,7 @@ msgstr "Chinees (versimpeld)"
|
||||
|
||||
#: pretix/_base_settings.py:83
|
||||
msgid "Chinese (traditional)"
|
||||
msgstr ""
|
||||
msgstr "Chinees (traditioneel)"
|
||||
|
||||
#: pretix/_base_settings.py:84
|
||||
msgid "Czech"
|
||||
@@ -522,22 +522,16 @@ msgid "Test-Mode of shop has been deactivated"
|
||||
msgstr "Testmode van winkel gedeactiveerd"
|
||||
|
||||
#: pretix/api/webhooks.py:339
|
||||
#, fuzzy
|
||||
#| msgid "Waiting list entry"
|
||||
msgid "Waiting list entry added"
|
||||
msgstr "Wachtlijstitem"
|
||||
msgstr "Wachtlijstitem toegevoegd"
|
||||
|
||||
#: pretix/api/webhooks.py:343
|
||||
#, fuzzy
|
||||
#| msgid "Waiting list entry"
|
||||
msgid "Waiting list entry changed"
|
||||
msgstr "Wachtlijstitem"
|
||||
msgstr "Wachtlijstitem aangepast"
|
||||
|
||||
#: pretix/api/webhooks.py:347
|
||||
#, fuzzy
|
||||
#| msgid "Waiting list entry"
|
||||
msgid "Waiting list entry deleted"
|
||||
msgstr "Wachtlijstitem"
|
||||
msgstr "Wachtlijstitem verwijderd"
|
||||
|
||||
#: pretix/api/webhooks.py:351
|
||||
#, fuzzy
|
||||
@@ -1679,7 +1673,7 @@ msgstr "Toon alleen met geldig lidmaatschap"
|
||||
|
||||
#: pretix/base/exporters/json.py:51 pretix/base/exporters/orderlist.py:85
|
||||
msgid "Order data"
|
||||
msgstr "Besteldatums"
|
||||
msgstr "Bestelgegevens"
|
||||
|
||||
#: pretix/base/exporters/json.py:53
|
||||
msgid ""
|
||||
@@ -3199,11 +3193,9 @@ msgid "Modern Invoice Renderer (pretix 2.7)"
|
||||
msgstr "Moderne factuurrenderer (pretix 2.7)"
|
||||
|
||||
#: pretix/base/invoice.py:947
|
||||
#, fuzzy
|
||||
#| msgid "Please enter a valid state."
|
||||
msgctxt "invoice"
|
||||
msgid "(Please quote at all times.)"
|
||||
msgstr "Kies een geldige staat."
|
||||
msgstr "(Gelieve steeds te vermelden.)"
|
||||
|
||||
#: pretix/base/media.py:58
|
||||
msgid "Barcode / QR-Code"
|
||||
@@ -6559,7 +6551,7 @@ msgstr "Productcategorie"
|
||||
|
||||
#: pretix/base/pdf.py:151 pretix/base/pdf.py:156
|
||||
msgid "123.45 EUR"
|
||||
msgstr "123,45 EUR"
|
||||
msgstr "€123,45"
|
||||
|
||||
#: pretix/base/pdf.py:155
|
||||
msgid "Price including add-ons"
|
||||
@@ -16069,7 +16061,7 @@ msgstr "Meldingen"
|
||||
|
||||
#: pretix/control/navigation.py:390
|
||||
msgid "2FA"
|
||||
msgstr "2FA"
|
||||
msgstr "tweefactorauthenticatie"
|
||||
|
||||
#: pretix/control/navigation.py:395
|
||||
msgid "Authorized apps"
|
||||
@@ -20400,7 +20392,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/change.html:250
|
||||
msgid "–"
|
||||
msgstr ""
|
||||
msgstr "-"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/change.html:274
|
||||
#, fuzzy
|
||||
@@ -31047,10 +31039,8 @@ msgstr "Toon volgende week, %(week)s"
|
||||
#: pretix/presale/templates/pretixpresale/fragment_week_calendar.html:57
|
||||
#: pretix/presale/templates/pretixpresale/organizers/index.html:85
|
||||
#: pretix/presale/views/widget.py:376
|
||||
#, fuzzy
|
||||
#| msgid "PDF ticket layout"
|
||||
msgid "Few tickets left"
|
||||
msgstr "PDF-ticketlay-out"
|
||||
msgstr "Laatste kaarten"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_list.html:28
|
||||
#: pretix/presale/templates/pretixpresale/fragment_calendar.html:75
|
||||
@@ -31058,11 +31048,9 @@ msgstr "PDF-ticketlay-out"
|
||||
#: pretix/presale/templates/pretixpresale/fragment_week_calendar.html:60
|
||||
#: pretix/presale/templates/pretixpresale/organizers/index.html:88
|
||||
#: pretix/presale/views/widget.py:381
|
||||
#, fuzzy
|
||||
#| msgid "Pay now"
|
||||
msgctxt "available_event_in_list"
|
||||
msgid "Buy now"
|
||||
msgstr "Betaal nu"
|
||||
msgstr "Koop nu"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_list.html:30
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_list.html:45
|
||||
|
||||
@@ -8,14 +8,16 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-06-27 12:51+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
"PO-Revision-Date: 2023-07-02 06:00+0000\n"
|
||||
"Last-Translator: Thomas Vranken <thvranken@gmail.com>\n"
|
||||
"Language-Team: Dutch (Belgium) <https://translate.pretix.eu/projects/pretix/"
|
||||
"pretix/nl_BE/>\n"
|
||||
"Language: nl_BE\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 4.17\n"
|
||||
|
||||
#: pretix/_base_settings.py:78
|
||||
msgid "English"
|
||||
@@ -3078,7 +3080,7 @@ msgstr ""
|
||||
#: pretix/base/invoice.py:947
|
||||
msgctxt "invoice"
|
||||
msgid "(Please quote at all times.)"
|
||||
msgstr ""
|
||||
msgstr "(Gelieve steeds te vermelden.)"
|
||||
|
||||
#: pretix/base/media.py:58
|
||||
msgid "Barcode / QR-Code"
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-06-27 12:51+0000\n"
|
||||
"PO-Revision-Date: 2023-02-23 23:00+0000\n"
|
||||
"Last-Translator: Toon Toetenel <toon@toetenel.com>\n"
|
||||
"PO-Revision-Date: 2023-07-16 22:00+0000\n"
|
||||
"Last-Translator: Freek Engelbarts <freekengelbarts@gmail.com>\n"
|
||||
"Language-Team: Dutch (informal) <https://translate.pretix.eu/projects/pretix/"
|
||||
"pretix/nl_Informal/>\n"
|
||||
"Language: nl_Informal\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 4.15.2\n"
|
||||
"X-Generator: Weblate 4.17\n"
|
||||
|
||||
#: pretix/_base_settings.py:78
|
||||
msgid "English"
|
||||
@@ -41,11 +41,11 @@ msgstr "Chinees (versimpeld)"
|
||||
|
||||
#: pretix/_base_settings.py:83
|
||||
msgid "Chinese (traditional)"
|
||||
msgstr ""
|
||||
msgstr "Chinees (traditioneel)"
|
||||
|
||||
#: pretix/_base_settings.py:84
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
msgstr "Tsjechisch"
|
||||
|
||||
#: pretix/_base_settings.py:85
|
||||
msgid "Danish"
|
||||
@@ -69,7 +69,7 @@ msgstr "Fins"
|
||||
|
||||
#: pretix/_base_settings.py:90
|
||||
msgid "Galician"
|
||||
msgstr ""
|
||||
msgstr "Galicisch"
|
||||
|
||||
#: pretix/_base_settings.py:91
|
||||
msgid "Greek"
|
||||
@@ -97,7 +97,7 @@ msgstr "Portugees (Brazilië)"
|
||||
|
||||
#: pretix/_base_settings.py:97
|
||||
msgid "Romanian"
|
||||
msgstr ""
|
||||
msgstr "Roemeens"
|
||||
|
||||
#: pretix/_base_settings.py:98
|
||||
msgid "Russian"
|
||||
@@ -113,7 +113,7 @@ msgstr "Turks"
|
||||
|
||||
#: pretix/_base_settings.py:101
|
||||
msgid "Ukrainian"
|
||||
msgstr ""
|
||||
msgstr "Oekraïens"
|
||||
|
||||
#: pretix/api/auth/devicesecurity.py:31
|
||||
msgid ""
|
||||
@@ -762,17 +762,17 @@ msgstr "Antwoorden op vragen"
|
||||
#: pretix/base/exporters/orderlist.py:1189
|
||||
#: pretix/plugins/reports/exporters.py:451
|
||||
#: pretix/plugins/reports/exporters.py:624
|
||||
#, fuzzy
|
||||
#| msgid "Order data"
|
||||
msgctxt "export_category"
|
||||
msgid "Order data"
|
||||
msgstr "Besteldatums"
|
||||
msgstr "Bestelgegevens"
|
||||
|
||||
#: pretix/base/exporters/answers.py:54
|
||||
msgid ""
|
||||
"Download a ZIP file including all files that have been uploaded by your "
|
||||
"customers while creating an order."
|
||||
msgstr ""
|
||||
"Download een ZIP-bestand dat alle bestanden bevat die bij bestellingen als "
|
||||
"antwoord op een vraag geüpload werden."
|
||||
|
||||
#: pretix/base/exporters/answers.py:64 pretix/base/models/items.py:1565
|
||||
#: pretix/control/navigation.py:182
|
||||
@@ -1709,7 +1709,7 @@ msgstr "Verberg zonder geldig lidmaatschap"
|
||||
|
||||
#: pretix/base/exporters/json.py:51 pretix/base/exporters/orderlist.py:85
|
||||
msgid "Order data"
|
||||
msgstr "Besteldatums"
|
||||
msgstr "Bestelgegevens"
|
||||
|
||||
#: pretix/base/exporters/json.py:53
|
||||
msgid ""
|
||||
@@ -2305,10 +2305,8 @@ msgid "Transaction time"
|
||||
msgstr "Transactiecode"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:833
|
||||
#, fuzzy
|
||||
#| msgid "Order data"
|
||||
msgid "Old data"
|
||||
msgstr "Besteldatums"
|
||||
msgstr "Oude gegevens"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:836 pretix/base/models/items.py:1361
|
||||
#: pretix/control/templates/pretixcontrol/order/transactions.html:22
|
||||
@@ -3249,11 +3247,9 @@ msgid "Modern Invoice Renderer (pretix 2.7)"
|
||||
msgstr "Moderne factuurrenderer (pretix 2.7)"
|
||||
|
||||
#: pretix/base/invoice.py:947
|
||||
#, fuzzy
|
||||
#| msgid "Please enter a valid state."
|
||||
msgctxt "invoice"
|
||||
msgid "(Please quote at all times.)"
|
||||
msgstr "Kies een geldige staat."
|
||||
msgstr "(Gelieve steeds te vermelden.)"
|
||||
|
||||
#: pretix/base/media.py:58
|
||||
msgid "Barcode / QR-Code"
|
||||
@@ -15231,10 +15227,8 @@ msgid "The order's internal comment has been updated."
|
||||
msgstr "Het interne commentaar van de bestelling is bijgewerkt."
|
||||
|
||||
#: pretix/control/logdisplay.py:405
|
||||
#, fuzzy
|
||||
#| msgid "The order of items has been updated."
|
||||
msgid "The order's follow-up date has been updated."
|
||||
msgstr "De volgorde van items is bijgewerkt."
|
||||
msgstr "De opvolgdatum van de bestelling werd bijgewerkt."
|
||||
|
||||
#: pretix/control/logdisplay.py:406
|
||||
msgid "The order's flag to require attention at check-in has been toggled."
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-06-27 12:51+0000\n"
|
||||
"PO-Revision-Date: 2023-05-27 22:00+0000\n"
|
||||
"PO-Revision-Date: 2023-07-04 06:00+0000\n"
|
||||
"Last-Translator: Maciej Szymczak <maciej+github@szymczak.at>\n"
|
||||
"Language-Team: Polish <https://translate.pretix.eu/projects/pretix/pretix/pl/"
|
||||
">\n"
|
||||
@@ -42,7 +42,7 @@ msgstr "Chiński (uproszczony)"
|
||||
|
||||
#: pretix/_base_settings.py:83
|
||||
msgid "Chinese (traditional)"
|
||||
msgstr ""
|
||||
msgstr "Chiński (tradycyjny)"
|
||||
|
||||
#: pretix/_base_settings.py:84
|
||||
msgid "Czech"
|
||||
@@ -244,8 +244,6 @@ msgid "Item meta data property '{name}' does not exist."
|
||||
msgstr "Klucz metadanych '{name}' nie istnieje."
|
||||
|
||||
#: pretix/api/serializers/item.py:182 pretix/control/forms/item.py:1082
|
||||
#, fuzzy
|
||||
#| msgid "The add-on's category must belong to the same event as the item."
|
||||
msgid "The bundled item must not be the same item as the bundling one."
|
||||
msgstr "Dodatek w pakiecie nie może być tym samym co bilet."
|
||||
|
||||
@@ -439,7 +437,6 @@ msgid "External refund of payment"
|
||||
msgstr "Zewnętrzny zwrot płatności"
|
||||
|
||||
#: pretix/api/webhooks.py:258
|
||||
#, fuzzy
|
||||
msgid "Refund of payment requested by customer"
|
||||
msgstr "Żądanie zwrotu płatności zgłoszone przez klienta"
|
||||
|
||||
@@ -527,28 +524,20 @@ msgid "Test-Mode of shop has been deactivated"
|
||||
msgstr "Tryb testowy sklepu został wyłączony"
|
||||
|
||||
#: pretix/api/webhooks.py:339
|
||||
#, fuzzy
|
||||
#| msgid "Waiting list entry"
|
||||
msgid "Waiting list entry added"
|
||||
msgstr "Numer na liście"
|
||||
msgstr "Dodano wpis na listę oczekujących"
|
||||
|
||||
#: pretix/api/webhooks.py:343
|
||||
#, fuzzy
|
||||
#| msgid "Waiting list entry"
|
||||
msgid "Waiting list entry changed"
|
||||
msgstr "Numer na liście"
|
||||
msgstr "Zmieniono wpis na liście oczekujących"
|
||||
|
||||
#: pretix/api/webhooks.py:347
|
||||
#, fuzzy
|
||||
#| msgid "Waiting list entry"
|
||||
msgid "Waiting list entry deleted"
|
||||
msgstr "Numer na liście"
|
||||
msgstr "Usunięto wpis z listy oczekujących"
|
||||
|
||||
#: pretix/api/webhooks.py:351
|
||||
#, fuzzy
|
||||
#| msgid "Waiting list entries"
|
||||
msgid "Waiting list entry received voucher"
|
||||
msgstr "Numery na liście"
|
||||
msgstr "Osoba z listy oczekujących dostała voucher"
|
||||
|
||||
#: pretix/base/addressvalidation.py:100 pretix/base/addressvalidation.py:103
|
||||
#: pretix/base/addressvalidation.py:108 pretix/base/forms/questions.py:938
|
||||
@@ -564,11 +553,11 @@ msgstr "To pole jest wymagane."
|
||||
|
||||
#: pretix/base/addressvalidation.py:213
|
||||
msgid "Enter a postal code in the format XXX."
|
||||
msgstr ""
|
||||
msgstr "Wprowadź kod pocztowy w formacie XXX."
|
||||
|
||||
#: pretix/base/addressvalidation.py:222 pretix/base/addressvalidation.py:224
|
||||
msgid "Enter a postal code in the format XXXX."
|
||||
msgstr ""
|
||||
msgstr "Wprowadź kod pocztowy w formacie XXXX."
|
||||
|
||||
#: pretix/base/auth.py:143
|
||||
#, python-brace-format
|
||||
@@ -637,8 +626,7 @@ msgid "Incompatible SSO provider: \"{error}\"."
|
||||
msgstr "Niekompatybilny dostawca SSO: \"{error}\"."
|
||||
|
||||
#: pretix/base/customersso/oidc.py:109
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "Presale not started"
|
||||
#, python-brace-format
|
||||
msgid "You are not requesting \"{scope}\"."
|
||||
msgstr "Nie prosisz o \"{scope}\"."
|
||||
|
||||
@@ -758,8 +746,6 @@ msgstr "Przesyłanie plików z odpowiedziami na pytania"
|
||||
#: pretix/base/exporters/orderlist.py:1189
|
||||
#: pretix/plugins/reports/exporters.py:451
|
||||
#: pretix/plugins/reports/exporters.py:624
|
||||
#, fuzzy
|
||||
#| msgid "Order data"
|
||||
msgctxt "export_category"
|
||||
msgid "Order data"
|
||||
msgstr "Dane zamówienia"
|
||||
@@ -785,8 +771,6 @@ msgid "Customer accounts"
|
||||
msgstr "Konta klientów"
|
||||
|
||||
#: pretix/base/exporters/customers.py:51
|
||||
#, fuzzy
|
||||
#| msgid "Cart positions"
|
||||
msgctxt "export_category"
|
||||
msgid "Customer accounts"
|
||||
msgstr "Rachunki klientów"
|
||||
@@ -976,8 +960,6 @@ msgid "No"
|
||||
msgstr "Nie"
|
||||
|
||||
#: pretix/base/exporters/dekodi.py:42 pretix/base/exporters/invoices.py:66
|
||||
#, fuzzy
|
||||
#| msgid "Invoices"
|
||||
msgctxt "export_category"
|
||||
msgid "Invoices"
|
||||
msgstr "Faktury"
|
||||
@@ -1688,8 +1670,6 @@ msgstr "Wymagaj ważnego członkostwa"
|
||||
|
||||
#: pretix/base/exporters/items.py:94 pretix/base/models/items.py:580
|
||||
#: pretix/base/models/items.py:1041
|
||||
#, fuzzy
|
||||
#| msgid "Team members"
|
||||
msgid "Hide without a valid membership"
|
||||
msgstr "Ukryj dla osób bez ważnego członkostwa"
|
||||
|
||||
@@ -1759,9 +1739,6 @@ msgid "Only paid orders"
|
||||
msgstr "Tylko opłacone zamówienia"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:115
|
||||
#, fuzzy
|
||||
#| msgctxt "checkin"
|
||||
#| msgid "Include pending orders"
|
||||
msgid "Include payment amounts"
|
||||
msgstr "Uwzględnij kwoty płatności"
|
||||
|
||||
@@ -1939,7 +1916,6 @@ msgstr "Kanał sprzedaży"
|
||||
#: pretix/base/exporters/orderlist.py:279
|
||||
#: pretix/base/exporters/orderlist.py:583 pretix/base/models/orders.py:233
|
||||
#: pretix/control/forms/filter.py:238
|
||||
#, fuzzy
|
||||
msgid "Follow-up date"
|
||||
msgstr "Data kontynuacji"
|
||||
|
||||
@@ -2309,18 +2285,13 @@ msgstr "Identyfikator zasady podatkowej"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:853
|
||||
#: pretix/plugins/reports/accountingreport.py:250
|
||||
#, fuzzy
|
||||
#| msgctxt "invoice"
|
||||
#| msgid "Gross value"
|
||||
msgid "Gross total"
|
||||
msgstr "Wartość brutto"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:854
|
||||
#: pretix/plugins/reports/accountingreport.py:249
|
||||
#, fuzzy
|
||||
#| msgid "Total"
|
||||
msgid "Tax total"
|
||||
msgstr "Razem"
|
||||
msgstr "Razem (podatki)"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:864
|
||||
msgid ""
|
||||
@@ -2603,6 +2574,8 @@ msgstr "Karty podarunkowe"
|
||||
#: pretix/base/exporters/orderlist.py:1237
|
||||
msgid "Download a spreadsheet of all gift cards including their current value."
|
||||
msgstr ""
|
||||
"Pobierz arkusz kalkulacyjny zawierający wszystkie karty podarunkowe wraz z "
|
||||
"ich aktualną wartością."
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:1244
|
||||
msgid "Show value at"
|
||||
@@ -2610,7 +2583,7 @@ msgstr "Pokaż wartość w"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:1247
|
||||
msgid "Defaults to the time of report."
|
||||
msgstr ""
|
||||
msgstr "Domyślny czas raportu."
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:1252
|
||||
#: pretix/base/exporters/orderlist.py:1262 pretix/control/forms/filter.py:507
|
||||
@@ -2632,7 +2605,6 @@ msgid "All"
|
||||
msgstr "Zaznacz wszystko"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:1254 pretix/control/forms/filter.py:1315
|
||||
#, fuzzy
|
||||
msgid "Live"
|
||||
msgstr "Na żywo"
|
||||
|
||||
@@ -2642,12 +2614,10 @@ msgid "Empty"
|
||||
msgstr "Pusty"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:1264 pretix/control/forms/filter.py:1324
|
||||
#, fuzzy
|
||||
msgid "Valid and with value"
|
||||
msgstr "Ważny z wartością"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:1265 pretix/control/forms/filter.py:1325
|
||||
#, fuzzy
|
||||
msgid "Expired and with value"
|
||||
msgstr "Wygaśnięty z wartością"
|
||||
|
||||
@@ -2685,33 +2655,25 @@ msgid "Current value"
|
||||
msgstr "Wartość netto"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:1310
|
||||
#, fuzzy
|
||||
#| msgid "Creation date"
|
||||
msgid "Created in order"
|
||||
msgstr "Data stworzenia"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:1311
|
||||
#, fuzzy
|
||||
#| msgid "Invoice number"
|
||||
msgid "Last invoice number of order"
|
||||
msgstr "Numer faktury ostatniego zamówienia"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:1312
|
||||
#, fuzzy
|
||||
#| msgid "Expiration date"
|
||||
msgid "Last invoice date of order"
|
||||
msgstr "Data wygaśnięcia"
|
||||
msgstr "Data ostatniej faktury zamówienia"
|
||||
|
||||
#: pretix/base/exporters/waitinglist.py:42
|
||||
#, fuzzy
|
||||
#| msgid "Waiting list"
|
||||
msgctxt "export_category"
|
||||
msgid "Waiting list"
|
||||
msgstr "Lista oczekiwania"
|
||||
|
||||
#: pretix/base/exporters/waitinglist.py:43
|
||||
msgid "Download a spread sheet with all your waiting list data."
|
||||
msgstr ""
|
||||
msgstr "Pobierz arkusz kalkulacyjny ze wszystkimi danymi z listy oczekujących."
|
||||
|
||||
#: pretix/base/exporters/waitinglist.py:49
|
||||
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:87
|
||||
@@ -2733,7 +2695,7 @@ msgstr "Voucher został przypisany"
|
||||
#: pretix/base/exporters/waitinglist.py:64
|
||||
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:95
|
||||
msgid "Waiting for redemption"
|
||||
msgstr ""
|
||||
msgstr "Oczekiwanie na wykupienie vouchera"
|
||||
|
||||
#: pretix/base/exporters/waitinglist.py:72
|
||||
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:208
|
||||
@@ -2745,7 +2707,6 @@ msgstr "Voucher został wykorzystany"
|
||||
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:101
|
||||
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:210
|
||||
#: pretix/control/views/waitinglist.py:322
|
||||
#, fuzzy
|
||||
msgid "Voucher expired"
|
||||
msgstr "Voucher wygaśnięty"
|
||||
|
||||
@@ -2784,6 +2745,8 @@ msgid ""
|
||||
"Due to technical reasons you cannot set inputs, that need to be masked (e.g. "
|
||||
"passwords), to %(value)s."
|
||||
msgstr ""
|
||||
"Ze względów technicznych nie można ustawić pól, które mają być maskowane ("
|
||||
"np. hasła) do %(value)s."
|
||||
|
||||
#: pretix/base/forms/auth.py:57 pretix/base/forms/auth.py:168
|
||||
msgid "Keep me logged in"
|
||||
@@ -2833,11 +2796,9 @@ msgid "Please enter a shorter name."
|
||||
msgstr "Wpisz proszę krótszą nazwę."
|
||||
|
||||
#: pretix/base/forms/questions.py:283
|
||||
#, fuzzy
|
||||
#| msgid "Internal reference"
|
||||
msgctxt "phonenumber"
|
||||
msgid "International area code"
|
||||
msgstr "Wewnętrzna adnotacja"
|
||||
msgstr "Wewnętrzny kod rejonu"
|
||||
|
||||
#: pretix/base/forms/questions.py:307
|
||||
msgctxt "phonenumber"
|
||||
@@ -2851,10 +2812,9 @@ msgid ""
|
||||
msgstr "Wgrałeś poziomie zdjęcie. Wymagane jest zdjęcie w pionie."
|
||||
|
||||
#: pretix/base/forms/questions.py:471
|
||||
#, fuzzy
|
||||
msgid "Please upload an image where the width is 3/4 of the height."
|
||||
msgstr ""
|
||||
"Wgraj obrazek, którego proporcje to: szerokość nie mniej niż 3/4 wysokości"
|
||||
"Wgraj obrazek, którego proporcje to: szerokość nie mniej niż 3/4 wysokości."
|
||||
|
||||
#: pretix/base/forms/questions.py:474
|
||||
msgid ""
|
||||
@@ -2876,7 +2836,7 @@ msgstr ""
|
||||
msgid ""
|
||||
"If you keep this empty, the ticket will be valid starting at the time of "
|
||||
"purchase."
|
||||
msgstr ""
|
||||
msgstr "Jeśli to pole pozostanie puste, bilet będzie ważny od momentu zakupu."
|
||||
|
||||
#: pretix/base/forms/questions.py:664 pretix/base/forms/questions.py:992
|
||||
msgid "Street and Number"
|
||||
@@ -2892,10 +2852,14 @@ msgid ""
|
||||
"Optional, but depending on the country you reside in we might need to charge "
|
||||
"you additional taxes if you do not enter it."
|
||||
msgstr ""
|
||||
"Opcjonalne, ale w zależności od kraju, w którym mieszkasz, możemy być "
|
||||
"zmuszeni do naliczenia dodatkowych podatków, jeśli go nie podasz."
|
||||
|
||||
#: pretix/base/forms/questions.py:1033 pretix/base/forms/questions.py:1039
|
||||
msgid "If you are registered in Switzerland, you can enter your UID instead."
|
||||
msgstr ""
|
||||
"Jeśli jesteś zarejestrowany w Szwajcarii, możesz zamiast tego wprowadzić "
|
||||
"swój identyfikator UID."
|
||||
|
||||
#: pretix/base/forms/questions.py:1037
|
||||
msgid ""
|
||||
|
||||
@@ -34,24 +34,27 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
from collections import OrderedDict
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from decimal import Decimal
|
||||
from io import BytesIO
|
||||
from typing import Tuple
|
||||
from typing import BinaryIO, List, Optional, Tuple
|
||||
|
||||
import dateutil.parser
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.core.files import File
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db import DataError, models
|
||||
from django.db.models import Case, Exists, OuterRef, Q, Subquery, When
|
||||
from django.db.models.functions import Cast, Coalesce
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy
|
||||
from pypdf import PdfReader, PdfWriter, Transformation
|
||||
from pypdf import PageObject, PdfReader, PdfWriter, Transformation
|
||||
from pypdf.generic import RectangleObject
|
||||
from reportlab.lib import pagesizes
|
||||
from reportlab.lib.units import mm
|
||||
@@ -59,8 +62,10 @@ from reportlab.pdfgen import canvas
|
||||
|
||||
from pretix.base.exporter import BaseExporter
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import Order, OrderPosition, Question, QuestionAnswer
|
||||
from pretix.base.pdf import Renderer
|
||||
from pretix.base.models import (
|
||||
Event, Order, OrderPosition, Question, QuestionAnswer,
|
||||
)
|
||||
from pretix.base.pdf import Renderer, merge_background
|
||||
from pretix.base.services.export import ExportError
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.helpers.templatetags.jsonfield import JSONExtract
|
||||
@@ -179,9 +184,137 @@ OPTIONS = OrderedDict([
|
||||
])
|
||||
|
||||
|
||||
def render_pdf(event, positions, opt):
|
||||
Renderer._register_fonts()
|
||||
def _chunks(lst, n):
|
||||
"""
|
||||
Yield successive n-sized chunks from lst.
|
||||
"""
|
||||
for i in range(0, len(lst), n):
|
||||
yield lst[i:i + n]
|
||||
|
||||
|
||||
def _render_nup_page(nup_pdf: PdfWriter, input_pages: PageObject, opt: dict) -> PageObject:
|
||||
"""
|
||||
Render the `Page` objects in `input_pages` onto one page of `nup_pdf` using the options given in `opt` and
|
||||
return the newly created page.
|
||||
"""
|
||||
badges_per_page = opt['cols'] * opt['rows']
|
||||
nup_page = nup_pdf.add_blank_page(
|
||||
width=Decimal('%.5f' % (opt['pagesize'][0])),
|
||||
height=Decimal('%.5f' % (opt['pagesize'][1])),
|
||||
)
|
||||
for i, page in enumerate(input_pages):
|
||||
di = i % badges_per_page
|
||||
tx = opt['margins'][3] + (di % opt['cols']) * opt['offsets'][0]
|
||||
ty = opt['margins'][2] + (opt['rows'] - 1 - (di // opt['cols'])) * opt['offsets'][1]
|
||||
page.add_transformation(Transformation().translate(tx, ty))
|
||||
page.mediabox = RectangleObject((
|
||||
Decimal('%.5f' % (page.mediabox.left.as_numeric() + tx)),
|
||||
Decimal('%.5f' % (page.mediabox.bottom.as_numeric() + ty)),
|
||||
Decimal('%.5f' % (page.mediabox.right.as_numeric() + tx)),
|
||||
Decimal('%.5f' % (page.mediabox.top.as_numeric() + ty))
|
||||
))
|
||||
page.trimbox = page.mediabox
|
||||
nup_page.merge_page(page)
|
||||
return nup_page
|
||||
|
||||
|
||||
def _merge_pages(file_paths: List[str], output_file: BinaryIO):
|
||||
"""
|
||||
Merge all pages from the PDF files named `file_paths` into the `output_file`.
|
||||
"""
|
||||
if settings.PDFTK:
|
||||
subprocess.run([
|
||||
settings.PDFTK,
|
||||
*file_paths,
|
||||
'cat',
|
||||
'output',
|
||||
'-',
|
||||
'compress'
|
||||
], check=True, stdout=output_file)
|
||||
else:
|
||||
merger = PdfWriter()
|
||||
merger.add_metadata({
|
||||
'/Title': 'Badges',
|
||||
'/Creator': 'pretix',
|
||||
})
|
||||
# append all temp-PDFs
|
||||
for pdf in file_paths:
|
||||
merger.append(pdf)
|
||||
|
||||
# write merged PDFs to buffer
|
||||
merger.write(output_file)
|
||||
|
||||
|
||||
def _render_nup(input_files: List[str], num_pages: int, output_file: BytesIO, opt: dict):
|
||||
"""
|
||||
Render the pages from the PDF files listed in `input_files` (file names) with a total number of `num_pages` pages
|
||||
into one file written to `output_file` using the -nup options given in `opt`.
|
||||
"""
|
||||
badges_per_page = opt['cols'] * opt['rows']
|
||||
max_nup_pages = 20 # chunk size to prevent working with huge files
|
||||
nup_pdf_files = []
|
||||
temp_dir = None
|
||||
if num_pages > badges_per_page * max_nup_pages:
|
||||
# to reduce memory consumption with lots of badges
|
||||
# we try to use temporary PDF-files with up to
|
||||
# max_nup_pages pages
|
||||
# If temp-files fail, we try to merge in-memory anyways
|
||||
try:
|
||||
temp_dir = tempfile.TemporaryDirectory()
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
try:
|
||||
badges_pdf = PdfReader(input_files.pop())
|
||||
offset = 0
|
||||
for i, chunk_indices in enumerate(_chunks(range(num_pages), badges_per_page * max_nup_pages)):
|
||||
chunk = []
|
||||
for j in chunk_indices:
|
||||
# We need to dynamically switch to the next input file as we don't know how many pages each input
|
||||
# file has beforehand
|
||||
if j - offset >= len(badges_pdf.pages):
|
||||
offset += len(badges_pdf.pages)
|
||||
badges_pdf = PdfReader(input_files.pop())
|
||||
chunk.append(badges_pdf.pages[j - offset])
|
||||
# Reset some internal state from pypdf. This will make it a little slower, but will prevent us from
|
||||
# running out of memory if we process a really large file.
|
||||
badges_pdf.flattened_pages = None
|
||||
|
||||
nup_pdf = PdfWriter()
|
||||
nup_pdf.add_metadata({
|
||||
'/Title': 'Badges',
|
||||
'/Creator': 'pretix',
|
||||
})
|
||||
|
||||
for page_chunk in _chunks(chunk, badges_per_page):
|
||||
_render_nup_page(nup_pdf, page_chunk, opt)
|
||||
|
||||
if temp_dir:
|
||||
file_path = os.path.join(temp_dir.name, 'badges-%d.pdf' % i)
|
||||
nup_pdf.write(file_path)
|
||||
nup_pdf_files.append(file_path)
|
||||
else:
|
||||
# everything fitted into one nup_pdf -- we can save some work
|
||||
nup_pdf.write(output_file)
|
||||
return
|
||||
|
||||
del badges_pdf # free up memory
|
||||
|
||||
file_paths = [os.path.join(temp_dir.name, fp) for fp in nup_pdf_files]
|
||||
_merge_pages(file_paths, output_file)
|
||||
finally:
|
||||
if temp_dir:
|
||||
try:
|
||||
temp_dir.cleanup()
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
|
||||
def _render_badges(event: Event, positions: List[OrderPosition], opt: dict) -> Tuple[PdfWriter, PdfWriter, int]:
|
||||
"""
|
||||
Render the badges for the given order positions into two different files, one with the foregrounds and one with
|
||||
the backgrounds.
|
||||
"""
|
||||
renderermap = {
|
||||
bi.item_id: _renderer(event, bi.layout)
|
||||
for bi in BadgeItem.objects.select_related('layout').filter(item__event=event)
|
||||
@@ -195,12 +328,13 @@ def render_pdf(event, positions, opt):
|
||||
if not len(op_renderers):
|
||||
raise ExportError(_("None of the selected products is configured to print badges."))
|
||||
|
||||
# render each badge on its own page first
|
||||
merger = PdfWriter()
|
||||
merger.add_metadata({
|
||||
fg_pdf = PdfWriter()
|
||||
fg_pdf.add_metadata({
|
||||
'/Title': 'Badges',
|
||||
'/Creator': 'pretix',
|
||||
})
|
||||
bg_pdf = PdfWriter()
|
||||
num_pages = 0
|
||||
for op, renderer in op_renderers:
|
||||
buffer = BytesIO()
|
||||
page = canvas.Canvas(buffer, pagesize=pagesizes.A4)
|
||||
@@ -210,46 +344,52 @@ def render_pdf(event, positions, opt):
|
||||
if opt['pagesize']:
|
||||
page.setPageSize(opt['pagesize'])
|
||||
page.save()
|
||||
buffer = renderer.render_background(buffer, _('Badge'))
|
||||
merger.append(ContentFile(buffer.read()))
|
||||
# to reduce disk-IO render backgrounds in own PDF and merge later
|
||||
fg_pdf.append(buffer)
|
||||
new_num_pages = len(fg_pdf.pages)
|
||||
for i in range(new_num_pages - num_pages):
|
||||
bg_pdf.add_page(renderer.bg_pdf.pages[i])
|
||||
num_pages = new_num_pages
|
||||
|
||||
outbuffer = BytesIO()
|
||||
merger.write(outbuffer)
|
||||
outbuffer.seek(0)
|
||||
return fg_pdf, bg_pdf, num_pages
|
||||
|
||||
|
||||
def render_pdf(event, positions, opt, output_file):
|
||||
Renderer._register_fonts()
|
||||
badges_per_page = opt['cols'] * opt['rows']
|
||||
|
||||
if badges_per_page == 1:
|
||||
# no need to place multiple badges on one page
|
||||
return outbuffer
|
||||
fg_pdf, bg_pdf, _ = _render_badges(event, positions, opt)
|
||||
merge_background(
|
||||
fg_pdf,
|
||||
bg_pdf,
|
||||
output_file,
|
||||
compress=True,
|
||||
)
|
||||
else:
|
||||
# place n-up badges/pages per page
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
page_pdfs = []
|
||||
total_num_pages = 0
|
||||
for position_chunk in _chunks(positions, 200):
|
||||
# We first render the foreground and background of every individual badge and merge them, but we do
|
||||
# so in chunks, since the n-up code is slower if it has to deal with huge PDFs. It doesn't matter
|
||||
# that not every position has the same number of pages, as the n-up code can deal with that
|
||||
fg_pdf, bg_pdf, num_pages = _render_badges(event, position_chunk, opt)
|
||||
out_pdf_name = os.path.join(tmp_dir, f'chunk-{len(page_pdfs)}.pdf')
|
||||
with open(out_pdf_name, 'wb') as out_pdf:
|
||||
merge_background(
|
||||
fg_pdf,
|
||||
bg_pdf,
|
||||
out_pdf,
|
||||
compress=False,
|
||||
)
|
||||
page_pdfs.append(out_pdf_name)
|
||||
total_num_pages += num_pages
|
||||
del fg_pdf, bg_pdf # free up memory
|
||||
|
||||
# place n-up badges/pages per page
|
||||
badges_pdf = PdfReader(outbuffer)
|
||||
nup_pdf = PdfWriter()
|
||||
nup_page = None
|
||||
for i, page in enumerate(badges_pdf.pages):
|
||||
di = i % badges_per_page
|
||||
if di == 0:
|
||||
nup_page = nup_pdf.add_blank_page(
|
||||
width=Decimal('%.5f' % (opt['pagesize'][0])),
|
||||
height=Decimal('%.5f' % (opt['pagesize'][1])),
|
||||
)
|
||||
tx = opt['margins'][3] + (di % opt['cols']) * opt['offsets'][0]
|
||||
ty = opt['margins'][2] + (opt['rows'] - 1 - (di // opt['cols'])) * opt['offsets'][1]
|
||||
page.add_transformation(Transformation().translate(tx, ty))
|
||||
page.mediabox = RectangleObject((
|
||||
Decimal('%.5f' % (page.mediabox.left.as_numeric() + tx)),
|
||||
Decimal('%.5f' % (page.mediabox.bottom.as_numeric() + ty)),
|
||||
Decimal('%.5f' % (page.mediabox.right.as_numeric() + tx)),
|
||||
Decimal('%.5f' % (page.mediabox.top.as_numeric() + ty))
|
||||
))
|
||||
page.trimbox = page.mediabox
|
||||
nup_page.merge_page(page)
|
||||
|
||||
outbuffer = BytesIO()
|
||||
nup_pdf.write(outbuffer)
|
||||
outbuffer.seek(0)
|
||||
|
||||
return outbuffer
|
||||
# Actually render a n-up file
|
||||
return _render_nup(page_pdfs, total_num_pages, output_file, opt)
|
||||
|
||||
|
||||
class BadgeExporter(BaseExporter):
|
||||
@@ -335,7 +475,7 @@ class BadgeExporter(BaseExporter):
|
||||
)
|
||||
return d
|
||||
|
||||
def render(self, form_data: dict) -> Tuple[str, str, str]:
|
||||
def render(self, form_data: dict, output_file=None) -> Tuple[str, str, Optional[bytes]]:
|
||||
qs = OrderPosition.objects.filter(
|
||||
order__event=self.event, item_id__in=form_data['items']
|
||||
).prefetch_related(
|
||||
@@ -431,11 +571,17 @@ class BadgeExporter(BaseExporter):
|
||||
)
|
||||
|
||||
try:
|
||||
outbuffer = render_pdf(self.event, qs, OPTIONS[form_data.get('rendering', 'one')])
|
||||
if output_file:
|
||||
render_pdf(self.event, qs, OPTIONS[form_data.get('rendering', 'one')], output_file=output_file)
|
||||
return 'badges.pdf', 'application/pdf', None
|
||||
else:
|
||||
with tempfile.NamedTemporaryFile(delete=True) as tmpfile:
|
||||
render_pdf(self.event, qs, OPTIONS[form_data.get('rendering', 'one')], output_file=tmpfile)
|
||||
tmpfile.seek(0)
|
||||
return 'badges.pdf', 'application/pdf', tmpfile.read()
|
||||
except DataError:
|
||||
logging.exception('DataError during export')
|
||||
raise ExportError(
|
||||
_('Your data could not be converted as requested. This could be caused by invalid values in your '
|
||||
'databases, such as answers to number questions which are not a number.')
|
||||
)
|
||||
return 'badges.pdf', 'application/pdf', outbuffer.read()
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
import tempfile
|
||||
from typing import List
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
@@ -41,7 +42,10 @@ logger = logging.getLogger(__name__)
|
||||
def badges_create_pdf(event: Event, fileid: int, positions: List[int]) -> int:
|
||||
file = CachedFile.objects.get(id=fileid)
|
||||
|
||||
pdfcontent = render_pdf(event, OrderPosition.objects.filter(id__in=positions), opt=OPTIONS['one'])
|
||||
file.file.save(cachedfile_name(file, file.filename), ContentFile(pdfcontent.read()))
|
||||
file.save()
|
||||
with tempfile.TemporaryFile() as tmp_file:
|
||||
render_pdf(event, OrderPosition.objects.filter(id__in=positions), opt=OPTIONS['one'],
|
||||
output_file=tmp_file)
|
||||
tmp_file.seek(0)
|
||||
file.file.save(cachedfile_name(file, file.filename), ContentFile(tmp_file.read()))
|
||||
file.save()
|
||||
return file.pk
|
||||
|
||||
@@ -49,7 +49,7 @@ class RuleSerializer(I18nAwareModelSerializer):
|
||||
if not full_data.get('send_date'):
|
||||
raise ValidationError('send_date is required for date_is_absolute=True')
|
||||
else:
|
||||
if not all([full_data.get(k) for k in ['send_offset_days', 'send_offset_time']]):
|
||||
if not all([full_data.get(k) is not None for k in ['send_offset_days', 'send_offset_time']]):
|
||||
raise ValidationError('send_offset_days and send_offset_time are required for date_is_absolute=False')
|
||||
|
||||
if full_data.get('all_products') is False:
|
||||
|
||||
@@ -59,7 +59,9 @@ from pretix import __version__
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.forms import SecretKeySettingsField
|
||||
from pretix.base.models import Event, OrderPayment, OrderRefund, Quota
|
||||
from pretix.base.payment import BasePaymentProvider, PaymentException
|
||||
from pretix.base.payment import (
|
||||
BasePaymentProvider, PaymentException, WalletQueries,
|
||||
)
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
@@ -219,6 +221,20 @@ class StripeSettingsHolder(BasePaymentProvider):
|
||||
]
|
||||
|
||||
extra_fields = [
|
||||
('walletdetection',
|
||||
forms.BooleanField(
|
||||
label=mark_safe(
|
||||
_('Check for Apple Pay/Google Pay') +
|
||||
' ' +
|
||||
'<span class="label label-info">{}</span>'.format(_('experimental'))
|
||||
),
|
||||
help_text=_("pretix will attempt to check if the customer's webbrowser supports wallet-based payment "
|
||||
"methods like Apple Pay or Google Pay and display them prominently with the credit card"
|
||||
"payment method. This detection does not take into consideration if Google Pay/Apple Pay "
|
||||
"has been disabled in the Stripe Dashboard."),
|
||||
initial=True,
|
||||
required=False,
|
||||
)),
|
||||
('postfix',
|
||||
forms.CharField(
|
||||
label=_('Statement descriptor postfix'),
|
||||
@@ -747,6 +763,15 @@ class StripeCC(StripeMethod):
|
||||
public_name = _('Credit card')
|
||||
method = 'cc'
|
||||
|
||||
@property
|
||||
def walletqueries(self):
|
||||
# ToDo: Check against Stripe API, if ApplePay and GooglePay are even activated/available
|
||||
# This is probably only really feasable once the Payment Methods Configuration API is out of beta
|
||||
# https://stripe.com/docs/connect/payment-method-configurations
|
||||
if self.settings.get("walletdetection", True, as_type=bool):
|
||||
return [WalletQueries.APPLEPAY, WalletQueries.GOOGLEPAY]
|
||||
return []
|
||||
|
||||
def payment_form_render(self, request, total) -> str:
|
||||
account = get_stripe_account_key(self)
|
||||
if not RegisteredApplePayDomain.objects.filter(account=account, domain=request.host).exists():
|
||||
|
||||
@@ -24,18 +24,22 @@ from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
from django.dispatch import receiver
|
||||
from django.http import HttpRequest
|
||||
from django.template.loader import get_template
|
||||
from django.urls import resolve, reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from paypalhttp import HttpResponse
|
||||
|
||||
from pretix.base.forms import SecretKeySettingsField
|
||||
from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
|
||||
from pretix.base.settings import settings_hierarkey
|
||||
from pretix.base.signals import (
|
||||
logentry_display, register_global_settings, register_payment_providers,
|
||||
)
|
||||
from pretix.control.signals import nav_organizer
|
||||
from pretix.plugins.stripe.forms import StripeKeyValidator
|
||||
from pretix.presale.signals import html_head
|
||||
from pretix.plugins.stripe.payment import StripeMethod
|
||||
from pretix.presale.signals import html_head, process_response
|
||||
|
||||
|
||||
@receiver(register_payment_providers, dispatch_uid="payment_stripe")
|
||||
@@ -178,3 +182,34 @@ def nav_o(sender, request, organizer, **kwargs):
|
||||
'active': 'settings.connect' in url.url_name,
|
||||
}]
|
||||
return []
|
||||
|
||||
|
||||
@receiver(signal=process_response, dispatch_uid="stripe_middleware_resp")
|
||||
def signal_process_response(sender, request: HttpRequest, response: HttpResponse, **kwargs):
|
||||
provider = StripeMethod(sender)
|
||||
url = resolve(request.path_info)
|
||||
|
||||
if provider.settings.get('_enabled', as_type=bool) and (
|
||||
url.url_name == "event.order.pay.change" or
|
||||
url.url_name == "event.order.pay" or
|
||||
(url.url_name == "event.checkout" and url.kwargs['step'] == "payment") or
|
||||
(url.namespace == "plugins:stripe" and url.url_name in ["sca", "sca.return"])
|
||||
):
|
||||
if 'Content-Security-Policy' in response:
|
||||
h = _parse_csp(response['Content-Security-Policy'])
|
||||
else:
|
||||
h = {}
|
||||
|
||||
# https://stripe.com/docs/security/guide#content-security-policy
|
||||
csps = {
|
||||
'connect-src': ['https://api.stripe.com'],
|
||||
'frame-src': ['https://js.stripe.com', 'https://hooks.stripe.com'],
|
||||
'script-src': ['https://js.stripe.com'],
|
||||
}
|
||||
|
||||
_merge_csp(h, csps)
|
||||
|
||||
if h:
|
||||
response['Content-Security-Policy'] = _render_csp(h)
|
||||
|
||||
return response
|
||||
|
||||
@@ -26,8 +26,10 @@
|
||||
{% if payment_info.source.type == "bancontact" %}
|
||||
<dt>{% trans "Bank" %}</dt>
|
||||
<dd>{{ payment_info.source.bancontact.bank_name }} ({{ payment_info.source.bancontact.bic }})</dd>
|
||||
<dt>{% trans "Payer name" %}</dt>
|
||||
<dd>{{ payment_info.source.owner.verified_name|default:payment_info.source.owner.name }}</dd>
|
||||
{% if owner in payment_info.source %}
|
||||
<dt>{% trans "Payer name" %}</dt>
|
||||
<dd>{{ payment_info.source.owner.verified_name|default:payment_info.source.owner.name }}</dd>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if payment_info.source.type == "ideal" %}
|
||||
<dt>{% trans "Bank" %}</dt>
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
{% load money %}
|
||||
{% load bootstrap3 %}
|
||||
{% load rich_text %}
|
||||
{% block custom_header %}
|
||||
{{ block.super }}
|
||||
{% include "pretixpresale/event/fragment_walletdetection_head.html" %}
|
||||
{% endblock %}
|
||||
{% block inner %}
|
||||
{% if current_payments %}
|
||||
<p>{% trans "You already selected the following payment methods:" %}</p>
|
||||
@@ -71,7 +75,8 @@
|
||||
{% if selected == p.provider.identifier %}checked="checked"{% endif %}
|
||||
id="input_payment_{{ p.provider.identifier }}"
|
||||
aria-describedby="payment_{{ p.provider.identifier }}"
|
||||
data-toggle="radiocollapse" data-target="#payment_{{ p.provider.identifier }}"/>
|
||||
data-toggle="radiocollapse" data-target="#payment_{{ p.provider.identifier }}"
|
||||
data-wallets="{{ p.provider.walletqueries|join:"|" }}" />
|
||||
<label for="input_payment_{{ p.provider.identifier }}"><strong>{{ p.provider.public_name }}</strong></label>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{% load static %}
|
||||
{% load compress %}
|
||||
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "pretixpresale/js/walletdetection.js" %}"></script>
|
||||
{% endcompress %}
|
||||
@@ -3,6 +3,10 @@
|
||||
{% load eventurl %}
|
||||
{% load money %}
|
||||
{% block title %}{% trans "Change payment method" %}{% endblock %}
|
||||
{% block custom_header %}
|
||||
{{ block.super }}
|
||||
{% include "pretixpresale/event/fragment_walletdetection_head.html" %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h2>
|
||||
{% blocktrans trimmed with code=order.code %}
|
||||
@@ -29,7 +33,8 @@
|
||||
<input type="radio" name="payment" value="{{ p.provider.identifier }}"
|
||||
data-parent="#payment_accordion"
|
||||
{% if selected == p.provider.identifier %}checked="checked"{% endif %}
|
||||
data-toggle="radiocollapse" data-target="#payment_{{ p.provider.identifier }}" />
|
||||
data-toggle="radiocollapse" data-target="#payment_{{ p.provider.identifier }}"
|
||||
data-wallets="{{ p.provider.walletqueries|join:"|" }}"/>
|
||||
<strong>{{ p.provider.public_name }}</strong>
|
||||
</label>
|
||||
</h4>
|
||||
|
||||
@@ -52,6 +52,7 @@ from pretix.base.customersso.oidc import (
|
||||
from pretix.base.models import Customer, InvoiceAddress, Order, OrderPosition
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import customer_created, customer_signed_in
|
||||
from pretix.helpers.compat import CompatDeleteView
|
||||
from pretix.helpers.http import redirect_to_url
|
||||
from pretix.multidomain.models import KnownDomain
|
||||
@@ -151,7 +152,9 @@ class LoginView(RedirectBackMixin, FormView):
|
||||
|
||||
def form_valid(self, form):
|
||||
"""Security check complete. Log the user in."""
|
||||
customer_login(self.request, form.get_customer())
|
||||
customer = form.get_customer()
|
||||
customer_login(self.request, customer)
|
||||
customer_signed_in.send(customer.organizer, customer=customer)
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
|
||||
@@ -237,7 +240,8 @@ class RegistrationView(RedirectBackMixin, FormView):
|
||||
|
||||
def form_valid(self, form):
|
||||
with transaction.atomic():
|
||||
form.create()
|
||||
customer = form.create()
|
||||
customer_created.send(customer.organizer, customer=customer)
|
||||
messages.success(
|
||||
self.request,
|
||||
_('Your account has been created. Please follow the link in the email we sent you to activate your '
|
||||
@@ -756,6 +760,7 @@ class SSOLoginReturnView(RedirectBackMixin, View):
|
||||
)
|
||||
try:
|
||||
customer.save(force_insert=True)
|
||||
customer_created.send(customer.organizer, customer=customer)
|
||||
except IntegrityError:
|
||||
# This might either be a race condition or the email address is taken
|
||||
# by a different customer account
|
||||
@@ -819,6 +824,7 @@ class SSOLoginReturnView(RedirectBackMixin, View):
|
||||
})
|
||||
else:
|
||||
customer_login(self.request, customer)
|
||||
customer_signed_in.send(customer.organizer, customer=customer)
|
||||
return redirect_to_url(self.get_success_url(redirect_to))
|
||||
|
||||
def _fail(self, message, popup_origin):
|
||||
|
||||
@@ -116,6 +116,29 @@ elif 'mysql' in db_backend:
|
||||
|
||||
db_options = {}
|
||||
|
||||
postgresql_sslmode = config.get('database', 'sslmode', fallback='disable')
|
||||
USE_DATABASE_TLS = postgresql_sslmode != 'disable'
|
||||
USE_DATABASE_MTLS = USE_DATABASE_TLS and config.has_option('database', 'sslcert')
|
||||
|
||||
if USE_DATABASE_TLS or USE_DATABASE_MTLS:
|
||||
tls_config = {}
|
||||
if not USE_DATABASE_MTLS:
|
||||
if 'postgresql' in db_backend:
|
||||
tls_config = {
|
||||
'sslmode': config.get('database', 'sslmode'),
|
||||
'sslrootcert': config.get('database', 'sslrootcert'),
|
||||
}
|
||||
else:
|
||||
if 'postgresql' in db_backend:
|
||||
tls_config = {
|
||||
'sslmode': config.get('database', 'sslmode'),
|
||||
'sslrootcert': config.get('database', 'sslrootcert'),
|
||||
'sslcert': config.get('database', 'sslcert'),
|
||||
'sslkey': config.get('database', 'sslkey'),
|
||||
}
|
||||
|
||||
db_options.update(tls_config)
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.' + db_backend,
|
||||
@@ -228,6 +251,9 @@ if HAS_MEMCACHED:
|
||||
|
||||
HAS_REDIS = config.has_option('redis', 'location')
|
||||
USE_REDIS_SENTINEL = config.has_option('redis', 'sentinels')
|
||||
redis_ssl_cert_reqs = config.get('redis', 'ssl_cert_reqs', fallback='none')
|
||||
USE_REDIS_TLS = redis_ssl_cert_reqs != 'none'
|
||||
USE_REDIS_MTLS = USE_REDIS_TLS and config.has_option('redis', 'ssl_certfile')
|
||||
HAS_REDIS_PASSWORD = config.has_option('redis', 'password')
|
||||
if HAS_REDIS:
|
||||
OPTIONS = {
|
||||
@@ -243,6 +269,29 @@ if HAS_REDIS:
|
||||
OPTIONS["SENTINEL_KWARGS"] = {"socket_timeout": 1}
|
||||
OPTIONS["SENTINELS"] = [tuple(sentinel) for sentinel in loads(config.get('redis', 'sentinels'))]
|
||||
|
||||
if USE_REDIS_TLS or USE_REDIS_MTLS:
|
||||
tls_config = {}
|
||||
if not USE_REDIS_MTLS:
|
||||
tls_config = {
|
||||
'ssl_cert_reqs': config.get('redis', 'ssl_cert_reqs'),
|
||||
'ssl_ca_certs': config.get('redis', 'ssl_ca_certs'),
|
||||
}
|
||||
else:
|
||||
tls_config = {
|
||||
'ssl_cert_reqs': config.get('redis', 'ssl_cert_reqs'),
|
||||
'ssl_ca_certs': config.get('redis', 'ssl_ca_certs'),
|
||||
'ssl_keyfile': config.get('redis', 'ssl_keyfile'),
|
||||
'ssl_certfile': config.get('redis', 'ssl_certfile'),
|
||||
}
|
||||
|
||||
if USE_REDIS_SENTINEL is False:
|
||||
# The CONNECTION_POOL_KWARGS option is necessary for self-signed certs. For further details, please check
|
||||
# https://github.com/jazzband/django-redis/issues/554#issuecomment-949498321
|
||||
OPTIONS["CONNECTION_POOL_KWARGS"] = tls_config
|
||||
OPTIONS["REDIS_CLIENT_KWARGS"].update(tls_config)
|
||||
else:
|
||||
OPTIONS["SENTINEL_KWARGS"].update(tls_config)
|
||||
|
||||
if HAS_REDIS_PASSWORD:
|
||||
OPTIONS["PASSWORD"] = config.get('redis', 'password')
|
||||
|
||||
|
||||
2284
src/pretix/static/npm_dir/package-lock.json
generated
2284
src/pretix/static/npm_dir/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,8 @@
|
||||
"private": true,
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.1",
|
||||
"@babel/preset-env": "^7.22.4",
|
||||
"@babel/core": "^7.22.5",
|
||||
"@babel/preset-env": "^7.22.9",
|
||||
"@rollup/plugin-babel": "^6.0.3",
|
||||
"@rollup/plugin-node-resolve": "^15.1.0",
|
||||
"vue": "^2.7.14",
|
||||
|
||||
@@ -1,6 +1,31 @@
|
||||
/*global $ */
|
||||
|
||||
setup_collapsible_details = function (el) {
|
||||
|
||||
el.find('details.sneak-peek:not([open])').each(function() {
|
||||
this.open = true;
|
||||
var $elements = $("> :not(summary)", this).show().filter(':not(.sneak-peek-trigger)').attr('aria-hidden', 'true');
|
||||
|
||||
var container = this;
|
||||
var trigger = $('summary, .sneak-peek-trigger button', container);
|
||||
function onclick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
container.addEventListener('transitionend', function() {
|
||||
$(container).removeClass('sneak-peek');
|
||||
container.style.removeProperty('height');
|
||||
}, {once: true});
|
||||
container.style.height = container.scrollHeight + 'px';
|
||||
$('.sneak-peek-trigger', container).fadeOut(function() {
|
||||
$(this).remove();
|
||||
});
|
||||
$elements.removeAttr('aria-hidden');
|
||||
|
||||
trigger.off('click', onclick);
|
||||
}
|
||||
trigger.on('click', onclick);
|
||||
});
|
||||
|
||||
var isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]';
|
||||
el.find("details summary").click(function (e) {
|
||||
if (this.tagName !== "A" && $(e.target).closest("a").length > 0) {
|
||||
|
||||
@@ -123,6 +123,8 @@ var form_handlers = function (el) {
|
||||
|
||||
// Vouchers
|
||||
el.find("#voucher-bulk-codes-generate").click(function () {
|
||||
if (!$("#voucher-bulk-codes-num").get(0).reportValidity())
|
||||
return;
|
||||
var num = $("#voucher-bulk-codes-num").val();
|
||||
var prefix = $('#voucher-bulk-codes-prefix').val();
|
||||
if (num != "") {
|
||||
|
||||
@@ -307,30 +307,6 @@ $(function () {
|
||||
|
||||
$("#ajaxerr").on("click", ".ajaxerr-close", ajaxErrDialog.hide);
|
||||
|
||||
$('details.sneak-peek:not([open])').each(function() {
|
||||
this.open = true;
|
||||
var $elements = $("> :not(summary)", this).show().filter(':not(.sneak-peek-trigger)').attr('aria-hidden', 'true');
|
||||
|
||||
var container = this;
|
||||
var trigger = $('summary, .sneak-peek-trigger button', container);
|
||||
function onclick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
container.addEventListener('transitionend', function() {
|
||||
$(container).removeClass('sneak-peek');
|
||||
container.style.removeProperty('height');
|
||||
}, {once: true});
|
||||
container.style.height = container.scrollHeight + 'px';
|
||||
$('.sneak-peek-trigger', container).fadeOut(function() {
|
||||
$(this).remove();
|
||||
});
|
||||
$elements.removeAttr('aria-hidden');
|
||||
|
||||
trigger.off('click', onclick);
|
||||
}
|
||||
trigger.on('click', onclick);
|
||||
});
|
||||
|
||||
// Copy answers
|
||||
$(".js-copy-answers").click(function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
71
src/pretix/static/pretixpresale/js/walletdetection.js
Normal file
71
src/pretix/static/pretixpresale/js/walletdetection.js
Normal file
@@ -0,0 +1,71 @@
|
||||
'use strict';
|
||||
|
||||
var walletdetection = {
|
||||
applepay: async function () {
|
||||
// This is a weak check for Apple Pay - in order to do a proper check, we would need to also call
|
||||
// canMakePaymentsWithActiveCard(merchantIdentifier)
|
||||
|
||||
return !!(window.ApplePaySession && window.ApplePaySession.canMakePayments());
|
||||
},
|
||||
googlepay: async function () {
|
||||
// Checking for Google Pay is a little bit more involved, since it requires including the Google Pay JS SDK, and
|
||||
// providing a lot of information.
|
||||
// So for the time being, we only check if Google Pay is available in TEST-mode, which should hopefully give us a
|
||||
// good enough idea if Google Pay could be present on this device; even though there are still a lot of other
|
||||
// factors that could inhibit Google Pay from actually being offered to the customer.
|
||||
|
||||
return $.ajax({
|
||||
url: 'https://pay.google.com/gp/p/js/pay.js',
|
||||
dataType: 'script',
|
||||
}).then(function() {
|
||||
const paymentsClient = new google.payments.api.PaymentsClient({environment: 'TEST'});
|
||||
return paymentsClient.isReadyToPay({
|
||||
apiVersion: 2,
|
||||
apiVersionMinor: 0,
|
||||
allowedPaymentMethods: [{
|
||||
type: 'CARD',
|
||||
parameters: {
|
||||
allowedAuthMethods: ["PAN_ONLY", "CRYPTOGRAM_3DS"],
|
||||
allowedCardNetworks: ["AMEX", "DISCOVER", "INTERAC", "JCB", "MASTERCARD", "VISA"]
|
||||
}
|
||||
}],
|
||||
})
|
||||
}).then(function (response) {
|
||||
return !!response.result;
|
||||
});
|
||||
},
|
||||
name_map: {
|
||||
applepay: gettext('Apple Pay'),
|
||||
googlepay: gettext('Google Pay'),
|
||||
}
|
||||
}
|
||||
|
||||
$(function () {
|
||||
const wallets = $('[data-wallets]')
|
||||
.map(function(index, pm) {
|
||||
return pm.getAttribute("data-wallets").split("|");
|
||||
})
|
||||
.get()
|
||||
.flat()
|
||||
.filter(function(item, pos, self) {
|
||||
// filter out empty or duplicate values
|
||||
return item && self.indexOf(item) == pos;
|
||||
});
|
||||
|
||||
wallets.forEach(function(wallet) {
|
||||
const labels = $('[data-wallets*='+wallet+'] + label strong, [data-wallets*='+wallet+'] + strong')
|
||||
.append('<span class="wallet wallet-loading"> <i aria-hidden="true" class="fa fa-cog fa-spin"></i></span>')
|
||||
walletdetection[wallet]()
|
||||
.then(function(result) {
|
||||
const spans = labels.find(".wallet-loading:nth-of-type(1)");
|
||||
if (result) {
|
||||
spans.removeClass('wallet-loading').hide().text(', ' + walletdetection.name_map[wallet]).fadeIn(300);
|
||||
} else {
|
||||
spans.remove();
|
||||
}
|
||||
})
|
||||
.catch(function(result) {
|
||||
labels.find(".wallet-loading:nth-of-type(1)").remove();
|
||||
})
|
||||
});
|
||||
});
|
||||
@@ -179,3 +179,7 @@
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.wallet-loading + .wallet-loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ def test_event_validate(token_client, organizer, team, event):
|
||||
assert resp.data == {"_format": ["\"FOOBAR\" is not a valid choice."]}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_org_validate_events(token_client, organizer, team, event):
|
||||
resp = token_client.post('/api/v1/organizers/{}/exporters/orderlist/run/'.format(organizer.slug), data={
|
||||
'_format': 'xlsx',
|
||||
@@ -164,7 +164,7 @@ def test_org_validate_events(token_client, organizer, team, event):
|
||||
assert resp.data == {"events": [f"Object with slug={event.slug} does not exist."]}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_org_run_limit_events(token_client, organizer, team, event, event2):
|
||||
resp = token_client.post('/api/v1/organizers/{}/exporters/eventdata/run/'.format(organizer.slug), data={
|
||||
'_format': 'default',
|
||||
@@ -199,7 +199,7 @@ def test_org_run_limit_events(token_client, organizer, team, event, event2):
|
||||
assert resp.getvalue().strip().count(b"\n") == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_run_success(token_client, organizer, team, event):
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/exporters/orderlist/run/'.format(organizer.slug, event.slug), data={
|
||||
'_format': 'xlsx',
|
||||
@@ -212,7 +212,7 @@ def test_run_success(token_client, organizer, team, event):
|
||||
assert resp["Content-Type"] == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_run_success_old_date_frame(token_client, organizer, team, event):
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/exporters/orderlist/run/'.format(organizer.slug, event.slug), data={
|
||||
'_format': 'xlsx',
|
||||
@@ -261,7 +261,7 @@ def test_gone_without_celery(token_client, organizer, team, event):
|
||||
assert resp.status_code == 410
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_org_level_export(token_client, organizer, team, event):
|
||||
resp = token_client.post('/api/v1/organizers/{}/exporters/giftcardlist/run/'.format(organizer.slug), data={
|
||||
'date': '2022-10-05T00:00:00Z',
|
||||
|
||||
@@ -119,6 +119,23 @@ def test_sendmail_rule_create_min_fail(token_client, organizer, event):
|
||||
)
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
@pytest.mark.django_db
|
||||
def test_sendmail_rule_offset_zero(token_client, organizer, event):
|
||||
create_rule(
|
||||
token_client, organizer, event,
|
||||
data={
|
||||
'subject': {'en': 'meow'},
|
||||
'template': {'en': 'creative text here'},
|
||||
'send_date': '2018-03-17T13:31Z',
|
||||
'send_offset_days': '0',
|
||||
'send_offset_time': '08:40',
|
||||
'date_is_absolute': False,
|
||||
},
|
||||
expected_failure=False,
|
||||
)
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
@pytest.mark.django_db
|
||||
def test_sendmail_rule_create_minimal(token_client, organizer, event):
|
||||
|
||||
@@ -156,7 +156,7 @@ def test_event_fail_user_no_permission(event, user, team):
|
||||
assert djmail.outbox[0].to == [user.email]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@freeze_time("2023-01-18 03:00:00+01:00")
|
||||
def test_event_ok(event, user, team):
|
||||
djmail.outbox = []
|
||||
@@ -286,7 +286,7 @@ def test_organizer_fail_user_does_not_have_specific_permission(event, user, team
|
||||
assert djmail.outbox[0].to == [user.email]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@freeze_time("2023-01-18 03:00:00+01:00")
|
||||
def test_organizer_limited_to_events(event, user, team):
|
||||
djmail.outbox = []
|
||||
@@ -323,7 +323,7 @@ def test_organizer_limited_to_events(event, user, team):
|
||||
assert len(djmail.outbox[0].attachments[0][1].splitlines()) == 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@freeze_time("2023-01-18 03:00:00+01:00")
|
||||
def test_organizer_ok(event, user, team):
|
||||
djmail.outbox = []
|
||||
|
||||
@@ -633,7 +633,7 @@ class EventsTest(SoupTest):
|
||||
},
|
||||
follow=True
|
||||
)
|
||||
assert doc.select('.alert-warning')
|
||||
assert doc.select('.alert-danger')
|
||||
self.event1.settings.flush()
|
||||
# not yet saved
|
||||
assert "mail_from" not in self.event1.settings._cache()
|
||||
|
||||
@@ -52,7 +52,7 @@ def env():
|
||||
return event, user, t
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_event_export(client, env):
|
||||
client.login(email="dummy@dummy.dummy", password="dummy")
|
||||
response = client.get("/control/event/dummy/dummy/orders/export/?identifier=itemdata")
|
||||
@@ -69,7 +69,7 @@ def test_event_export(client, env):
|
||||
assert len(b"".join(response.streaming_content).split(b"\n")) == 3
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_event_export_schedule(client, env):
|
||||
client.login(email="dummy@dummy.dummy", password="dummy")
|
||||
response = client.get("/control/event/dummy/dummy/orders/export/?identifier=itemdata")
|
||||
@@ -161,7 +161,7 @@ def test_event_export_schedule(client, env):
|
||||
assert env[0].scheduled_exports.count() == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_event_limited_permission(client, env):
|
||||
env[2].can_change_event_settings = False
|
||||
env[2].save()
|
||||
@@ -212,7 +212,7 @@ def test_event_limited_permission(client, env):
|
||||
assert response.status_code == 302
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_organizer_export(client, env):
|
||||
client.login(email="dummy@dummy.dummy", password="dummy")
|
||||
response = client.get("/control/organizer/dummy/export/?identifier=eventdata")
|
||||
@@ -230,7 +230,7 @@ def test_organizer_export(client, env):
|
||||
assert len(b"".join(response.streaming_content).split(b"\n")) == 3
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_organizer_export_schedule(client, env):
|
||||
client.login(email="dummy@dummy.dummy", password="dummy")
|
||||
response = client.get("/control/organizer/dummy/export/?identifier=eventdata")
|
||||
@@ -312,7 +312,6 @@ def test_organizer_export_schedule(client, env):
|
||||
"schedule-mail_subject": "Product data, my friend!",
|
||||
"schedule-mail_template": "Mail body"
|
||||
}, follow=True)
|
||||
print(response.content)
|
||||
assert b"Your export schedule has been saved, but no next export is planned" in response.content
|
||||
s.refresh_from_db()
|
||||
assert s.schedule_next_run is None
|
||||
@@ -329,7 +328,7 @@ def test_organizer_export_schedule(client, env):
|
||||
assert env[0].organizer.scheduled_exports.count() == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_organizer_limited_permission(client, env):
|
||||
env[2].can_change_organizer_settings = False
|
||||
env[2].save()
|
||||
|
||||
@@ -221,7 +221,7 @@ class OrganizerTest(SoupTest):
|
||||
},
|
||||
follow=True
|
||||
)
|
||||
assert doc.select('.alert-warning')
|
||||
assert doc.select('.alert-danger')
|
||||
self.orga1.settings.flush()
|
||||
# not yet saved
|
||||
assert "mail_from" not in self.orga1.settings._cache()
|
||||
|
||||
@@ -95,8 +95,12 @@ def test_native_disabled(env, client):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_org_register(env, client):
|
||||
def test_org_register(env, client, mocker):
|
||||
from pretix.base.signals import customer_created
|
||||
mocker.patch('pretix.base.signals.customer_created.send')
|
||||
|
||||
signer = signing.TimestampSigner(salt='customer-registration-captcha-127.0.0.1')
|
||||
|
||||
r = client.post('/bigevents/account/register', {
|
||||
'email': 'john@example.org',
|
||||
'name_parts_0': 'John Doe',
|
||||
@@ -109,6 +113,7 @@ def test_org_register(env, client):
|
||||
customer = env[0].customers.get(email='john@example.org')
|
||||
assert not customer.is_verified
|
||||
assert customer.is_active
|
||||
customer_created.send.assert_called_once_with(customer.organizer, customer=customer)
|
||||
|
||||
r = client.post(
|
||||
f'/bigevents/account/activate?id={customer.identifier}&token={TokenGenerator().make_token(customer)}', {
|
||||
@@ -123,7 +128,10 @@ def test_org_register(env, client):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_org_register_duplicate_email(env, client):
|
||||
def test_org_register_duplicate_email(env, client, mocker):
|
||||
from pretix.base.signals import customer_created
|
||||
mocker.patch('pretix.base.signals.customer_created.send')
|
||||
|
||||
with scopes_disabled():
|
||||
env[0].customers.create(email='john@example.org')
|
||||
r = client.post('/bigevents/account/register', {
|
||||
@@ -132,6 +140,7 @@ def test_org_register_duplicate_email(env, client):
|
||||
})
|
||||
assert b'already registered' in r.content
|
||||
assert r.status_code == 200
|
||||
customer_created.send.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -167,7 +176,11 @@ def test_org_activate_invalid_token(env, client):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_org_login_logout(env, client):
|
||||
def test_org_login_logout(env, client, mocker):
|
||||
from pretix.base.signals import customer_signed_in
|
||||
mocker.patch('pretix.base.signals.customer_signed_in.send')
|
||||
|
||||
customer = None
|
||||
with scopes_disabled():
|
||||
customer = env[0].customers.create(email='john@example.org', is_verified=True)
|
||||
customer.set_password('foo')
|
||||
@@ -179,6 +192,8 @@ def test_org_login_logout(env, client):
|
||||
})
|
||||
assert r.status_code == 302
|
||||
|
||||
customer_signed_in.send.assert_called_once_with(customer.organizer, customer=customer)
|
||||
|
||||
r = client.get('/bigevents/account/')
|
||||
assert r.status_code == 200
|
||||
|
||||
@@ -190,7 +205,10 @@ def test_org_login_logout(env, client):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_org_login_invalid_password(env, client):
|
||||
def test_org_login_invalid_password(env, client, mocker):
|
||||
from pretix.base.signals import customer_signed_in
|
||||
mocker.patch('pretix.base.signals.customer_signed_in.send')
|
||||
|
||||
with scopes_disabled():
|
||||
customer = env[0].customers.create(email='john@example.org', is_verified=True)
|
||||
customer.set_password('foo')
|
||||
@@ -202,10 +220,15 @@ def test_org_login_invalid_password(env, client):
|
||||
})
|
||||
assert r.status_code == 200
|
||||
assert b'alert-danger' in r.content
|
||||
customer_signed_in.send.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_org_login_not_verified(env, client):
|
||||
def test_org_login_not_verified(env, client, mocker):
|
||||
from pretix.base.signals import customer_signed_in
|
||||
mocker.patch('pretix.base.signals.customer_signed_in.send')
|
||||
|
||||
customer = None
|
||||
with scopes_disabled():
|
||||
customer = env[0].customers.create(email='john@example.org', is_verified=False)
|
||||
customer.set_password('foo')
|
||||
@@ -217,10 +240,14 @@ def test_org_login_not_verified(env, client):
|
||||
})
|
||||
assert r.status_code == 200
|
||||
assert b'alert-danger' in r.content
|
||||
customer_signed_in.send.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_org_login_not_active(env, client):
|
||||
def test_org_login_not_active(env, client, mocker):
|
||||
from pretix.base.signals import customer_signed_in
|
||||
mocker.patch('pretix.base.signals.customer_signed_in.send')
|
||||
|
||||
with scopes_disabled():
|
||||
customer = env[0].customers.create(email='john@example.org', is_verified=True, is_active=False)
|
||||
customer.set_password('foo')
|
||||
@@ -232,6 +259,7 @@ def test_org_login_not_active(env, client):
|
||||
})
|
||||
assert r.status_code == 200
|
||||
assert b'alert-danger' in r.content
|
||||
customer_signed_in.send.assert_not_called()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -309,12 +337,18 @@ def _sso_login(client, provider, email='test@example.org', popup_origin=None, ex
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_org_sso_login_new_customer(env, client, provider):
|
||||
def test_org_sso_login_new_customer(env, client, provider, mocker):
|
||||
from pretix.base.signals import customer_created, customer_signed_in
|
||||
mocker.patch('pretix.base.signals.customer_created.send')
|
||||
mocker.patch('pretix.base.signals.customer_signed_in.send')
|
||||
|
||||
_sso_login(client, provider)
|
||||
|
||||
with scopes_disabled():
|
||||
c = Customer.objects.get(provider=provider)
|
||||
assert c.external_identifier == "abcdf"
|
||||
customer_created.send.assert_called_once_with(c.organizer, customer=c)
|
||||
customer_signed_in.send.assert_called_once_with(c.organizer, customer=c)
|
||||
|
||||
r = client.get('/bigevents/account/')
|
||||
assert r.status_code == 200
|
||||
@@ -352,10 +386,15 @@ def test_org_sso_login_new_customer_popup_invalid_origin(env, client, provider):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_org_sso_login_returning_customer_new_email(env, client, provider):
|
||||
def test_org_sso_login_returning_customer_new_email(env, client, provider, mocker):
|
||||
from pretix.base.signals import customer_signed_in
|
||||
mocker.patch('pretix.base.signals.customer_signed_in.send')
|
||||
|
||||
_sso_login(client, provider)
|
||||
with scopes_disabled():
|
||||
c = Customer.objects.get(provider=provider)
|
||||
customer_signed_in.send.assert_called_once_with(c.organizer, customer=c)
|
||||
customer_signed_in.send.reset_mock()
|
||||
|
||||
r = client.get('/bigevents/account/logout')
|
||||
assert r.status_code == 302
|
||||
@@ -363,6 +402,7 @@ def test_org_sso_login_returning_customer_new_email(env, client, provider):
|
||||
_sso_login(client, provider, 'new@example.net')
|
||||
c.refresh_from_db()
|
||||
assert c.email == "new@example.net"
|
||||
customer_signed_in.send.assert_called_once_with(c.organizer, customer=c)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
|
||||
Reference in New Issue
Block a user