mirror of
https://github.com/pretix/pretix.git
synced 2026-08-28 13:34:40 +00:00
Compare commits
45
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0630b67546 | ||
|
|
9101238743 | ||
|
|
cefbfc1ad1 | ||
|
|
69a798046e | ||
|
|
c7983bf811 | ||
|
|
042be3603b | ||
|
|
1de2320cc5 | ||
|
|
8042d9d3f4 | ||
|
|
44afe9e193 | ||
|
|
46dce1bf43 | ||
|
|
4865879978 | ||
|
|
4c793076b7 | ||
|
|
5294d819a9 | ||
|
|
7bfe94139a | ||
|
|
e5cbaa9246 | ||
|
|
37208286b1 | ||
|
|
6da56291d4 | ||
|
|
2d335cd095 | ||
|
|
569f1719b0 | ||
|
|
00dd1a5b31 | ||
|
|
18bebb6d31 | ||
|
|
77c8e81cd7 | ||
|
|
3d03f30119 | ||
|
|
91b2d685da | ||
|
|
9787ed1820 | ||
|
|
204b8e53de | ||
|
|
64358be4ae | ||
|
|
5b1175ff05 | ||
|
|
e6f56bfdc2 | ||
|
|
9610e9c89f | ||
|
|
c5f4eeeb28 | ||
|
|
b61880fb5b | ||
|
|
b29c7fc11d | ||
|
|
d99bf7437a | ||
|
|
648cc14ae0 | ||
|
|
5d71cb500a | ||
|
|
68d81982ba | ||
|
|
efa0d5f362 | ||
|
|
046898678b | ||
|
|
f38ecd0ec7 | ||
|
|
3dca6c232e | ||
|
|
297bf566ad | ||
|
|
67f09b5ede | ||
|
|
752137ad84 | ||
|
|
100528ad0f |
+16
-16
@@ -1,30 +1,29 @@
|
||||
before_script:
|
||||
tests:
|
||||
image:
|
||||
name: pretix/ci-image
|
||||
stage: test
|
||||
before_script:
|
||||
- pip install -U pip uv
|
||||
- uv pip install --system -U wheel setuptools
|
||||
script:
|
||||
- uv pip install --system -e ".[dev]"
|
||||
- virtualenv env
|
||||
- source env/bin/activate
|
||||
- pip install -U pip wheel setuptools
|
||||
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
|
||||
- cd src
|
||||
- python manage.py check
|
||||
- make all compress
|
||||
- PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg py.test --reruns 3 -n 3 tests --maxfail=100
|
||||
- py.test --reruns 3 -n 3 tests
|
||||
tags:
|
||||
- python3
|
||||
except:
|
||||
- pypi
|
||||
pypi:
|
||||
stage: release
|
||||
image:
|
||||
name: pretix/ci-image
|
||||
before_script:
|
||||
- cat $PYPIRC > ~/.pypirc
|
||||
- pip install -U pip uv
|
||||
- uv pip install --system -U wheel setuptools twine build pretix-plugin-build check-manifest
|
||||
script:
|
||||
- uv pip install --system -e ".[dev]"
|
||||
- cp /keys/.pypirc ~/.pypirc
|
||||
- virtualenv env
|
||||
- source env/bin/activate
|
||||
- pip install -U pip wheel setuptools check-manifest twine
|
||||
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
|
||||
- python setup.py sdist
|
||||
- uv pip install --system dist/pretix-*.tar.gz
|
||||
- pip install dist/pretix-*.tar.gz
|
||||
- python -m pretix migrate
|
||||
- python -m pretix check
|
||||
- cd src
|
||||
@@ -34,12 +33,13 @@ pypi:
|
||||
- python -m build
|
||||
- twine check dist/*
|
||||
- twine upload dist/*
|
||||
tags:
|
||||
- python3
|
||||
only:
|
||||
- pypi
|
||||
artifacts:
|
||||
paths:
|
||||
- src/dist/
|
||||
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
|
||||
@@ -3,16 +3,11 @@
|
||||
.. _`community`:
|
||||
|
||||
Community install guides
|
||||
========================
|
||||
================================
|
||||
|
||||
.. warning:: The guides are maintained by the community and not by the pretix core team. If you encounter any issues with the guides, please report them to the maintainers of the guides. The pretix core team can not provide support for installs using these guides.
|
||||
|
||||
Kubernetes
|
||||
----------
|
||||
|
||||
------------
|
||||
- Helm Chart by techwolf12 - A Helm chart for deploying pretix on Kubernetes. The chart documentation is available on `ArtifactHub <https://artifacthub.io/packages/helm/techwolf12/pretix>`_ and the source code is available on `GitHub <https://github.com/Techwolf12/charts/tree/main/pretix-helm>`_.
|
||||
|
||||
Docker
|
||||
------
|
||||
|
||||
- `docker compose setup <https://github.com/ZPascal/pretix-docker-compose>`_ by ZPascal
|
||||
@@ -103,10 +103,10 @@ pretix_celery_tasks_queued_count
|
||||
pretix_celery_tasks_queued_age_seconds
|
||||
The age of the longest-waiting in the worker queue in seconds, labeled with ``queue``.
|
||||
|
||||
pretix_logins_successful
|
||||
pretix_successful_logins
|
||||
Counter. The number of successful backend logins.
|
||||
|
||||
pretix_logins_failed
|
||||
pretix_failed_logins
|
||||
Counter. The number of failed backend logins, labeled with ``reason``.
|
||||
|
||||
.. _metric types: https://prometheus.io/docs/concepts/metric_types/
|
||||
|
||||
@@ -15,7 +15,7 @@ includes serializers for serializing the following types:
|
||||
* Built-in types: ``int``, ``float``, ``decimal.Decimal``, ``dict``, ``list``, ``bool``
|
||||
* ``datetime.date``, ``datetime.datetime``, ``datetime.time``
|
||||
* ``LazyI18nString``
|
||||
* References to Django ``File`` objects that are already stored in a storage backend [#f1]_
|
||||
* References to Django ``File`` objects that are already stored in a storage backend
|
||||
* References to model instances
|
||||
|
||||
In code, we recommend to always use the ``.get()`` method on the settings object to access a value, but for
|
||||
@@ -55,9 +55,6 @@ You can simply use it like this:
|
||||
"preserve his reservation."),
|
||||
)
|
||||
|
||||
|
||||
.. _settings-defaults-in-plugins:
|
||||
|
||||
Defaults in plugins
|
||||
-------------------
|
||||
|
||||
@@ -73,9 +70,3 @@ Make sure that you include this code in a module that is imported at app loading
|
||||
|
||||
.. _django-hierarkey: https://github.com/raphaelm/django-hierarkey
|
||||
.. _documentation: https://django-hierarkey.readthedocs.io/en/latest/
|
||||
|
||||
.. rubric:: Footnotes
|
||||
|
||||
.. [#f1] If you store ``File`` instances in per-event settings, make sure to always register them with ``add_default``
|
||||
as described above in :ref:`settings-defaults-in-plugins`. Otherwise, the file won't get copied properly if the
|
||||
user copies the settings of an existing event to a new one.
|
||||
|
||||
@@ -41,6 +41,6 @@ pretix/
|
||||
|
||||
tests/
|
||||
This is the root directory for all test codes. It includes subdirectories ``api``, ``base``,
|
||||
``control``, ``presale``, ``helpers``, ``multidomain`` and ``plugins`` to mirror the structure
|
||||
``control``, ``presale``, ``helpers`, ``multidomain`` and ``plugins`` to mirror the structure
|
||||
of the pretix source code as well as ``testdummy``, which is a pretix plugin used during
|
||||
testing.
|
||||
|
||||
@@ -45,8 +45,6 @@ allow_voucher_access boolean Enables access
|
||||
lead_scanning_scope_by_device string Enables lead scanning to be handled as one lead per attendee
|
||||
per scanning device, instead of only per exhibitor.
|
||||
comment string Internal comment, not shown to exhibitor
|
||||
exhibitor_tags list of strings Internal tags to categorize exhibitors, not shown to exhibitor.
|
||||
The tags need to be created through the web interface currently.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
You can also access the scanned leads through the API which contains the following public fields:
|
||||
@@ -121,8 +119,7 @@ Endpoints
|
||||
"allow_lead_scanning": true,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
"comment": "",
|
||||
"exhibitor_tags": []
|
||||
"comment": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -176,8 +173,7 @@ Endpoints
|
||||
"allow_lead_scanning": true,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
"comment": "",
|
||||
"exhibitor_tags": []
|
||||
"comment": ""
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -378,10 +374,7 @@ Endpoints
|
||||
"allow_lead_scanning": true,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
"comment": "",
|
||||
"exhibitor_tags": [
|
||||
"Gold Sponsor"
|
||||
]
|
||||
"comment": ""
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -414,10 +407,7 @@ Endpoints
|
||||
"allow_lead_scanning": true,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
"comment": "",
|
||||
"exhibitor_tags": [
|
||||
"Gold Sponsor"
|
||||
]
|
||||
"comment": ""
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create new exhibitor for
|
||||
@@ -478,10 +468,7 @@ Endpoints
|
||||
"allow_lead_scanning": true,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
"comment": "",
|
||||
"exhibitor_tags": [
|
||||
"Gold Sponsor"
|
||||
]
|
||||
"comment": ""
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
|
||||
@@ -339,9 +339,9 @@ Currently, the following attributes are understood by pretix itself:
|
||||
``data-attendee-name``, which will pre-fill the last part of the name, whatever that is.
|
||||
|
||||
* ``data-invoice-address-FIELD`` will pre-fill the corresponding field of the invoice address. Possible values for
|
||||
``FIELD`` are ``company``, ``street``, ``zipcode``, ``city``, ``country``, ``internal-reference``, ``vat-id``, and
|
||||
``custom-field``, as well as fields specified by the naming scheme such as ``name-title`` or ``name-given-name``
|
||||
(see above). ``country`` expects a two-character country code.
|
||||
``FIELD`` are ``company``, ``street``, ``zipcode``, ``city`` and ``country``, as well as fields specified by the
|
||||
naming scheme such as ``name-title`` or ``name-given-name`` (see above). ``country`` expects a two-character
|
||||
country code.
|
||||
|
||||
* If ``data-fix="true"`` is given, the user will not be able to change the other given values later. This currently
|
||||
only works for the order email address as well as the invoice address. Attendee-level fields and questions can
|
||||
|
||||
+10
-10
@@ -36,15 +36,15 @@ dependencies = [
|
||||
"css-inline==0.14.*",
|
||||
"defusedcsv>=1.1.0",
|
||||
"dj-static",
|
||||
"Django[argon2]==4.2.*",
|
||||
"Django==4.2.*",
|
||||
"django-bootstrap3==24.2",
|
||||
"django-compressor==4.5",
|
||||
"django-compressor==4.4",
|
||||
"django-countries==7.6.*",
|
||||
"django-filter==24.2",
|
||||
"django-formset-js-improved==0.5.0.3",
|
||||
"django-formtools==2.5.1",
|
||||
"django-hierarkey==1.2.*",
|
||||
"django-hijack==3.5.*",
|
||||
"django-hierarkey==1.1.*",
|
||||
"django-hijack==3.4.*",
|
||||
"django-i18nfield==1.9.*,>=1.9.4",
|
||||
"django-libsass==0.9",
|
||||
"django-localflavor==4.0",
|
||||
@@ -77,12 +77,12 @@ dependencies = [
|
||||
"phonenumberslite==8.13.*",
|
||||
"Pillow==10.3.*",
|
||||
"pretix-plugin-build",
|
||||
"protobuf==5.27.*",
|
||||
"protobuf==5.26.*",
|
||||
"psycopg2-binary",
|
||||
"pycountry",
|
||||
"pycparser==2.22",
|
||||
"pycryptodome==3.20.*",
|
||||
"pypdf==4.2.*",
|
||||
"pypdf==3.9.*",
|
||||
"python-bidi==0.4.*", # Support for Arabic in reportlab
|
||||
"python-dateutil==2.9.*",
|
||||
"pytz",
|
||||
@@ -92,7 +92,7 @@ dependencies = [
|
||||
"redis==5.0.*",
|
||||
"reportlab==4.2.*",
|
||||
"requests==2.31.*",
|
||||
"sentry-sdk==2.5.*",
|
||||
"sentry-sdk==1.45.*",
|
||||
"sepaxml==2.6.*",
|
||||
"slimit",
|
||||
"static3==0.7.*",
|
||||
@@ -113,11 +113,11 @@ dev = [
|
||||
"aiohttp==3.9.*",
|
||||
"coverage",
|
||||
"coveralls",
|
||||
"fakeredis==2.23.*",
|
||||
"flake8==7.1.*",
|
||||
"fakeredis==2.22.*",
|
||||
"flake8==7.0.*",
|
||||
"freezegun",
|
||||
"isort==5.13.*",
|
||||
"pep8-naming==0.14.*",
|
||||
"pep8-naming==0.13.*",
|
||||
"potypo",
|
||||
"pytest-asyncio",
|
||||
"pytest-cache",
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
__version__ = "2024.6.0.dev0"
|
||||
__version__ = "2024.5.0.dev0"
|
||||
|
||||
@@ -79,7 +79,6 @@ ALL_LANGUAGES = [
|
||||
('de', _('German')),
|
||||
('de-informal', _('German (informal)')),
|
||||
('ar', _('Arabic')),
|
||||
('ca', _('Catalan')),
|
||||
('zh-hans', _('Chinese (simplified)')),
|
||||
('zh-hant', _('Chinese (traditional)')),
|
||||
('cs', _('Czech')),
|
||||
@@ -99,7 +98,6 @@ ALL_LANGUAGES = [
|
||||
('pt-br', _('Portuguese (Brazil)')),
|
||||
('ro', _('Romanian')),
|
||||
('ru', _('Russian')),
|
||||
('sk', _('Slovak')),
|
||||
('es', _('Spanish')),
|
||||
('tr', _('Turkish')),
|
||||
('uk', _('Ukrainian')),
|
||||
|
||||
@@ -684,9 +684,8 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'locales',
|
||||
'locale',
|
||||
'region',
|
||||
'allow_modifications',
|
||||
'allow_modifications_after_checkin',
|
||||
'last_order_modification_date',
|
||||
'allow_modifications_after_checkin',
|
||||
'show_quota_left',
|
||||
'waiting_list_enabled',
|
||||
'waiting_list_auto_disable',
|
||||
@@ -736,7 +735,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'payment_term_accept_late',
|
||||
'payment_explanation',
|
||||
'payment_pending_hidden',
|
||||
'payment_giftcard__enabled',
|
||||
'mail_days_order_expire_warning',
|
||||
'ticket_download',
|
||||
'ticket_download_date',
|
||||
|
||||
@@ -564,8 +564,6 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
attendee_name = AttendeeNameField(source='*')
|
||||
attendee_name_parts = AttendeeNamePartsField(source='*')
|
||||
order__status = serializers.SlugRelatedField(read_only=True, slug_field='status', source='order')
|
||||
order__valid_if_pending = serializers.SlugRelatedField(read_only=True, slug_field='valid_if_pending', source='order')
|
||||
order__require_approval = serializers.SlugRelatedField(read_only=True, slug_field='require_approval', source='order')
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
@@ -573,8 +571,7 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'require_attention',
|
||||
'order__status', 'order__valid_if_pending', 'order__require_approval', 'valid_from', 'valid_until',
|
||||
'blocked')
|
||||
'order__status', 'valid_from', 'valid_until', 'blocked')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from collections import OrderedDict, defaultdict
|
||||
from collections import OrderedDict
|
||||
from decimal import Decimal
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
@@ -605,9 +605,10 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
]
|
||||
|
||||
questions = list(Question.objects.filter(event__in=self.events))
|
||||
options = defaultdict(list)
|
||||
options = {}
|
||||
for q in questions:
|
||||
if q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
options[q.pk] = []
|
||||
if form_data['group_multiple_choice']:
|
||||
for o in q.options.all():
|
||||
options[q.pk].append(o)
|
||||
@@ -617,9 +618,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
headers.append(str(q.question) + ' – ' + str(o.answer))
|
||||
options[q.pk].append(o)
|
||||
else:
|
||||
if q.type == Question.TYPE_CHOICE:
|
||||
for o in q.options.all():
|
||||
options[q.pk].append(o)
|
||||
headers.append(str(q.question))
|
||||
headers += [
|
||||
_('Company'),
|
||||
@@ -729,7 +727,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
for a in op.answers.all():
|
||||
# We do not want to localize Date, Time and Datetime question answers, as those can lead
|
||||
# to difficulties parsing the data (for example 2019-02-01 may become Février, 2019 01 in French).
|
||||
if a.question.type in (Question.TYPE_CHOICE_MULTIPLE, Question.TYPE_CHOICE):
|
||||
if a.question.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
acache[a.question_id] = set(o.pk for o in a.options.all())
|
||||
elif a.question.type in Question.UNLOCALIZED_TYPES:
|
||||
acache[a.question_id] = a.answer
|
||||
@@ -742,10 +740,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
else:
|
||||
for o in options[q.pk]:
|
||||
row.append(_('Yes') if o.pk in acache.get(q.pk, set()) else _('No'))
|
||||
elif q.type == Question.TYPE_CHOICE:
|
||||
# Join is only necessary if the question type was modified but also keeps the code simpler here
|
||||
# as we'd otherwise need some [0] and existance checks
|
||||
row.append(", ".join(str(o.answer) for o in options[q.pk] if o.pk in acache.get(q.pk, set())))
|
||||
else:
|
||||
row.append(acache.get(q.pk, ''))
|
||||
|
||||
|
||||
@@ -1035,7 +1035,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
self.all_optional = kwargs.pop('all_optional', False)
|
||||
|
||||
kwargs.setdefault('initial', {})
|
||||
if (not kwargs.get('instance') or not kwargs['instance'].country) and not kwargs["initial"].get("country"):
|
||||
if not kwargs.get('instance') or not kwargs['instance'].country:
|
||||
kwargs['initial']['country'] = guess_country_from_request(self.request, self.event)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -1171,7 +1171,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
self.instance.vat_id_validated = False
|
||||
messages.warning(self.request, e.message)
|
||||
else:
|
||||
raise ValidationError({"vat_id": e.message})
|
||||
raise ValidationError(e.message)
|
||||
except VATIDTemporaryError as e:
|
||||
self.instance.vat_id_validated = False
|
||||
if self.request and self.vat_warning:
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
from bootstrap3.renderers import FieldRenderer
|
||||
from bootstrap3.text import text_value
|
||||
from django.forms import CheckboxInput, CheckboxSelectMultiple, RadioSelect
|
||||
from django.forms.utils import flatatt
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import pgettext
|
||||
|
||||
|
||||
def render_label(content, label_for=None, label_class=None, label_title='', label_id='', optional=False, is_valid=None, attrs=None):
|
||||
"""
|
||||
Render a label with content
|
||||
"""
|
||||
attrs = attrs or {}
|
||||
if label_for:
|
||||
attrs['for'] = label_for
|
||||
if label_class:
|
||||
attrs['class'] = label_class
|
||||
if label_title:
|
||||
attrs['title'] = label_title
|
||||
if label_id:
|
||||
attrs['id'] = label_id
|
||||
|
||||
opt = ""
|
||||
|
||||
if is_valid is not None:
|
||||
if is_valid:
|
||||
validation_text = pgettext('form', 'is valid')
|
||||
else:
|
||||
validation_text = pgettext('form', 'has errors')
|
||||
opt += '<strong class="sr-only"> {}</strong>'.format(validation_text)
|
||||
|
||||
if text_value(content) == ' ':
|
||||
# Empty label, e.g. checkbox
|
||||
attrs.setdefault('class', '')
|
||||
attrs['class'] += ' label-empty'
|
||||
# usually checkboxes have overall empty labels and special labels per checkbox
|
||||
# => remove for-attribute as well as "required"-text appended to label
|
||||
if 'for' in attrs:
|
||||
del attrs['for']
|
||||
else:
|
||||
opt += '<i class="sr-only label-required">, {}</i>'.format(pgettext('form', 'required')) if not optional else ''
|
||||
|
||||
builder = '<{tag}{attrs}>{content}{opt}</{tag}>'
|
||||
return format_html(
|
||||
builder,
|
||||
tag='label',
|
||||
attrs=mark_safe(flatatt(attrs)) if attrs else '',
|
||||
opt=mark_safe(opt),
|
||||
content=text_value(content),
|
||||
)
|
||||
|
||||
|
||||
class PretixFieldRenderer(FieldRenderer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['layout'] = 'horizontal'
|
||||
super().__init__(*args, **kwargs)
|
||||
self.is_group_widget = isinstance(self.widget, (CheckboxSelectMultiple, RadioSelect, )) or (self.is_multi_widget and len(self.widget.widgets) > 1)
|
||||
|
||||
def add_label(self, html):
|
||||
attrs = {}
|
||||
label = self.get_label()
|
||||
|
||||
if hasattr(self.field.field, '_show_required'):
|
||||
# e.g. payment settings forms where a field is only required if the payment provider is active
|
||||
required = self.field.field._show_required
|
||||
elif hasattr(self.field.field, '_required'):
|
||||
# e.g. payment settings forms where a field is only required if the payment provider is active
|
||||
required = self.field.field._required
|
||||
else:
|
||||
required = self.field.field.required
|
||||
|
||||
if self.field.form.is_bound:
|
||||
is_valid = len(self.field.errors) == 0
|
||||
else:
|
||||
is_valid = None
|
||||
|
||||
if self.is_group_widget:
|
||||
label_for = ""
|
||||
label_id = "legend-{}".format(self.field.html_name)
|
||||
else:
|
||||
label_for = self.field.id_for_label
|
||||
label_id = ""
|
||||
|
||||
if hasattr(self.field.field, 'question') and self.field.field.question.identifier:
|
||||
attrs["data-identifier"] = self.field.field.question.identifier
|
||||
|
||||
html = render_label(
|
||||
label,
|
||||
label_for=label_for,
|
||||
label_class=self.get_label_class(),
|
||||
label_id=label_id,
|
||||
attrs=attrs,
|
||||
optional=not required and not isinstance(self.widget, CheckboxInput),
|
||||
is_valid=is_valid
|
||||
) + html
|
||||
return html
|
||||
|
||||
def wrap_label_and_field(self, html):
|
||||
if self.is_group_widget:
|
||||
attrs = ' role="group" aria-labelledby="legend-{}"'.format(self.field.html_name)
|
||||
else:
|
||||
attrs = ''
|
||||
return '<div class="{klass}"{attrs}>{html}</div>'.format(klass=self.get_form_group_class(), html=html, attrs=attrs)
|
||||
|
||||
def wrap_widget(self, html):
|
||||
if isinstance(self.widget, CheckboxInput):
|
||||
css_class = "checkbox"
|
||||
if self.field.field.disabled:
|
||||
css_class += " disabled"
|
||||
html = f'<div class="{css_class}">{html}</div>'
|
||||
return html
|
||||
@@ -32,13 +32,13 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import re
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import BaseValidator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.helpers.format import format_map
|
||||
|
||||
|
||||
class PlaceholderValidator(BaseValidator):
|
||||
"""
|
||||
@@ -47,12 +47,6 @@ class PlaceholderValidator(BaseValidator):
|
||||
which are not presented in taken list.
|
||||
"""
|
||||
|
||||
error_message = _(
|
||||
'There is an error with your placeholder syntax. Please check that the opening "{" and closing "}" curly '
|
||||
'brackets on your placeholders match up. '
|
||||
'Please note: to use literal "{" or "}", you need to double them as "{{" and "}}".'
|
||||
)
|
||||
|
||||
def __init__(self, limit_value):
|
||||
super().__init__(limit_value)
|
||||
self.limit_value = limit_value
|
||||
@@ -63,15 +57,22 @@ class PlaceholderValidator(BaseValidator):
|
||||
self.__call__(v)
|
||||
return
|
||||
|
||||
try:
|
||||
format_map(value, {key.strip('{}'): "" for key in self.limit_value}, raise_on_missing=True)
|
||||
except ValueError:
|
||||
raise ValidationError(self.error_message, code='invalid_placeholder_syntax')
|
||||
except KeyError as e:
|
||||
if value.count('{') != value.count('}'):
|
||||
raise ValidationError(
|
||||
_('Invalid placeholder: {%(value)s}'),
|
||||
_('Invalid placeholder syntax: You used a different number of "{" than of "}".'),
|
||||
code='invalid_placeholder_syntax',
|
||||
)
|
||||
|
||||
data_placeholders = list(re.findall(r'({[^}]*})', value, re.X))
|
||||
invalid_placeholders = []
|
||||
for placeholder in data_placeholders:
|
||||
if placeholder not in self.limit_value:
|
||||
invalid_placeholders.append(placeholder)
|
||||
if invalid_placeholders:
|
||||
raise ValidationError(
|
||||
_('Invalid placeholder(s): %(value)s'),
|
||||
code='invalid_placeholders',
|
||||
params={'value': e.args[0]})
|
||||
params={'value': ", ".join(invalid_placeholders,)})
|
||||
|
||||
def clean(self, x):
|
||||
return x
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Generated by Django 4.2.11 on 2024-05-16 11:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.orders
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0263_auto_20240409_0732"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="order",
|
||||
name="internal_secret",
|
||||
field=models.CharField(
|
||||
default=None,
|
||||
max_length=32,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -19,7 +19,6 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
import pycountry
|
||||
@@ -27,7 +26,6 @@ from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import EmailValidator
|
||||
from django.db.models import Q
|
||||
from django.utils import formats
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import (
|
||||
gettext as _, gettext_lazy, pgettext, pgettext_lazy,
|
||||
@@ -511,30 +509,6 @@ class ValidUntil(DatetimeColumnMixin, ImportColumn):
|
||||
position.valid_until = value
|
||||
|
||||
|
||||
class Expires(DatetimeColumnMixin, ImportColumn):
|
||||
identifier = 'expires'
|
||||
verbose_name = gettext_lazy('Expiry date')
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if not value:
|
||||
return
|
||||
|
||||
input_formats = formats.get_format('DATE_INPUT_FORMATS', use_l10n=True)
|
||||
for format in input_formats:
|
||||
try:
|
||||
d = datetime.datetime.strptime(value, format)
|
||||
d = d.replace(tzinfo=self.timezone, hour=23, minute=59, second=59)
|
||||
return d
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
else:
|
||||
return super().clean(value, previous_values) # parse date
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
if value:
|
||||
order.expires = value
|
||||
|
||||
|
||||
class Saleschannel(ImportColumn):
|
||||
identifier = 'sales_channel'
|
||||
verbose_name = gettext_lazy('Sales channel')
|
||||
@@ -728,13 +702,12 @@ def get_order_import_columns(event):
|
||||
AttendeeState(event),
|
||||
Price(event),
|
||||
Secret(event),
|
||||
SeatColumn(event),
|
||||
ValidFrom(event),
|
||||
ValidUntil(event),
|
||||
Locale(event),
|
||||
Saleschannel(event),
|
||||
Expires(event),
|
||||
SeatColumn(event),
|
||||
Comment(event),
|
||||
ValidFrom(event),
|
||||
ValidUntil(event),
|
||||
]
|
||||
for q in event.questions.prefetch_related('options').exclude(type=Question.TYPE_FILE):
|
||||
default.append(QuestionColumn(event, q))
|
||||
|
||||
@@ -44,6 +44,8 @@ class CodeColumn(ImportColumn):
|
||||
super().__init__(*args)
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if not value:
|
||||
raise ValidationError(_('A voucher cannot be created without a code.'))
|
||||
if value:
|
||||
MinLengthValidator(5)(value)
|
||||
if value and (value in self._cached or Voucher.objects.filter(event=self.event, code=value).exists()):
|
||||
@@ -75,7 +77,7 @@ class MaxUsagesColumn(IntegerColumnMixin, ImportColumn):
|
||||
]
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value is None and previous_values.get("code"):
|
||||
if value is None:
|
||||
raise ValidationError(_('The maximum number of usages must be set.'))
|
||||
return super().clean(value, previous_values)
|
||||
|
||||
|
||||
@@ -418,22 +418,18 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
else:
|
||||
return set()
|
||||
|
||||
def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=None) -> bool:
|
||||
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
|
||||
"""
|
||||
Checks if this user is part of any team that grants access of type ``perm_name``
|
||||
to the event ``event``.
|
||||
|
||||
Either ``request`` or ``session_key`` are required to detect staff sessions properly.
|
||||
|
||||
:param organizer: The organizer of the event
|
||||
:param event: The event to check
|
||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||
:param request: The current request (optional)
|
||||
:param session_key: The current session key (optional)
|
||||
:param request: The current request (optional). Required to detect staff sessions properly.
|
||||
:return: bool
|
||||
"""
|
||||
assert not (session_key and request)
|
||||
if (session_key or request) and self.has_active_staff_session(session_key or request.session.session_key):
|
||||
if request and self.has_active_staff_session(request.session.session_key):
|
||||
return True
|
||||
teams = self._get_teams_for_event(organizer, event)
|
||||
if teams:
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
from collections import defaultdict
|
||||
from decimal import Decimal
|
||||
from itertools import groupby
|
||||
from math import ceil
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -273,7 +272,7 @@ class Discount(LoggedModel):
|
||||
|
||||
# Prevent over-consuming of items, i.e. if our discount is "buy 2, get 1 free", we only
|
||||
# want to match multiples of 3
|
||||
n_groups = min(len(condition_idx_group) // self.condition_min_count, ceil(len(benefit_idx_group) / self.benefit_only_apply_to_cheapest_n_matches))
|
||||
n_groups = min(len(condition_idx_group) // self.condition_min_count, len(benefit_idx_group))
|
||||
consume_idx = condition_idx_group[:n_groups * self.condition_min_count]
|
||||
benefit_idx = benefit_idx_group[:n_groups * self.benefit_only_apply_to_cheapest_n_matches]
|
||||
else:
|
||||
|
||||
@@ -45,7 +45,6 @@ from zoneinfo import ZoneInfo
|
||||
import pytz_deprecation_shim
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files import File
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.mail import get_connection
|
||||
from django.core.validators import (
|
||||
@@ -1027,7 +1026,7 @@ class Event(EventMixin, LoggedModel):
|
||||
|
||||
s.object = self
|
||||
s.pk = None
|
||||
if s.value.startswith('file://') and settings_hierarkey.get_declared_type(s.key) == File:
|
||||
if s.value.startswith('file://'):
|
||||
fi = default_storage.open(s.value[len('file://'):], 'rb')
|
||||
nonce = get_random_string(length=8)
|
||||
fname_base = clean_filename(os.path.basename(s.value))
|
||||
@@ -1468,6 +1467,8 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
seating_plan = models.ForeignKey('SeatingPlan', on_delete=models.PROTECT, null=True, blank=True,
|
||||
related_name='subevents', verbose_name=_('Seating plan'))
|
||||
|
||||
items = models.ManyToManyField('Item', through='SubEventItem')
|
||||
variations = models.ManyToManyField('ItemVariation', through='SubEventItemVariation')
|
||||
comment = models.TextField(
|
||||
verbose_name=_("Internal comment"),
|
||||
null=True, blank=True
|
||||
|
||||
@@ -444,8 +444,7 @@ class Item(LoggedModel):
|
||||
free_price_suggestion = models.DecimalField(
|
||||
verbose_name=_("Suggested price"),
|
||||
help_text=_("This price will be used as the default value of the input field. The user can choose a lower "
|
||||
"value, but not lower than the price this product would have without the free price option. This "
|
||||
"will be ignored if a voucher is used that lowers the price."),
|
||||
"value, but not lower than the price this product would have without the free price option."),
|
||||
max_digits=13, decimal_places=2, null=True, blank=True,
|
||||
)
|
||||
tax_rule = models.ForeignKey(
|
||||
@@ -1087,8 +1086,7 @@ class ItemVariation(models.Model):
|
||||
free_price_suggestion = models.DecimalField(
|
||||
verbose_name=_("Suggested price"),
|
||||
help_text=_("This price will be used as the default value of the input field. The user can choose a lower "
|
||||
"value, but not lower than the price this product would have without the free price option. This "
|
||||
"will be ignored if a voucher is used that lowers the price."),
|
||||
"value, but not lower than the price this product would have without the free price option."),
|
||||
max_digits=13, decimal_places=2, null=True, blank=True,
|
||||
)
|
||||
require_approval = models.BooleanField(
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import operator
|
||||
@@ -60,7 +59,7 @@ from django.db.models.functions import Coalesce, Greatest
|
||||
from django.db.models.signals import post_delete
|
||||
from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string, salted_hmac
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.encoding import escape_uri_path
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.functional import cached_property
|
||||
@@ -106,34 +105,6 @@ def generate_position_secret():
|
||||
raise TypeError("Function no longer exists, use secret generators")
|
||||
|
||||
|
||||
class OrderQuerySet(models.QuerySet):
|
||||
def get_with_secret_check(self, code, received_secret, tag, secret_length=64):
|
||||
dummy = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"[:secret_length]
|
||||
try:
|
||||
order = self.get(code=code)
|
||||
except Order.DoesNotExist:
|
||||
# Do a hash comparison as well to harden against timing attacks
|
||||
hmac.compare_digest(
|
||||
salted_hmac(key_salt=b"", value=tag, algorithm="sha256",
|
||||
secret=dummy).hexdigest()[:secret_length],
|
||||
received_secret[:secret_length]
|
||||
)
|
||||
raise Order.DoesNotExist
|
||||
|
||||
if not hmac.compare_digest(
|
||||
order.tagged_secret(tag, secret_length) if tag else order.secret,
|
||||
received_secret[:secret_length].lower() if tag else received_secret.lower()
|
||||
) and not (
|
||||
# TODO: remove this clause after a while (compatibility with old secrets currently in flight)
|
||||
tag and hmac.compare_digest(
|
||||
hashlib.sha1(order.secret.lower().encode()).hexdigest(),
|
||||
received_secret.lower()
|
||||
)
|
||||
):
|
||||
raise Order.DoesNotExist
|
||||
return order
|
||||
|
||||
|
||||
class Order(LockModel, LoggedModel):
|
||||
"""
|
||||
An order is created when a user clicks 'buy' on his cart. It holds
|
||||
@@ -252,7 +223,6 @@ class Order(LockModel, LoggedModel):
|
||||
verbose_name=_('Locale')
|
||||
)
|
||||
secret = models.CharField(max_length=32, default=generate_secret)
|
||||
internal_secret = models.CharField(null=True, blank=True, max_length=32, default=generate_secret)
|
||||
datetime = models.DateTimeField(
|
||||
verbose_name=_("Date"), db_index=False
|
||||
)
|
||||
@@ -315,7 +285,7 @@ class Order(LockModel, LoggedModel):
|
||||
default=False,
|
||||
)
|
||||
|
||||
objects = ScopedManager(OrderQuerySet.as_manager().__class__, organizer='event__organizer')
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Order")
|
||||
@@ -881,9 +851,6 @@ class Order(LockModel, LoggedModel):
|
||||
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
|
||||
return False
|
||||
|
||||
if self.event.settings.allow_modifications not in ("order", "attendee"):
|
||||
return False
|
||||
|
||||
modify_deadline = self.modify_deadline
|
||||
if modify_deadline is not None and time_machine_now() > modify_deadline:
|
||||
return False
|
||||
@@ -1253,10 +1220,6 @@ class Order(LockModel, LoggedModel):
|
||||
_transactions_mark_order_clean(self.pk)
|
||||
return create
|
||||
|
||||
def tagged_secret(self, tag, secret_length=64):
|
||||
return salted_hmac(value=tag, key_salt=b"", algorithm="sha256",
|
||||
secret=self.internal_secret or self.secret).hexdigest()[:secret_length]
|
||||
|
||||
|
||||
def answerfile_name(instance, filename: str) -> str:
|
||||
secret = get_random_string(length=32, allowed_chars=string.ascii_letters + string.digits)
|
||||
@@ -2551,43 +2514,6 @@ class OrderPosition(AbstractPosition):
|
||||
reasons[b] = b
|
||||
return reasons
|
||||
|
||||
@property
|
||||
def can_modify_answers(self) -> bool:
|
||||
"""
|
||||
``True`` if the user can change the question answers / attendee names that are
|
||||
related to the position. This checks order status and modification deadlines. It also
|
||||
returns ``False`` if there are no questions that can be answered.
|
||||
"""
|
||||
from .checkin import Checkin
|
||||
|
||||
if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
|
||||
return False
|
||||
|
||||
if self.event.settings.allow_modifications != "attendee":
|
||||
return False
|
||||
|
||||
modify_deadline = self.order.modify_deadline
|
||||
if modify_deadline is not None and now() > modify_deadline:
|
||||
return False
|
||||
|
||||
positions = list(
|
||||
self.order.positions.all().annotate(
|
||||
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True))
|
||||
).select_related('item').prefetch_related('item__questions')
|
||||
)
|
||||
if not self.event.settings.allow_modifications_after_checkin:
|
||||
for cp in positions:
|
||||
if cp.has_checkin:
|
||||
return False
|
||||
|
||||
ask_names = self.event.settings.get('attendee_names_asked', as_type=bool)
|
||||
for cp in positions:
|
||||
if cp.pk == self.pk or cp.addon_to_id == self.pk:
|
||||
if (cp.item.ask_attendee_data and ask_names) or cp.item.questions.all():
|
||||
return True
|
||||
|
||||
return False # nothing there to modify
|
||||
|
||||
@classmethod
|
||||
def transform_cart_positions(cls, cp: List, order) -> list:
|
||||
from . import Voucher
|
||||
|
||||
@@ -370,11 +370,10 @@ class Voucher(LoggedModel):
|
||||
'redeemed': redeemed
|
||||
}
|
||||
)
|
||||
if data.get('min_usages') is not None:
|
||||
if data.get('max_usages', 1) < data.get('min_usages', 1):
|
||||
raise ValidationError(
|
||||
_('The maximum number of usages may not be lower than the minimum number of usages.'),
|
||||
)
|
||||
if data.get('max_usages', 1) < data.get('min_usages', 1):
|
||||
raise ValidationError(
|
||||
_('The maximum number of usages may not be lower than the minimum number of usages.'),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def clean_subevent(data, event):
|
||||
@@ -460,7 +459,7 @@ class Voucher(LoggedModel):
|
||||
new_quotas = set(
|
||||
Quota.objects.filter(
|
||||
pk__in=Quota.variations.through.objects.filter(
|
||||
itemvariation__item=item,
|
||||
itemvariation__item=old_instance.item,
|
||||
quota__subevent=data.get('subevent'),
|
||||
).values('quota_id')
|
||||
)
|
||||
|
||||
+84
-129
@@ -62,7 +62,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, PdfWriter
|
||||
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
|
||||
@@ -407,30 +408,6 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
"TIME_FORMAT"
|
||||
)
|
||||
}),
|
||||
("purchase_date", {
|
||||
"label": _("Purchase date"),
|
||||
"editor_sample": _("2017-05-31"),
|
||||
"evaluate": lambda op, order, ev: date_format(
|
||||
order.datetime.astimezone(ev.timezone),
|
||||
"SHORT_DATE_FORMAT"
|
||||
)
|
||||
}),
|
||||
("purchase_datetime", {
|
||||
"label": _("Purchase date and time"),
|
||||
"editor_sample": _("2017-05-31 19:00"),
|
||||
"evaluate": lambda op, order, ev: date_format(
|
||||
order.datetime.astimezone(ev.timezone),
|
||||
"SHORT_DATETIME_FORMAT"
|
||||
)
|
||||
}),
|
||||
("purchase_time", {
|
||||
"label": _("Purchase time"),
|
||||
"editor_sample": _("19:00"),
|
||||
"evaluate": lambda op, order, ev: date_format(
|
||||
order.datetime.astimezone(ev.timezone),
|
||||
"TIME_FORMAT"
|
||||
)
|
||||
}),
|
||||
("valid_from_date", {
|
||||
"label": _("Validity start date"),
|
||||
"editor_sample": _("2017-05-31"),
|
||||
@@ -1068,71 +1045,56 @@ class Renderer:
|
||||
canvas.showPage()
|
||||
|
||||
def render_background(self, buffer, title=_('Ticket')):
|
||||
buffer.seek(0)
|
||||
fg_pdf = PdfReader(buffer)
|
||||
|
||||
if settings.PDFTK:
|
||||
buffer.seek(0)
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
fg_filename = os.path.join(d, 'fg.pdf')
|
||||
bg_filename = os.path.join(d, 'bg.pdf')
|
||||
out_filename = os.path.join(d, 'out.pdf')
|
||||
|
||||
buffer.seek(0)
|
||||
with open(fg_filename, 'wb') as f:
|
||||
with open(os.path.join(d, 'back.pdf'), 'wb') as f:
|
||||
f.write(self.bg_bytes)
|
||||
with open(os.path.join(d, 'front.pdf'), 'wb') as f:
|
||||
f.write(buffer.read())
|
||||
# pdf_header is a string like "%pdf-X.X"
|
||||
if float(self.bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
|
||||
# To fix issues with pdftk and background-PDF using pdf-version greater
|
||||
# than foreground-PDF, we stamp front onto back instead.
|
||||
# Just changing PDF-version in fg.pdf to match the version of
|
||||
# bg.pdf as we do with pypdf, does not work with pdftk.
|
||||
#
|
||||
# Make sure that bg.pdf matches the number of pages of fg.pdf
|
||||
# note: self.bg_pdf is a PdfReader(), not a PdfWriter()
|
||||
fg_num_pages = fg_pdf.get_num_pages()
|
||||
bg_num_pages = self.bg_pdf.get_num_pages()
|
||||
bg_pdf_to_merge = PdfWriter()
|
||||
bg_pdf_to_merge.append(self.bg_pdf, pages=(0, min(bg_num_pages, fg_num_pages)))
|
||||
if fg_num_pages > bg_num_pages:
|
||||
# repeat last page in bg_pdf to match fg_pdf
|
||||
bg_pdf_to_merge.append(bg_pdf_to_merge, pages=[bg_num_pages - 1] * (fg_num_pages - bg_num_pages))
|
||||
|
||||
bg_pdf_to_merge.write(bg_filename)
|
||||
|
||||
pdftk_cmd = [
|
||||
settings.PDFTK,
|
||||
bg_filename,
|
||||
'multistamp',
|
||||
fg_filename
|
||||
]
|
||||
|
||||
else:
|
||||
with open(bg_filename, 'wb') as f:
|
||||
f.write(self.bg_bytes)
|
||||
pdftk_cmd = [
|
||||
settings.PDFTK,
|
||||
fg_filename,
|
||||
'multibackground',
|
||||
bg_filename
|
||||
]
|
||||
|
||||
pdftk_cmd.extend(('output', out_filename, 'compress'))
|
||||
subprocess.run(pdftk_cmd, check=True)
|
||||
with open(out_filename, 'rb') as f:
|
||||
subprocess.run([
|
||||
settings.PDFTK,
|
||||
os.path.join(d, 'front.pdf'),
|
||||
'multibackground',
|
||||
os.path.join(d, 'back.pdf'),
|
||||
'output',
|
||||
os.path.join(d, 'out.pdf'),
|
||||
'compress'
|
||||
], check=True)
|
||||
with open(os.path.join(d, 'out.pdf'), 'rb') as f:
|
||||
return BytesIO(f.read())
|
||||
else:
|
||||
buffer.seek(0)
|
||||
new_pdf = PdfReader(buffer)
|
||||
output = PdfWriter()
|
||||
|
||||
for i, page in enumerate(fg_pdf.pages):
|
||||
bg_page = self.bg_pdf.pages[i]
|
||||
if bg_page.rotation != 0:
|
||||
bg_page.transfer_rotation_to_content()
|
||||
page.merge_page(bg_page, over=False)
|
||||
output.add_page(page)
|
||||
|
||||
# pdf_header is a string like "%pdf-X.X"
|
||||
if float(self.bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
|
||||
output.pdf_header = self.bg_pdf.pdf_header
|
||||
for i, page in enumerate(new_pdf.pages):
|
||||
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
|
||||
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.add_metadata({
|
||||
'/Title': str(title),
|
||||
@@ -1144,61 +1106,54 @@ class Renderer:
|
||||
return outbuffer
|
||||
|
||||
|
||||
def merge_background(fg_pdf: PdfWriter, bg_pdf: PdfWriter, out_file, compress):
|
||||
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')
|
||||
|
||||
# pdf_header is a string like "%pdf-X.X"
|
||||
if float(bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
|
||||
# To fix issues with pdftk and background-PDF using pdf-version greater
|
||||
# than foreground-PDF, we stamp front onto back instead.
|
||||
# Just changing PDF-version in fg.pdf to match the version of
|
||||
# bg.pdf as we do with pypdf, does not work with pdftk.
|
||||
|
||||
# Make sure that bg.pdf matches the number of pages of fg.pdf
|
||||
fg_num_pages = fg_pdf.get_num_pages()
|
||||
bg_num_pages = bg_pdf.get_num_pages()
|
||||
if fg_num_pages > bg_num_pages:
|
||||
# repeat last page in bg_pdf to match fg_pdf
|
||||
bg_pdf.append(bg_pdf, pages=[bg_num_pages - 1] * (fg_num_pages - bg_num_pages))
|
||||
|
||||
bg_pdf.write(bg_filename)
|
||||
|
||||
pdftk_cmd = [
|
||||
settings.PDFTK,
|
||||
bg_filename,
|
||||
'multistamp',
|
||||
fg_filename,
|
||||
]
|
||||
else:
|
||||
pdftk_cmd = [
|
||||
settings.PDFTK,
|
||||
fg_filename,
|
||||
'multibackground',
|
||||
bg_filename
|
||||
]
|
||||
|
||||
pdftk_cmd.extend(('output', '-'))
|
||||
if compress:
|
||||
pdftk_cmd.append('compress')
|
||||
|
||||
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 = bg_pdf.pages[i]
|
||||
if bg_page.rotation != 0:
|
||||
bg_page.transfer_rotation_to_content()
|
||||
page.merge_page(bg_page, over=False)
|
||||
|
||||
# pdf_header is a string like "%pdf-X.X"
|
||||
if float(bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
|
||||
fg_pdf.pdf_header = bg_pdf.pdf_header
|
||||
|
||||
fg_pdf.write(out_file)
|
||||
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
|
||||
|
||||
@@ -213,8 +213,6 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
|
||||
voucher = Voucher(event=event)
|
||||
vouchers.append(voucher)
|
||||
|
||||
if not record.get("code"):
|
||||
raise ValidationError(_('A voucher cannot be created without a code.'))
|
||||
Voucher.clean_item_properties(
|
||||
record,
|
||||
event,
|
||||
|
||||
@@ -99,8 +99,8 @@ from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.base.services.tasks import ProfiledEventTask, ProfiledTask
|
||||
from pretix.base.signals import (
|
||||
order_approved, order_canceled, order_changed, order_denied, order_expired,
|
||||
order_fee_calculation, order_paid, order_placed, order_reactivated,
|
||||
order_split, order_valid_if_pending, periodic_task, validate_order,
|
||||
order_fee_calculation, order_paid, order_placed, order_split,
|
||||
order_valid_if_pending, periodic_task, validate_order,
|
||||
)
|
||||
from pretix.base.timemachine import time_machine_now, time_machine_now_assigned
|
||||
from pretix.celery_app import app
|
||||
@@ -254,7 +254,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
|
||||
else:
|
||||
raise OrderError(is_available)
|
||||
|
||||
order_reactivated.send(order.event, order=order)
|
||||
order_approved.send(order.event, order=order)
|
||||
if order.status == Order.STATUS_PAID:
|
||||
order_paid.send(order.event, order=order)
|
||||
|
||||
|
||||
@@ -337,40 +337,6 @@ def base_placeholders(sender, **kwargs):
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalTextPlaceholder(
|
||||
'url_info_change', ['position', 'event'], lambda position, event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.position.modify', kwargs={
|
||||
'order': position.order.code,
|
||||
'secret': position.web_secret,
|
||||
'position': position.positionid
|
||||
}
|
||||
), lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.position.modify', kwargs={
|
||||
'order': 'F8VVL',
|
||||
'secret': '6zzjnumtsx136ddy',
|
||||
'position': '123',
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalTextPlaceholder(
|
||||
'url_products_change', ['position', 'event'], lambda position, event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.position.change', kwargs={
|
||||
'order': position.order.code,
|
||||
'secret': position.web_secret,
|
||||
'position': position.positionid
|
||||
}
|
||||
), lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.position.change', kwargs={
|
||||
'order': 'F8VVL',
|
||||
'secret': '6zzjnumtsx136ddy',
|
||||
'position': '123'
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalTextPlaceholder(
|
||||
'order_modification_deadline_date_and_time', ['order', 'event'],
|
||||
lambda order, event:
|
||||
|
||||
@@ -62,10 +62,7 @@ class VATIDTemporaryError(VATIDError):
|
||||
|
||||
def _validate_vat_id_NO(vat_id, country_code):
|
||||
# Inspired by vat_moss library
|
||||
try:
|
||||
vat_id = vat_moss.id.normalize(vat_id)
|
||||
except ValueError:
|
||||
raise VATIDFinalError(error_messages['invalid'])
|
||||
vat_id = vat_moss.id.normalize(vat_id)
|
||||
|
||||
if not vat_id or len(vat_id) < 3 or not re.match('^\\d{9}MVA$', vat_id[2:]):
|
||||
raise VATIDFinalError(error_messages['invalid'])
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db import transaction
|
||||
@@ -50,28 +49,19 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
|
||||
|
||||
quota_cache = {}
|
||||
gone = set()
|
||||
_seats_available_cache = {}
|
||||
seats_used = defaultdict(int)
|
||||
seats_available = {}
|
||||
|
||||
seated_product_set = set(
|
||||
SeatCategoryMapping.objects.filter(event=event).values_list('product_id', 'subevent_id')
|
||||
)
|
||||
|
||||
def _seats_available(item, subevent):
|
||||
for m in SeatCategoryMapping.objects.filter(event=event).select_related('subevent'):
|
||||
# See comment in WaitingListEntry.send_voucher() for rationale
|
||||
subevent_id = subevent.pk if subevent else None
|
||||
if (item.pk, subevent_id) not in _seats_available_cache:
|
||||
num_free_seats_for_product = (subevent or event).free_seats().filter(product_id=item.pk).count()
|
||||
num_valid_vouchers_for_product = event.vouchers.filter(
|
||||
Q(valid_until__isnull=True) | Q(valid_until__gte=now()),
|
||||
block_quota=True,
|
||||
item_id=item.pk,
|
||||
subevent_id=subevent_id,
|
||||
waitinglistentries__isnull=False
|
||||
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
|
||||
_seats_available_cache[item.pk, subevent_id] = num_free_seats_for_product - num_valid_vouchers_for_product
|
||||
|
||||
return _seats_available_cache[item.pk, subevent_id] - seats_used[item.pk, subevent_id]
|
||||
num_free_seets_for_product = (m.subevent or event).free_seats().filter(product_id=m.product_id).count()
|
||||
num_valid_vouchers_for_product = event.vouchers.filter(
|
||||
Q(valid_until__isnull=True) | Q(valid_until__gte=now()),
|
||||
block_quota=True,
|
||||
item_id=m.product_id,
|
||||
subevent_id=m.subevent_id,
|
||||
waitinglistentries__isnull=False
|
||||
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
|
||||
seats_available[(m.product_id, m.subevent_id)] = num_free_seets_for_product - num_valid_vouchers_for_product
|
||||
|
||||
prefetch_related_objects(
|
||||
[event.organizer],
|
||||
@@ -113,7 +103,7 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
|
||||
|
||||
lock_objects(quotas, shared_lock_objects=[event])
|
||||
for wle in qs:
|
||||
if (wle.item_id, wle.variation_id, wle.subevent_id) in gone:
|
||||
if (wle.item, wle.variation, wle.subevent) in gone:
|
||||
continue
|
||||
ev = (wle.subevent or event)
|
||||
if not ev.presale_is_running or (wle.subevent and not wle.subevent.active):
|
||||
@@ -121,15 +111,15 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
|
||||
if wle.subevent and not wle.subevent.presale_is_running:
|
||||
continue
|
||||
if event.settings.waiting_list_auto_disable and event.settings.waiting_list_auto_disable.datetime(wle.subevent or event) <= now():
|
||||
gone.add((wle.item_id, wle.variation_id, wle.subevent_id))
|
||||
gone.add((wle.item, wle.variation, wle.subevent))
|
||||
continue
|
||||
if not wle.item.is_available():
|
||||
gone.add((wle.item_id, wle.variation_id, wle.subevent_id))
|
||||
gone.add((wle.item, wle.variation, wle.subevent))
|
||||
continue
|
||||
|
||||
if (wle.item_id, wle.subevent_id) in seated_product_set:
|
||||
if _seats_available(wle.item, wle.subevent) < 1:
|
||||
gone.add((wle.item_id, wle.variation_id, wle.subevent_id))
|
||||
if (wle.item_id, wle.subevent_id) in seats_available:
|
||||
if seats_available[wle.item_id, wle.subevent_id] < 1:
|
||||
gone.add((wle.item, wle.variation, wle.subevent))
|
||||
continue
|
||||
|
||||
availability = (
|
||||
@@ -151,10 +141,10 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
|
||||
quota_cache[q.pk][1] - 1 if quota_cache[q.pk][1] is not None else sys.maxsize
|
||||
)
|
||||
|
||||
if (wle.item_id, wle.subevent_id) in seated_product_set:
|
||||
seats_used[wle.item_id, wle.subevent_id] += 1
|
||||
if (wle.item_id, wle.subevent_id) in seats_available:
|
||||
seats_available[wle.item_id, wle.subevent_id] -= 1
|
||||
else:
|
||||
gone.add((wle.item_id, wle.variation_id, wle.subevent_id))
|
||||
gone.add((wle.item, wle.variation, wle.subevent))
|
||||
|
||||
return sent
|
||||
|
||||
|
||||
@@ -971,8 +971,7 @@ DEFAULTS = {
|
||||
},
|
||||
'payment_giftcard__enabled': {
|
||||
'default': 'True',
|
||||
'type': bool,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'type': bool
|
||||
},
|
||||
'payment_giftcard_public_name': {
|
||||
'default': LazyI18nString.from_gettext(gettext_noop('Gift card')),
|
||||
@@ -1654,28 +1653,6 @@ DEFAULTS = {
|
||||
"calendar.")
|
||||
)
|
||||
},
|
||||
'allow_modifications': {
|
||||
'default': 'order',
|
||||
'type': str,
|
||||
'form_class': forms.ChoiceField,
|
||||
'serializer_class': serializers.ChoiceField,
|
||||
'serializer_kwargs': dict(
|
||||
choices=(
|
||||
('no', _('No modifications after order was submitted')),
|
||||
('order', _('Only the person who ordered can make changes')),
|
||||
('attendee', _('Both the attendee and the person who ordered can make changes')),
|
||||
)
|
||||
),
|
||||
'form_kwargs': dict(
|
||||
label=_("Allow customers to modify their information"),
|
||||
widget=forms.RadioSelect,
|
||||
choices=(
|
||||
('no', _('No modifications after order was submitted')),
|
||||
('order', _('Only the person who ordered can make changes')),
|
||||
('attendee', _('Both the attendee and the person who ordered can make changes')),
|
||||
)
|
||||
),
|
||||
},
|
||||
'allow_modifications_after_checkin': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
@@ -1683,8 +1660,6 @@ DEFAULTS = {
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Allow customers to modify their information after they checked in."),
|
||||
help_text=_("By default, no more modifications are possible for an order as soon as one of the tickets "
|
||||
"in the order has been checked in.")
|
||||
)
|
||||
},
|
||||
'last_order_modification_date': {
|
||||
|
||||
@@ -81,6 +81,5 @@ def has_time_machine_permission(request, event):
|
||||
) or (
|
||||
getattr(request, 'event_access_user', None) and
|
||||
request.event_access_user.is_authenticated and
|
||||
request.event_access_user.has_event_permission(request.organizer, request.event, permission,
|
||||
session_key=request.event_access_parent_session_key)
|
||||
request.event_access_user.has_event_permission(request.organizer, request.event, permission, request=request)
|
||||
)
|
||||
|
||||
@@ -580,7 +580,6 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
|
||||
'banner_text',
|
||||
'banner_text_bottom',
|
||||
'order_email_asked_twice',
|
||||
'allow_modifications',
|
||||
'last_order_modification_date',
|
||||
'allow_modifications_after_checkin',
|
||||
'checkout_show_copy_answers_button',
|
||||
|
||||
@@ -698,14 +698,6 @@ class ItemUpdateForm(I18nModelForm):
|
||||
'tax_rule',
|
||||
_("Gift card products should use a tax rule with a rate of 0 percent since sales tax will be applied when the gift card is redeemed.")
|
||||
)
|
||||
if d.get('validity_mode'):
|
||||
self.add_error(
|
||||
'validity_mode',
|
||||
_(
|
||||
"Do not set a specific validity for gift card products as it will not restrict the validity "
|
||||
"of the gift card. A validity of gift cards can be set in your organizer settings."
|
||||
)
|
||||
)
|
||||
if d.get('admission'):
|
||||
self.add_error(
|
||||
'admission',
|
||||
|
||||
@@ -21,16 +21,82 @@
|
||||
#
|
||||
from bootstrap3.renderers import FieldRenderer, InlineFieldRenderer
|
||||
from bootstrap3.text import text_value
|
||||
from django.forms import CheckboxInput, CheckboxSelectMultiple, RadioSelect
|
||||
from django.forms.utils import flatatt
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import pgettext
|
||||
|
||||
from pretix.base.forms.renderers import PretixFieldRenderer
|
||||
from i18nfield.forms import I18nFormField
|
||||
|
||||
|
||||
class ControlFieldRenderer(PretixFieldRenderer):
|
||||
pass
|
||||
def render_label(content, label_for=None, label_class=None, label_title='', label_id='', optional=False):
|
||||
"""
|
||||
Render a label with content
|
||||
"""
|
||||
attrs = {}
|
||||
if label_for:
|
||||
attrs['for'] = label_for
|
||||
if label_class:
|
||||
attrs['class'] = label_class
|
||||
if label_title:
|
||||
attrs['title'] = label_title
|
||||
if label_id:
|
||||
attrs['id'] = label_id
|
||||
|
||||
if text_value(content) == ' ':
|
||||
# Empty label, e.g. checkbox
|
||||
attrs.setdefault('class', '')
|
||||
attrs['class'] += ' label-empty'
|
||||
|
||||
builder = '<{tag}{attrs}>{content}{opt}</{tag}>'
|
||||
return format_html(
|
||||
builder,
|
||||
tag='label',
|
||||
attrs=mark_safe(flatatt(attrs)) if attrs else '',
|
||||
opt=mark_safe('<br><span class="optional">{}</span>'.format(pgettext('form', 'Optional'))) if optional else '',
|
||||
content=text_value(content),
|
||||
)
|
||||
|
||||
|
||||
class ControlFieldRenderer(FieldRenderer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['layout'] = 'horizontal'
|
||||
super().__init__(*args, **kwargs)
|
||||
self.is_group_widget = isinstance(self.widget, (CheckboxSelectMultiple, RadioSelect, )) or (self.is_multi_widget and len(self.widget.widgets) > 1)
|
||||
|
||||
def add_label(self, html):
|
||||
label = self.get_label()
|
||||
|
||||
if hasattr(self.field.field, '_required'):
|
||||
# e.g. payment settings forms where a field is only required if the payment provider is active
|
||||
required = self.field.field._required
|
||||
elif isinstance(self.field.field, I18nFormField):
|
||||
required = self.field.field.one_required
|
||||
else:
|
||||
required = self.field.field.required
|
||||
|
||||
if self.is_group_widget:
|
||||
label_for = ""
|
||||
label_id = "legend-{}".format(self.field.html_name)
|
||||
else:
|
||||
label_for = self.field.id_for_label
|
||||
label_id = ""
|
||||
|
||||
html = render_label(
|
||||
label,
|
||||
label_for=label_for,
|
||||
label_class=self.get_label_class(),
|
||||
label_id=label_id,
|
||||
optional=not required and not isinstance(self.widget, CheckboxInput)
|
||||
) + html
|
||||
return html
|
||||
|
||||
def wrap_label_and_field(self, html):
|
||||
if self.is_group_widget:
|
||||
attrs = ' role="group" aria-labelledby="legend-{}"'.format(self.field.html_name)
|
||||
else:
|
||||
attrs = ''
|
||||
return '<div class="{klass}"{attrs}>{html}</div>'.format(klass=self.get_form_group_class(), html=html, attrs=attrs)
|
||||
|
||||
|
||||
class ControlFieldWithVisibilityRenderer(ControlFieldRenderer):
|
||||
|
||||
@@ -90,10 +90,8 @@ class VoucherForm(I18nModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial')
|
||||
self.initial_instance_data = None
|
||||
if instance:
|
||||
if instance.pk:
|
||||
self.initial_instance_data = modelcopy(instance)
|
||||
self.initial_instance_data = modelcopy(instance)
|
||||
try:
|
||||
if instance.variation:
|
||||
initial['itemvar'] = '%d-%d' % (instance.item.pk, instance.variation.pk)
|
||||
@@ -103,6 +101,8 @@ class VoucherForm(I18nModelForm):
|
||||
initial['itemvar'] = 'q-%d' % instance.quota.pk
|
||||
except Item.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
self.initial_instance_data = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if instance.event.has_subevents:
|
||||
@@ -234,8 +234,8 @@ class VoucherForm(I18nModelForm):
|
||||
)
|
||||
if check_quota:
|
||||
Voucher.clean_quota_check(
|
||||
data, cnt, self.initial_instance_data,
|
||||
self.instance.event, self.instance.quota, self.instance.item, self.instance.variation
|
||||
data, cnt, self.initial_instance_data, self.instance.event,
|
||||
self.instance.quota, self.instance.item, self.instance.variation
|
||||
)
|
||||
Voucher.clean_voucher_code(data, self.instance.event, self.instance.pk)
|
||||
if 'seat' in self.fields and data.get('seat'):
|
||||
@@ -373,7 +373,7 @@ class VoucherBulkForm(VoucherForm):
|
||||
res.append(self.Recipient(
|
||||
name=row.get('name', ''),
|
||||
email=row['email'].strip(),
|
||||
number=int(row.get('number', 1) or ""),
|
||||
number=int(row.get('number', 1)),
|
||||
tag=row.get('tag', None)
|
||||
))
|
||||
except ValueError as err:
|
||||
|
||||
@@ -444,11 +444,6 @@ def get_global_navigation(request):
|
||||
'url': reverse('control:global.license'),
|
||||
'active': (url.url_name == 'global.license'),
|
||||
},
|
||||
{
|
||||
'label': _('System report'),
|
||||
'url': reverse('control:global.sysreport'),
|
||||
'active': (url.url_name == 'global.sysreport'),
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@@ -1,307 +0,0 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import zoneinfo
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Count, Exists, F, Min, OuterRef, Q, Sum
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.timezone import now
|
||||
from reportlab.lib import pagesizes
|
||||
from reportlab.lib.enums import TA_CENTER
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.platypus import Paragraph, Spacer, Table, TableStyle
|
||||
|
||||
from pretix import __version__
|
||||
from pretix.base.models import Order, OrderPayment, Transaction
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.plugins.reports.exporters import ReportlabExportMixin
|
||||
from pretix.settings import DATA_DIR
|
||||
|
||||
|
||||
class SysReport(ReportlabExportMixin):
|
||||
@property
|
||||
def pagesize(self):
|
||||
return pagesizes.portrait(pagesizes.A4)
|
||||
|
||||
def __init__(self, start_month, tzname):
|
||||
self.tzname = tzname
|
||||
self.tz = zoneinfo.ZoneInfo(tzname)
|
||||
self.start_month = start_month
|
||||
|
||||
def page_header(self, canvas, doc):
|
||||
pass
|
||||
|
||||
def page_footer(self, canvas, doc):
|
||||
from reportlab.lib.units import mm
|
||||
|
||||
canvas.setFont("OpenSans", 8)
|
||||
canvas.drawString(15 * mm, 10 * mm, "Page %d" % doc.page)
|
||||
canvas.drawRightString(
|
||||
self.pagesize[0] - doc.rightMargin,
|
||||
10 * mm,
|
||||
"Created: %s"
|
||||
% date_format(now().astimezone(self.tz), "SHORT_DATETIME_FORMAT"),
|
||||
)
|
||||
|
||||
def render(self):
|
||||
return "sysreport.pdf", "application/pdf", self.create({})
|
||||
|
||||
def get_story(self, doc, form_data):
|
||||
headlinestyle = self.get_style()
|
||||
headlinestyle.fontSize = 15
|
||||
subheadlinestyle = self.get_style()
|
||||
subheadlinestyle.fontSize = 13
|
||||
style_small = self.get_style()
|
||||
style_small.fontSize = 6
|
||||
|
||||
story = [
|
||||
Paragraph("System report", headlinestyle),
|
||||
Spacer(1, 5 * mm),
|
||||
Paragraph("Usage", subheadlinestyle),
|
||||
Spacer(1, 5 * mm),
|
||||
self._usage_table(),
|
||||
Spacer(1, 5 * mm),
|
||||
Paragraph("Installed versions", subheadlinestyle),
|
||||
Spacer(1, 5 * mm),
|
||||
self._tech_table(),
|
||||
Spacer(1, 5 * mm),
|
||||
Paragraph("Plugins", subheadlinestyle),
|
||||
Spacer(1, 5 * mm),
|
||||
Paragraph(self._get_plugin_versions(), style_small),
|
||||
Spacer(1, 5 * mm),
|
||||
Paragraph("Custom templates", subheadlinestyle),
|
||||
Spacer(1, 5 * mm),
|
||||
Paragraph(self._get_custom_templates(), style_small),
|
||||
Spacer(1, 5 * mm),
|
||||
]
|
||||
|
||||
return story
|
||||
|
||||
def _tech_table(self):
|
||||
style = self.get_style()
|
||||
style.fontSize = 8
|
||||
style_small = self.get_style()
|
||||
style_small.fontSize = 6
|
||||
|
||||
w = self.pagesize[0] - 30 * mm
|
||||
colwidths = [
|
||||
a * w
|
||||
for a in (
|
||||
0.2,
|
||||
0.8,
|
||||
)
|
||||
]
|
||||
tstyledata = [
|
||||
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||
("LEFTPADDING", (0, 0), (0, -1), 0),
|
||||
("RIGHTPADDING", (-1, 0), (-1, -1), 0),
|
||||
]
|
||||
tdata = [
|
||||
[Paragraph("Site URL:", style), Paragraph(settings.SITE_URL, style)],
|
||||
[Paragraph("pretix version:", style), Paragraph(__version__, style)],
|
||||
[Paragraph("Python version:", style), Paragraph(sys.version, style)],
|
||||
[Paragraph("Platform:", style), Paragraph(platform.platform(), style)],
|
||||
[
|
||||
Paragraph("Database engine:", style),
|
||||
Paragraph(settings.DATABASES["default"]["ENGINE"], style),
|
||||
],
|
||||
]
|
||||
table = Table(tdata, colWidths=colwidths, repeatRows=0)
|
||||
table.setStyle(TableStyle(tstyledata))
|
||||
return table
|
||||
|
||||
def _usage_table(self):
|
||||
style = self.get_style()
|
||||
style.fontSize = 8
|
||||
style_small = self.get_style()
|
||||
style_small.fontSize = 6
|
||||
style_small.leading = 8
|
||||
style_small.alignment = TA_CENTER
|
||||
style_small_head = self.get_style()
|
||||
style_small_head.fontSize = 6
|
||||
style_small_head.leading = 8
|
||||
style_small_head.alignment = TA_CENTER
|
||||
style_small_head.fontName = "OpenSansBd"
|
||||
|
||||
w = self.pagesize[0] - 30 * mm
|
||||
|
||||
successful = (
|
||||
Q(status=Order.STATUS_PAID)
|
||||
| Q(valid_if_pending=True, status=Order.STATUS_PENDING)
|
||||
| Q(
|
||||
Exists(
|
||||
OrderPayment.objects.filter(
|
||||
order_id=OuterRef("pk"),
|
||||
state__in=(
|
||||
OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||||
OrderPayment.PAYMENT_STATE_REFUNDED,
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
orders_q = Order.objects.filter(
|
||||
successful,
|
||||
testmode=False,
|
||||
)
|
||||
orders_testmode_q = Order.objects.filter(
|
||||
testmode=True,
|
||||
)
|
||||
orders_unconfirmed_q = Order.objects.filter(
|
||||
~successful,
|
||||
testmode=False,
|
||||
)
|
||||
revenue_q = Transaction.objects.filter(
|
||||
Exists(
|
||||
OrderPayment.objects.filter(
|
||||
order_id=OuterRef("order_id"),
|
||||
state__in=(
|
||||
OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||||
OrderPayment.PAYMENT_STATE_REFUNDED,
|
||||
),
|
||||
)
|
||||
),
|
||||
order__testmode=False,
|
||||
)
|
||||
|
||||
currencies = sorted(
|
||||
list(
|
||||
set(
|
||||
Transaction.objects.annotate(c=F("order__event__currency"))
|
||||
.values_list("c", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
year_first = orders_q.aggregate(m=Min("datetime__year"))["m"]
|
||||
if not year_first:
|
||||
year_first = now().year
|
||||
elif datetime.now().month - 1 <= self.start_month:
|
||||
year_first -= 1
|
||||
year_last = now().year
|
||||
tdata = [
|
||||
[
|
||||
Paragraph(l, style_small_head)
|
||||
for l in (
|
||||
"Time frame",
|
||||
"Currency",
|
||||
"Successful orders",
|
||||
"Net revenue",
|
||||
"Testmode orders",
|
||||
"Unsucessful orders",
|
||||
"Positions",
|
||||
"Gross revenue",
|
||||
)
|
||||
]
|
||||
]
|
||||
|
||||
for year in range(year_first, year_last + 1):
|
||||
for i, c in enumerate(currencies):
|
||||
first_day = datetime(
|
||||
year, self.start_month, 1, 0, 0, 0, 0, tzinfo=self.tz
|
||||
)
|
||||
after_day = datetime(
|
||||
year + 1, self.start_month, 1, 0, 0, 0, 0, tzinfo=self.tz
|
||||
)
|
||||
|
||||
orders_count = (
|
||||
orders_q.filter(
|
||||
datetime__gte=first_day, datetime__lt=after_day
|
||||
).aggregate(c=Count("*"))["c"]
|
||||
or 0
|
||||
)
|
||||
testmode_count = (
|
||||
orders_testmode_q.filter(
|
||||
datetime__gte=first_day, datetime__lt=after_day
|
||||
).aggregate(c=Count("*"))["c"]
|
||||
or 0
|
||||
)
|
||||
unconfirmed_count = (
|
||||
orders_unconfirmed_q.filter(
|
||||
datetime__gte=first_day, datetime__lt=after_day
|
||||
).aggregate(c=Count("*"))["c"]
|
||||
or 0
|
||||
)
|
||||
revenue_data = revenue_q.filter(
|
||||
datetime__gte=first_day, datetime__lt=after_day, order__event__currency=c
|
||||
).aggregate(
|
||||
c=Sum("count"),
|
||||
s_net=Sum(F("price") - F("tax_value")),
|
||||
s_gross=Sum(F("price")),
|
||||
)
|
||||
|
||||
tdata.append(
|
||||
(
|
||||
Paragraph(
|
||||
date_format(first_day, "M Y")
|
||||
+ " – "
|
||||
+ date_format(after_day - timedelta(days=1), "M Y"),
|
||||
style_small,
|
||||
),
|
||||
Paragraph(c, style_small),
|
||||
Paragraph(str(orders_count), style_small) if i == 0 else "",
|
||||
Paragraph(money_filter(revenue_data.get("s_net") or 0, c), style_small),
|
||||
Paragraph(str(testmode_count), style_small) if i == 0 else "",
|
||||
Paragraph(str(unconfirmed_count), style_small) if i == 0 else "",
|
||||
Paragraph(str(revenue_data.get("c") or 0), style_small),
|
||||
Paragraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small),
|
||||
)
|
||||
)
|
||||
|
||||
colwidths = [a * w for a in (0.18,) + (0.82 / 7,) * 7]
|
||||
tstyledata = [
|
||||
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||
("LEFTPADDING", (0, 0), (0, -1), 0),
|
||||
("RIGHTPADDING", (-1, 0), (-1, -1), 0),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 0),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), 1),
|
||||
]
|
||||
table = Table(tdata, colWidths=colwidths, repeatRows=0)
|
||||
table.setStyle(TableStyle(tstyledata))
|
||||
return table
|
||||
|
||||
def _get_plugin_versions(self):
|
||||
lines = []
|
||||
for p in get_all_plugins():
|
||||
lines.append(f"{p.name} {p.version}")
|
||||
return ", ".join(lines)
|
||||
|
||||
def _get_custom_templates(self):
|
||||
lines = []
|
||||
for dirpath, dirnames, filenames in os.walk(
|
||||
os.path.join(DATA_DIR, "templates")
|
||||
):
|
||||
for f in filenames:
|
||||
lines.append(f"{dirpath}/{f}")
|
||||
|
||||
d = "<br/>".join(lines[:50])
|
||||
if len(lines) > 50:
|
||||
d += "<br/>..."
|
||||
if not d:
|
||||
return "–"
|
||||
return d
|
||||
@@ -327,7 +327,8 @@
|
||||
{% endblocktrans %}
|
||||
{% blocktrans trimmed %}
|
||||
Internet Explorer is an old browser that does not support lots of recent web-based
|
||||
technologies and is no longer supported by this website.
|
||||
technologies. While some features might already not work properly, we plan on no longer
|
||||
supporting Internet Explorer in our administrative backend in the next months.
|
||||
{% endblocktrans %}
|
||||
{% blocktrans trimmed %}
|
||||
We kindly ask you to move to one of our supported browsers, such as Microsoft Edge,
|
||||
|
||||
@@ -113,17 +113,10 @@
|
||||
</div>
|
||||
{% bootstrap_field sform.attendee_data_explanation_text layout="control" %}
|
||||
|
||||
<h4>{% trans "Form settings" %}</h4>
|
||||
<h4>{% trans "Other settings" %}</h4>
|
||||
{% bootstrap_field sform.name_scheme layout="control" %}
|
||||
{% bootstrap_field sform.name_scheme_titles layout="control" %}
|
||||
{% bootstrap_field sform.checkout_show_copy_answers_button layout="control" %}
|
||||
|
||||
<h4>{% trans "Changes to existing orders" %}</h4>
|
||||
{% bootstrap_field sform.allow_modifications layout="control" %}
|
||||
<div data-display-dependency='#id_settings-allow_modifications_0' data-inverse>
|
||||
{% bootstrap_field sform.last_order_modification_date layout="control" %}
|
||||
{% bootstrap_field sform.allow_modifications_after_checkin layout="control" %}
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Texts" %}</legend>
|
||||
@@ -232,47 +225,21 @@
|
||||
{% bootstrap_field sform.presale_start_show_date layout="control" %}
|
||||
{% bootstrap_field form.presale_end layout="control" %}
|
||||
{% bootstrap_field sform.show_items_outside_presale_period layout="control" %}
|
||||
{% bootstrap_field sform.last_order_modification_date layout="control" %}
|
||||
{% bootstrap_field sform.allow_modifications_after_checkin layout="control" %}
|
||||
{% bootstrap_field sform.show_checkin_number_user layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Display" %}</legend>
|
||||
<h4>{% trans "Date and time" %}</h4>
|
||||
{% bootstrap_field sform.show_dates_on_frontpage layout="control" %}
|
||||
{% bootstrap_field sform.show_date_to layout="control" %}
|
||||
{% bootstrap_field sform.show_times layout="control" %}
|
||||
<h4>{% trans "Product list" %}</h4>
|
||||
{% bootstrap_field sform.show_quota_left layout="control" %}
|
||||
{% bootstrap_field sform.display_net_prices layout="control" %}
|
||||
{% bootstrap_field sform.hide_prices_from_attendees layout="control" %}
|
||||
{% bootstrap_field sform.show_variations_expanded layout="control" %}
|
||||
{% bootstrap_field sform.hide_sold_out layout="control" %}
|
||||
|
||||
<h4>{% trans "Calendar and list views" context "subevents" %}</h4>
|
||||
{% if sform.frontpage_subevent_ordering %}
|
||||
{% bootstrap_field sform.frontpage_subevent_ordering layout="control" %}
|
||||
{% endif %}
|
||||
{% if sform.event_list_type %}
|
||||
{% bootstrap_field sform.event_list_type layout="control" %}
|
||||
{% endif %}
|
||||
{% if sform.event_list_available_only %}
|
||||
{% bootstrap_field sform.event_list_available_only layout="control" %}
|
||||
{% endif %}
|
||||
{% if sform.event_list_filters %}
|
||||
{% bootstrap_field sform.event_list_filters layout="control" %}
|
||||
{% endif %}
|
||||
{% if sform.event_calendar_future_only %}
|
||||
{% bootstrap_field sform.event_calendar_future_only layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field sform.low_availability_percentage layout="control" addon_after="%" %}
|
||||
|
||||
|
||||
<h4>{% trans "Order details" %}</h4>
|
||||
{% bootstrap_field sform.hide_prices_from_attendees layout="control" %}
|
||||
{% bootstrap_field sform.show_checkin_number_user layout="control" %}
|
||||
|
||||
<h4>{% trans "Other settings" %}</h4>
|
||||
{% url "control:organizer.edit" organizer=request.organizer.slug as org_url %}
|
||||
{% propagated request.event org_url "meta_noindex" %}
|
||||
{% bootstrap_field sform.meta_noindex layout="control" %}
|
||||
{% endpropagated %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Footer links" %}<br>
|
||||
@@ -339,6 +306,28 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if sform.frontpage_subevent_ordering %}
|
||||
{% bootstrap_field sform.frontpage_subevent_ordering layout="control" %}
|
||||
{% endif %}
|
||||
{% if sform.event_list_type %}
|
||||
{% bootstrap_field sform.event_list_type layout="control" %}
|
||||
{% endif %}
|
||||
{% if sform.event_list_available_only %}
|
||||
{% bootstrap_field sform.event_list_available_only layout="control" %}
|
||||
{% endif %}
|
||||
{% if sform.event_list_filters %}
|
||||
{% bootstrap_field sform.event_list_filters layout="control" %}
|
||||
{% endif %}
|
||||
{% if sform.event_calendar_future_only %}
|
||||
{% bootstrap_field sform.event_calendar_future_only layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field sform.low_availability_percentage layout="control" addon_after="%" %}
|
||||
|
||||
{% url "control:organizer.edit" organizer=request.organizer.slug as org_url %}
|
||||
{% propagated request.event org_url "meta_noindex" %}
|
||||
{% bootstrap_field sform.meta_noindex layout="control" %}
|
||||
{% endpropagated %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Cart" %}</legend>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
{% extends "pretixcontrol/global_settings_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block inner %}
|
||||
<p>
|
||||
{% trans "If you have a pretix Enterprise license, this report must be submitted to pretix support when your license renews. It may also be requested by pretix support to aid debugging of problems." %}
|
||||
{% trans "It serves two purposes: Collecting useful information that might help with debugging problems in your pretix installation, and verifying that your usage of pretix is in compliance with the Enterprise license you purchased." %}
|
||||
</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
<label>
|
||||
{% trans "First month of license term:" %}
|
||||
<select name="month" class="form-control">
|
||||
<option value="1">{% trans "January" %}</option>
|
||||
<option value="2">{% trans "February" %}</option>
|
||||
<option value="3">{% trans "March" %}</option>
|
||||
<option value="4">{% trans "April" %}</option>
|
||||
<option value="5">{% trans "May" %}</option>
|
||||
<option value="6">{% trans "June" %}</option>
|
||||
<option value="7">{% trans "July" %}</option>
|
||||
<option value="8">{% trans "August" %}</option>
|
||||
<option value="9">{% trans "September" %}</option>
|
||||
<option value="10">{% trans "October" %}</option>
|
||||
<option value="11">{% trans "November" %}</option>
|
||||
<option value="11">{% trans "December" %}</option>
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
{% trans "Generate report" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -436,7 +436,7 @@
|
||||
{% endif %}
|
||||
{% if line.used_membership %}
|
||||
<br /><span class="fa fa-id-card fa-fw" aria-hidden="true"></span>
|
||||
<a href="{% url "control:organizer.customer.membership.edit" organizer=request.organizer.slug customer=line.used_membership.customer.identifier id=line.used_membership.pk %}">
|
||||
<a href="{% url "control:organizer.customer.membership.edit" organizer=request.organizer.slug customer=order.customer.identifier id=line.used_membership.pk %}">
|
||||
{{ line.used_membership }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -192,7 +192,6 @@
|
||||
<input type="hidden" name="start-action" value="{{ start_form.cleaned_data.action }}">
|
||||
<input type="hidden" name="start-mode" value="{{ start_form.cleaned_data.mode }}">
|
||||
<input type="hidden" name="start-partial_amount" value="{{ partial_amount }}">
|
||||
<input type="hidden" name="last_known_refund_id" value="{{ last_known_refund_id }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="id_comment">{% trans "Refund reason" %}</label>
|
||||
|
||||
@@ -48,12 +48,6 @@
|
||||
class="form-control"
|
||||
id="id_url" readonly>
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default btn-clipboard" data-clipboard-target="#id_url">
|
||||
<i class="fa fa-clipboard" aria-hidden="true"></i>
|
||||
<span class="sr-only">
|
||||
{% trans "Copy" %}
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" title="{% trans "Create QR code" %}" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fa fa-qrcode" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
@@ -56,7 +56,6 @@ urlpatterns = [
|
||||
re_path(r'^global/settings/$', global_settings.GlobalSettingsView.as_view(), name='global.settings'),
|
||||
re_path(r'^global/update/$', global_settings.UpdateCheckView.as_view(), name='global.update'),
|
||||
re_path(r'^global/license/$', global_settings.LicenseCheckView.as_view(), name='global.license'),
|
||||
re_path(r'^global/sysreport/$', global_settings.SysReportView.as_view(), name='global.sysreport'),
|
||||
re_path(r'^global/message/$', global_settings.MessageView.as_view(), name='global.message'),
|
||||
re_path(r'^logdetail/$', global_settings.LogDetailView.as_view(), name='global.logdetail'),
|
||||
re_path(r'^logdetail/payment/$', global_settings.PaymentDetailView.as_view(), name='global.paymentdetail'),
|
||||
|
||||
@@ -73,7 +73,6 @@ from i18nfield.utils import I18nJSONEncoder
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.email import get_available_placeholders
|
||||
from pretix.base.forms import PlaceholderValidator
|
||||
from pretix.base.models import Event, LogEntry, Order, TaxRule, Voucher
|
||||
from pretix.base.models.event import EventMetaValue
|
||||
from pretix.base.services import tickets
|
||||
@@ -714,6 +713,11 @@ class MailSettingsSetup(EventPermissionRequiredMixin, MailSettingsSetupView):
|
||||
class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
# return the origin text if key is missing in dict
|
||||
class SafeDict(dict):
|
||||
def __missing__(self, key):
|
||||
return '{' + key + '}'
|
||||
|
||||
# create index-language mapping
|
||||
@cached_property
|
||||
def supported_locale(self):
|
||||
@@ -738,7 +742,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
_('This value will be replaced based on dynamic parameters.'),
|
||||
s
|
||||
)
|
||||
return ctx
|
||||
return self.SafeDict(ctx)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
preview_item = request.POST.get('item', '')
|
||||
@@ -754,21 +758,12 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
idx = matched.group('idx')
|
||||
if idx in self.supported_locale:
|
||||
with language(self.supported_locale[idx], self.request.event.settings.region):
|
||||
try:
|
||||
if k.startswith('mail_subject_'):
|
||||
msgs[self.supported_locale[idx]] = format_map(
|
||||
bleach.clean(v), self.placeholders(preview_item), raise_on_missing=True
|
||||
)
|
||||
else:
|
||||
msgs[self.supported_locale[idx]] = markdown_compile_email(
|
||||
format_map(v, self.placeholders(preview_item), raise_on_missing=True)
|
||||
)
|
||||
except ValueError:
|
||||
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
|
||||
PlaceholderValidator.error_message)
|
||||
except KeyError as e:
|
||||
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
|
||||
_('Invalid placeholder: {%(value)s}') % {'value': e.args[0]})
|
||||
if k.startswith('mail_subject_'):
|
||||
msgs[self.supported_locale[idx]] = format_map(bleach.clean(v), self.placeholders(preview_item))
|
||||
else:
|
||||
msgs[self.supported_locale[idx]] = markdown_compile_email(
|
||||
format_map(v, self.placeholders(preview_item))
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'item': preview_item,
|
||||
|
||||
@@ -32,16 +32,14 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
import importlib_metadata as metadata
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, reverse
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from django.views.generic import FormView, TemplateView
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import LogEntry, OrderPayment, OrderRefund
|
||||
from pretix.base.services.update_check import check_result_table, update_check
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
@@ -51,7 +49,6 @@ from pretix.control.forms.global_settings import (
|
||||
from pretix.control.permissions import (
|
||||
AdministratorPermissionRequiredMixin, StaffMemberRequiredMixin,
|
||||
)
|
||||
from pretix.control.sysreport import SysReport
|
||||
|
||||
|
||||
class GlobalSettingsView(AdministratorPermissionRequiredMixin, FormView):
|
||||
@@ -265,25 +262,3 @@ class LicenseCheckView(StaffMemberRequiredMixin, FormView):
|
||||
))
|
||||
|
||||
return res
|
||||
|
||||
|
||||
class SysReportView(AdministratorPermissionRequiredMixin, TemplateView):
|
||||
template_name = 'pretixcontrol/global_sysreport.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
with language("en"):
|
||||
try:
|
||||
month = int(request.POST.get("month"))
|
||||
except ValueError:
|
||||
return super().get(request, *args, **kwargs)
|
||||
if month < 1 or month > 12:
|
||||
return super().get(request, *args, **kwargs)
|
||||
name, mime, data = SysReport(month, settings.TIME_ZONE).render()
|
||||
resp = HttpResponse(data)
|
||||
resp['Content-Type'] = mime
|
||||
resp['Content-Disposition'] = 'inline; filename="{}"'.format(name)
|
||||
resp._csp_ignore = True
|
||||
return resp
|
||||
|
||||
@@ -1323,8 +1323,6 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
|
||||
def plugin_forms(self):
|
||||
forms = []
|
||||
for rec, resp in item_forms.send(sender=self.request.event, item=self.item, request=self.request):
|
||||
if not resp:
|
||||
continue
|
||||
if isinstance(resp, (list, tuple)):
|
||||
forms.extend(resp)
|
||||
else:
|
||||
|
||||
+245
-260
@@ -49,7 +49,7 @@ from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.core.files import File
|
||||
from django.db import transaction
|
||||
from django.db.models import (
|
||||
Count, Exists, F, IntegerField, Max, OuterRef, Prefetch, ProtectedError, Q,
|
||||
Count, Exists, F, IntegerField, OuterRef, Prefetch, ProtectedError, Q,
|
||||
QuerySet, Subquery, Sum,
|
||||
)
|
||||
from django.forms import formset_factory
|
||||
@@ -1104,9 +1104,249 @@ class OrderRefundView(OrderView):
|
||||
p.propose_refund = proposals.get(p, 0)
|
||||
|
||||
if 'perform' in self.request.POST:
|
||||
r = self.perform_refund(comment, full_refund, payments)
|
||||
if r:
|
||||
return r
|
||||
refund_selected = Decimal('0.00')
|
||||
refunds = []
|
||||
|
||||
is_valid = True
|
||||
manual_value = self.request.POST.get('refund-manual', '0') or '0'
|
||||
manual_value = formats.sanitize_separators(manual_value)
|
||||
try:
|
||||
manual_value = Decimal(manual_value)
|
||||
except (DecimalException, TypeError):
|
||||
messages.error(self.request, _('You entered an invalid number.'))
|
||||
is_valid = False
|
||||
else:
|
||||
refund_selected += manual_value
|
||||
if manual_value:
|
||||
refunds.append(OrderRefund(
|
||||
order=self.order,
|
||||
payment=None,
|
||||
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
||||
state=(
|
||||
OrderRefund.REFUND_STATE_DONE
|
||||
if self.request.POST.get('manual_state') == 'done'
|
||||
else OrderRefund.REFUND_STATE_CREATED
|
||||
),
|
||||
execution_date=(
|
||||
now()
|
||||
if self.request.POST.get('manual_state') == 'done'
|
||||
else None
|
||||
),
|
||||
amount=manual_value,
|
||||
comment=comment,
|
||||
provider='manual'
|
||||
))
|
||||
|
||||
giftcard_value = self.request.POST.get('refund-new-giftcard', '0') or '0'
|
||||
giftcard_value = formats.sanitize_separators(giftcard_value)
|
||||
try:
|
||||
giftcard_value = Decimal(giftcard_value)
|
||||
except (DecimalException, TypeError):
|
||||
messages.error(self.request, _('You entered an invalid number.'))
|
||||
is_valid = False
|
||||
else:
|
||||
if giftcard_value:
|
||||
refund_selected += giftcard_value
|
||||
|
||||
if self.request.POST.get('giftcard-expires'):
|
||||
try:
|
||||
expires = forms.DateField().to_python(self.request.POST.get('giftcard-expires'))
|
||||
expires = make_aware(datetime.combine(
|
||||
expires,
|
||||
time(hour=23, minute=59, second=59)
|
||||
), self.request.event.timezone)
|
||||
except ValidationError as e:
|
||||
messages.error(self.request, e.message)
|
||||
is_valid = False
|
||||
else:
|
||||
expires = None
|
||||
|
||||
giftcard = self.request.organizer.issued_gift_cards.create(
|
||||
expires=expires,
|
||||
currency=self.request.event.currency,
|
||||
testmode=self.order.testmode
|
||||
)
|
||||
giftcard.log_action('pretix.giftcards.created', user=self.request.user, data={})
|
||||
refunds.append(OrderRefund(
|
||||
order=self.order,
|
||||
payment=None,
|
||||
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
||||
state=OrderRefund.REFUND_STATE_CREATED,
|
||||
execution_date=now(),
|
||||
amount=giftcard_value,
|
||||
provider='giftcard',
|
||||
comment=comment,
|
||||
info=json.dumps({
|
||||
'gift_card': giftcard.pk
|
||||
})
|
||||
))
|
||||
|
||||
offsetting_value = self.request.POST.get('refund-offsetting', '0') or '0'
|
||||
offsetting_value = formats.sanitize_separators(offsetting_value)
|
||||
try:
|
||||
offsetting_value = Decimal(offsetting_value)
|
||||
except (DecimalException, TypeError):
|
||||
messages.error(self.request, _('You entered an invalid number.'))
|
||||
is_valid = False
|
||||
else:
|
||||
if offsetting_value:
|
||||
refund_selected += offsetting_value
|
||||
try:
|
||||
order = Order.objects.get(code=self.request.POST.get('order-offsetting'),
|
||||
event__organizer=self.request.organizer)
|
||||
except Order.DoesNotExist:
|
||||
messages.error(self.request, _('You entered an order that could not be found.'))
|
||||
is_valid = False
|
||||
else:
|
||||
if order.event.currency != self.request.event.currency:
|
||||
messages.error(self.request, _('You entered an order in an event with a different currency.'))
|
||||
is_valid = False
|
||||
refunds.append(OrderRefund(
|
||||
order=self.order,
|
||||
payment=None,
|
||||
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
||||
state=OrderRefund.REFUND_STATE_DONE,
|
||||
execution_date=now(),
|
||||
amount=offsetting_value,
|
||||
provider='offsetting',
|
||||
comment=comment,
|
||||
info=json.dumps({
|
||||
'orders': [order.code]
|
||||
})
|
||||
))
|
||||
|
||||
for identifier, prov in self.request.event.get_payment_providers().items():
|
||||
prof_value = self.request.POST.get(f'newrefund-{identifier}', '0') or '0'
|
||||
prof_value = formats.sanitize_separators(prof_value)
|
||||
try:
|
||||
prof_value = Decimal(prof_value)
|
||||
except (DecimalException, TypeError):
|
||||
messages.error(self.request, _('You entered an invalid number.'))
|
||||
is_valid = False
|
||||
continue
|
||||
if prof_value > Decimal('0.00'):
|
||||
try:
|
||||
refund = prov.new_refund_control_form_process(self.request, prof_value, self.order)
|
||||
except ValidationError as e:
|
||||
for err in e:
|
||||
messages.error(self.request, err)
|
||||
is_valid = False
|
||||
continue
|
||||
if refund:
|
||||
refund_selected += refund.amount
|
||||
refund.comment = comment
|
||||
refund.source = OrderRefund.REFUND_SOURCE_ADMIN
|
||||
refunds.append(refund)
|
||||
|
||||
for p in payments:
|
||||
value = self.request.POST.get('refund-{}'.format(p.pk), '0') or '0'
|
||||
value = formats.sanitize_separators(value)
|
||||
try:
|
||||
value = Decimal(value)
|
||||
except (DecimalException, TypeError):
|
||||
messages.error(self.request, _('You entered an invalid number.'))
|
||||
is_valid = False
|
||||
else:
|
||||
if value == 0:
|
||||
continue
|
||||
elif value > p.available_amount:
|
||||
messages.error(self.request, _('You can not refund more than the amount of a '
|
||||
'payment that is not yet refunded.'))
|
||||
is_valid = False
|
||||
break
|
||||
elif value != p.amount and not p.partial_refund_possible:
|
||||
messages.error(self.request, _('You selected a partial refund for a payment method that '
|
||||
'only supports full refunds.'))
|
||||
is_valid = False
|
||||
break
|
||||
elif (p.partial_refund_possible or p.full_refund_possible) and value > 0:
|
||||
refund_selected += value
|
||||
refunds.append(OrderRefund(
|
||||
order=self.order,
|
||||
payment=p,
|
||||
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
||||
state=OrderRefund.REFUND_STATE_CREATED,
|
||||
amount=value,
|
||||
comment=comment,
|
||||
provider=p.provider
|
||||
))
|
||||
|
||||
any_success = False
|
||||
if refund_selected == full_refund and is_valid:
|
||||
for r in refunds:
|
||||
r.save()
|
||||
self.order.log_action('pretix.event.order.refund.created', {
|
||||
'local_id': r.local_id,
|
||||
'provider': r.provider,
|
||||
}, user=self.request.user)
|
||||
if r.provider != "manual":
|
||||
try:
|
||||
r.payment_provider.execute_refund(r)
|
||||
except PaymentException as e:
|
||||
r.state = OrderRefund.REFUND_STATE_FAILED
|
||||
r.save()
|
||||
messages.error(self.request, _('One of the refunds failed to be processed. You should '
|
||||
'retry to refund in a different way. The error message '
|
||||
'was: {}').format(str(e)))
|
||||
else:
|
||||
any_success = True
|
||||
if r.state == OrderRefund.REFUND_STATE_DONE:
|
||||
messages.success(self.request, _('A refund of {} has been processed.').format(
|
||||
money_filter(r.amount, self.request.event.currency)
|
||||
))
|
||||
elif r.state == OrderRefund.REFUND_STATE_CREATED:
|
||||
messages.info(self.request, _('A refund of {} has been saved, but not yet '
|
||||
'fully executed. You can mark it as complete '
|
||||
'below.').format(
|
||||
money_filter(r.amount, self.request.event.currency)
|
||||
))
|
||||
else:
|
||||
any_success = True
|
||||
|
||||
if r.state == OrderRefund.REFUND_STATE_DONE:
|
||||
self.order.log_action('pretix.event.order.refund.done', {
|
||||
'local_id': r.local_id,
|
||||
'provider': r.provider,
|
||||
}, user=self.request.user)
|
||||
|
||||
if any_success:
|
||||
if self.start_form.cleaned_data.get('action') == 'mark_refunded':
|
||||
if self.order.cancel_allowed():
|
||||
mark_order_refunded(self.order, user=self.request.user)
|
||||
elif self.start_form.cleaned_data.get('action') == 'mark_pending':
|
||||
if not (self.order.status == Order.STATUS_PAID and self.order.pending_sum <= 0):
|
||||
self.order.status = Order.STATUS_PENDING
|
||||
self.order.set_expires(
|
||||
now(),
|
||||
self.order.event.subevents.filter(
|
||||
id__in=self.order.positions.values_list('subevent_id', flat=True))
|
||||
)
|
||||
self.order.save(update_fields=['status', 'expires'])
|
||||
|
||||
if giftcard_value and self.order.email:
|
||||
messages.success(self.request, _('A new gift card was created. You can now send the user their '
|
||||
'gift card code.'))
|
||||
with language(self.order.locale, self.request.event.settings.region):
|
||||
return redirect(reverse('control:event.order.sendmail', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'code': self.order.code
|
||||
}) + '?' + urlencode({
|
||||
'subject': gettext('Your gift card code'),
|
||||
'message': gettext(
|
||||
'Hello,\n\nwe have refunded you {amount} for your order.\n\nYou can use the gift '
|
||||
'card code {giftcard} to pay for future ticket purchases in our shop.\n\n'
|
||||
'Your {event} team'
|
||||
).format(
|
||||
event="{event}",
|
||||
amount=money_filter(giftcard_value, self.request.event.currency),
|
||||
giftcard=giftcard.secret,
|
||||
)
|
||||
}))
|
||||
return redirect(self.get_order_url())
|
||||
else:
|
||||
messages.error(self.request, _('The refunds you selected do not match the selected total refund '
|
||||
'amount.'))
|
||||
|
||||
new_refunds = []
|
||||
for identifier, prov in self.request.event.get_payment_providers().items():
|
||||
@@ -1136,264 +1376,9 @@ class OrderRefundView(OrderView):
|
||||
self.request.POST.get('start-partial_amount') if self.request.method == 'POST'
|
||||
else self.request.GET.get('start-partial_amount')
|
||||
),
|
||||
'start_form': self.start_form,
|
||||
'last_known_refund_id': self.order.refunds.aggregate(m=Max("id"))["m"] or 0,
|
||||
'start_form': self.start_form
|
||||
})
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_refund(self, comment, full_refund, payments):
|
||||
order = Order.objects.select_for_update(of=OF_SELF).get(pk=self.order.pk)
|
||||
|
||||
if self.request.POST.get("last_known_refund_id", "0") != str(self.order.refunds.aggregate(m=Max("id"))["m"] or 0):
|
||||
messages.error(self.request, _('The refund was prevented due to a refund already being processed at the '
|
||||
'same time. Please have a look at the order details and check if your '
|
||||
'refund is still necessary.'))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
refund_selected = Decimal('0.00')
|
||||
refunds = []
|
||||
|
||||
is_valid = True
|
||||
manual_value = self.request.POST.get('refund-manual', '0') or '0'
|
||||
manual_value = formats.sanitize_separators(manual_value)
|
||||
try:
|
||||
manual_value = Decimal(manual_value)
|
||||
except (DecimalException, TypeError):
|
||||
messages.error(self.request, _('You entered an invalid number.'))
|
||||
is_valid = False
|
||||
else:
|
||||
refund_selected += manual_value
|
||||
if manual_value:
|
||||
refunds.append(OrderRefund(
|
||||
order=order,
|
||||
payment=None,
|
||||
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
||||
state=(
|
||||
OrderRefund.REFUND_STATE_DONE
|
||||
if self.request.POST.get('manual_state') == 'done'
|
||||
else OrderRefund.REFUND_STATE_CREATED
|
||||
),
|
||||
execution_date=(
|
||||
now()
|
||||
if self.request.POST.get('manual_state') == 'done'
|
||||
else None
|
||||
),
|
||||
amount=manual_value,
|
||||
comment=comment,
|
||||
provider='manual'
|
||||
))
|
||||
|
||||
giftcard_value = self.request.POST.get('refund-new-giftcard', '0') or '0'
|
||||
giftcard_value = formats.sanitize_separators(giftcard_value)
|
||||
try:
|
||||
giftcard_value = Decimal(giftcard_value)
|
||||
except (DecimalException, TypeError):
|
||||
messages.error(self.request, _('You entered an invalid number.'))
|
||||
is_valid = False
|
||||
else:
|
||||
if giftcard_value:
|
||||
refund_selected += giftcard_value
|
||||
|
||||
if self.request.POST.get('giftcard-expires'):
|
||||
try:
|
||||
expires = forms.DateField().to_python(self.request.POST.get('giftcard-expires'))
|
||||
expires = make_aware(datetime.combine(
|
||||
expires,
|
||||
time(hour=23, minute=59, second=59)
|
||||
), self.request.event.timezone)
|
||||
except ValidationError as e:
|
||||
messages.error(self.request, e.message)
|
||||
is_valid = False
|
||||
else:
|
||||
expires = None
|
||||
|
||||
giftcard = self.request.organizer.issued_gift_cards.create(
|
||||
expires=expires,
|
||||
currency=self.request.event.currency,
|
||||
testmode=order.testmode
|
||||
)
|
||||
giftcard.log_action('pretix.giftcards.created', user=self.request.user, data={})
|
||||
refunds.append(OrderRefund(
|
||||
order=order,
|
||||
payment=None,
|
||||
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
||||
state=OrderRefund.REFUND_STATE_CREATED,
|
||||
execution_date=now(),
|
||||
amount=giftcard_value,
|
||||
provider='giftcard',
|
||||
comment=comment,
|
||||
info=json.dumps({
|
||||
'gift_card': giftcard.pk
|
||||
})
|
||||
))
|
||||
|
||||
offsetting_value = self.request.POST.get('refund-offsetting', '0') or '0'
|
||||
offsetting_value = formats.sanitize_separators(offsetting_value)
|
||||
try:
|
||||
offsetting_value = Decimal(offsetting_value)
|
||||
except (DecimalException, TypeError):
|
||||
messages.error(self.request, _('You entered an invalid number.'))
|
||||
is_valid = False
|
||||
else:
|
||||
if offsetting_value:
|
||||
refund_selected += offsetting_value
|
||||
try:
|
||||
offset_order = Order.objects.get(code=self.request.POST.get('order-offsetting'),
|
||||
event__organizer=self.request.organizer)
|
||||
except Order.DoesNotExist:
|
||||
messages.error(self.request, _('You entered an order that could not be found.'))
|
||||
is_valid = False
|
||||
else:
|
||||
if offset_order.event.currency != self.request.event.currency:
|
||||
messages.error(self.request, _('You entered an order in an event with a different currency.'))
|
||||
is_valid = False
|
||||
refunds.append(OrderRefund(
|
||||
order=order,
|
||||
payment=None,
|
||||
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
||||
state=OrderRefund.REFUND_STATE_DONE,
|
||||
execution_date=now(),
|
||||
amount=offsetting_value,
|
||||
provider='offsetting',
|
||||
comment=comment,
|
||||
info=json.dumps({
|
||||
'orders': [offset_order.code]
|
||||
})
|
||||
))
|
||||
|
||||
for identifier, prov in self.request.event.get_payment_providers().items():
|
||||
prof_value = self.request.POST.get(f'newrefund-{identifier}', '0') or '0'
|
||||
prof_value = formats.sanitize_separators(prof_value)
|
||||
try:
|
||||
prof_value = Decimal(prof_value)
|
||||
except (DecimalException, TypeError):
|
||||
messages.error(self.request, _('You entered an invalid number.'))
|
||||
is_valid = False
|
||||
continue
|
||||
if prof_value > Decimal('0.00'):
|
||||
try:
|
||||
refund = prov.new_refund_control_form_process(self.request, prof_value, order)
|
||||
except ValidationError as e:
|
||||
for err in e:
|
||||
messages.error(self.request, err)
|
||||
is_valid = False
|
||||
continue
|
||||
if refund:
|
||||
refund_selected += refund.amount
|
||||
refund.comment = comment
|
||||
refund.source = OrderRefund.REFUND_SOURCE_ADMIN
|
||||
refunds.append(refund)
|
||||
|
||||
for p in payments:
|
||||
value = self.request.POST.get('refund-{}'.format(p.pk), '0') or '0'
|
||||
value = formats.sanitize_separators(value)
|
||||
try:
|
||||
value = Decimal(value)
|
||||
except (DecimalException, TypeError):
|
||||
messages.error(self.request, _('You entered an invalid number.'))
|
||||
is_valid = False
|
||||
else:
|
||||
if value == 0:
|
||||
continue
|
||||
elif value > p.available_amount:
|
||||
messages.error(self.request, _('You can not refund more than the amount of a '
|
||||
'payment that is not yet refunded.'))
|
||||
is_valid = False
|
||||
break
|
||||
elif value != p.amount and not p.partial_refund_possible:
|
||||
messages.error(self.request, _('You selected a partial refund for a payment method that '
|
||||
'only supports full refunds.'))
|
||||
is_valid = False
|
||||
break
|
||||
elif (p.partial_refund_possible or p.full_refund_possible) and value > 0:
|
||||
refund_selected += value
|
||||
refunds.append(OrderRefund(
|
||||
order=order,
|
||||
payment=p,
|
||||
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
||||
state=OrderRefund.REFUND_STATE_CREATED,
|
||||
amount=value,
|
||||
comment=comment,
|
||||
provider=p.provider
|
||||
))
|
||||
|
||||
any_success = False
|
||||
if refund_selected == full_refund and is_valid:
|
||||
for r in refunds:
|
||||
r.save()
|
||||
order.log_action('pretix.event.order.refund.created', {
|
||||
'local_id': r.local_id,
|
||||
'provider': r.provider,
|
||||
}, user=self.request.user)
|
||||
if r.provider != "manual":
|
||||
try:
|
||||
r.payment_provider.execute_refund(r)
|
||||
except PaymentException as e:
|
||||
r.state = OrderRefund.REFUND_STATE_FAILED
|
||||
r.save()
|
||||
messages.error(self.request, _('One of the refunds failed to be processed. You should '
|
||||
'retry to refund in a different way. The error message '
|
||||
'was: {}').format(str(e)))
|
||||
else:
|
||||
any_success = True
|
||||
if r.state == OrderRefund.REFUND_STATE_DONE:
|
||||
messages.success(self.request, _('A refund of {} has been processed.').format(
|
||||
money_filter(r.amount, self.request.event.currency)
|
||||
))
|
||||
elif r.state == OrderRefund.REFUND_STATE_CREATED:
|
||||
messages.info(self.request, _('A refund of {} has been saved, but not yet '
|
||||
'fully executed. You can mark it as complete '
|
||||
'below.').format(
|
||||
money_filter(r.amount, self.request.event.currency)
|
||||
))
|
||||
else:
|
||||
any_success = True
|
||||
|
||||
if r.state == OrderRefund.REFUND_STATE_DONE:
|
||||
order.log_action('pretix.event.order.refund.done', {
|
||||
'local_id': r.local_id,
|
||||
'provider': r.provider,
|
||||
}, user=self.request.user)
|
||||
|
||||
if any_success:
|
||||
if self.start_form.cleaned_data.get('action') == 'mark_refunded':
|
||||
if order.cancel_allowed():
|
||||
mark_order_refunded(order, user=self.request.user)
|
||||
elif self.start_form.cleaned_data.get('action') == 'mark_pending':
|
||||
if not (order.status == Order.STATUS_PAID and self.order.pending_sum <= 0):
|
||||
order.status = Order.STATUS_PENDING
|
||||
order.set_expires(
|
||||
now(),
|
||||
order.event.subevents.filter(
|
||||
id__in=order.positions.values_list('subevent_id', flat=True))
|
||||
)
|
||||
order.save(update_fields=['status', 'expires'])
|
||||
|
||||
if giftcard_value and order.email:
|
||||
messages.success(self.request, _('A new gift card was created. You can now send the user their '
|
||||
'gift card code.'))
|
||||
with language(order.locale, self.request.event.settings.region):
|
||||
return redirect(reverse('control:event.order.sendmail', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'code': order.code
|
||||
}) + '?' + urlencode({
|
||||
'subject': gettext('Your gift card code'),
|
||||
'message': gettext(
|
||||
'Hello,\n\nwe have refunded you {amount} for your order.\n\nYou can use the gift '
|
||||
'card code {giftcard} to pay for future ticket purchases in our shop.\n\n'
|
||||
'Your {event} team'
|
||||
).format(
|
||||
event="{event}",
|
||||
amount=money_filter(giftcard_value, self.request.event.currency),
|
||||
giftcard=giftcard.secret,
|
||||
)
|
||||
}))
|
||||
return redirect(self.get_order_url())
|
||||
else:
|
||||
messages.error(self.request, _('The refunds you selected do not match the selected total refund '
|
||||
'amount.'))
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
if self.start_form.is_valid():
|
||||
return self.choose_form()
|
||||
|
||||
@@ -30,15 +30,17 @@ class SafeFormatter(Formatter):
|
||||
Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and
|
||||
(b) does not allow any unwanted shenanigans like attribute access or format specifiers.
|
||||
"""
|
||||
def __init__(self, context, raise_on_missing=False):
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
self.raise_on_missing = raise_on_missing
|
||||
|
||||
def get_field(self, field_name, args, kwargs):
|
||||
return self.get_value(field_name, args, kwargs), field_name
|
||||
if '.' in field_name or '[' in field_name:
|
||||
logger.warning(f'Ignored invalid field name "{field_name}"')
|
||||
return ('{' + str(field_name) + '}', field_name)
|
||||
return super().get_field(field_name, args, kwargs)
|
||||
|
||||
def get_value(self, key, args, kwargs):
|
||||
if not self.raise_on_missing and key not in self.context:
|
||||
if key not in self.context:
|
||||
return '{' + str(key) + '}'
|
||||
return self.context[key]
|
||||
|
||||
@@ -47,7 +49,7 @@ class SafeFormatter(Formatter):
|
||||
return super().format_field(value, '')
|
||||
|
||||
|
||||
def format_map(template, context, raise_on_missing=False):
|
||||
def format_map(template, context):
|
||||
if not isinstance(template, str):
|
||||
template = str(template)
|
||||
return SafeFormatter(context, raise_on_missing).format(template)
|
||||
return SafeFormatter(context).format(template)
|
||||
|
||||
@@ -72,13 +72,9 @@ def remove_invalid_excel_chars(val):
|
||||
return val
|
||||
|
||||
|
||||
def SafeCell(worksheet, row=None, column=None, value=None, **kwargs):
|
||||
def SafeCell(*args, value=None, **kwargs):
|
||||
value = remove_invalid_excel_chars(value)
|
||||
if not column:
|
||||
column = 1
|
||||
if not row:
|
||||
row = 1
|
||||
c = Cell(worksheet, row=row, column=column, value=value, **kwargs)
|
||||
c = Cell(*args, value=value, **kwargs)
|
||||
if c.data_type == TYPE_FORMULA:
|
||||
c.data_type = TYPE_STRING
|
||||
return c
|
||||
|
||||
@@ -180,11 +180,7 @@ def create_thumbnail(source, size, formats=None):
|
||||
except:
|
||||
raise ThumbnailError('Could not load image')
|
||||
|
||||
frames = []
|
||||
durations = []
|
||||
for f in ImageSequence.Iterator(image):
|
||||
durations.append(f.info.get("duration", 1000))
|
||||
frames.append(resize_image(f, size))
|
||||
frames = [resize_image(frame, size) for frame in ImageSequence.Iterator(image)]
|
||||
image_out = frames[0]
|
||||
save_kwargs = {}
|
||||
source_ext = os.path.splitext(source_name)[1].lower()
|
||||
@@ -202,8 +198,6 @@ def create_thumbnail(source, size, formats=None):
|
||||
'loop': image.info.get('loop', 0),
|
||||
'save_all': True,
|
||||
}
|
||||
if len(frames) > 1 and 'duration' in image.info:
|
||||
save_kwargs['duration'] = durations
|
||||
else:
|
||||
target_ext = 'png'
|
||||
quality = None
|
||||
|
||||
+1856
-1997
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-08 13:19+0000\n"
|
||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -627,23 +627,23 @@ msgstr ""
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+1882
-2049
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-08 13:19+0000\n"
|
||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
||||
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
|
||||
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
|
||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -657,23 +657,23 @@ msgstr "لا شيء"
|
||||
msgid "Selected only"
|
||||
msgstr "المختارة فقط"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr "قم باستخدم اسم مختلف داخليا"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr "اضغط لاغلاق الصفحة"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "لم تقم بحفظ التعديلات!"
|
||||
|
||||
|
||||
+1858
-2036
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-08 13:19+0000\n"
|
||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
||||
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
|
||||
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
|
||||
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
@@ -628,23 +628,23 @@ msgstr ""
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+1858
-2037
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-08 13:19+0000\n"
|
||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
||||
"PO-Revision-Date: 2023-09-15 06:00+0000\n"
|
||||
"Last-Translator: Michael <michael.happl@gmx.at>\n"
|
||||
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -651,23 +651,23 @@ msgstr "Žádný"
|
||||
msgid "Selected only"
|
||||
msgstr "Pouze vybrané"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Interně používat jiný název"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr "Kliknutím zavřete"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Máte neuložené změny!"
|
||||
|
||||
|
||||
+1856
-2005
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-08 13:19+0000\n"
|
||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -628,23 +628,23 @@ msgstr ""
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+2405
-2083
File diff suppressed because it is too large
Load Diff
@@ -6,9 +6,9 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-08 13:19+0000\n"
|
||||
"PO-Revision-Date: 2024-05-30 17:00+0000\n"
|
||||
"Last-Translator: Nikolai <nikolai@lengefeldt.de>\n"
|
||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
||||
"PO-Revision-Date: 2022-12-01 17:00+0000\n"
|
||||
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
|
||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
"da/>\n"
|
||||
"Language: da\n"
|
||||
@@ -16,7 +16,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 5.5.5\n"
|
||||
"X-Generator: Weblate 4.14.2\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -671,23 +671,23 @@ msgstr "Ingen"
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr "Klik for at lukke"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Du har ændringer, der ikke er gemt!"
|
||||
|
||||
@@ -724,6 +724,10 @@ msgid "Cart expired"
|
||||
msgstr "Kurv udløbet"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:50
|
||||
#, fuzzy
|
||||
#| msgid "The items in your cart are reserved for you for one minute."
|
||||
#| msgid_plural ""
|
||||
#| "The items in your cart are reserved for you for {num} minutes."
|
||||
msgid "The items in your cart are reserved for you for one minute."
|
||||
msgid_plural "The items in your cart are reserved for you for {num} minutes."
|
||||
msgstr[0] "Varerne i din kurv er reserveret for dig i et minut."
|
||||
|
||||
+1877
-2023
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-08 13:19+0000\n"
|
||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
||||
"PO-Revision-Date: 2024-03-15 18:00+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -649,23 +649,23 @@ msgstr "Keine"
|
||||
msgid "Selected only"
|
||||
msgstr "Nur ausgewählte"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr "Geben Sie eine Seitenzahl zwischen 1 und %(max)s ein."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr "Ungültige Seitenzahl."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Intern einen anderen Namen verwenden"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr "Klicken zum Schließen"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Sie haben ungespeicherte Änderungen!"
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ Backend
|
||||
Badge
|
||||
Badges
|
||||
Bancontact
|
||||
BankID
|
||||
Banking
|
||||
barcodes
|
||||
Bcc
|
||||
@@ -302,7 +301,6 @@ Strg
|
||||
Stripe
|
||||
Stripes
|
||||
Strong
|
||||
Swish
|
||||
systemweiten
|
||||
Tab
|
||||
tag
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-08 13:19+0000\n"
|
||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
||||
"PO-Revision-Date: 2024-03-15 18:00+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||
@@ -648,23 +648,23 @@ msgstr "Keine"
|
||||
msgid "Selected only"
|
||||
msgstr "Nur ausgewählte"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr "Gib eine Seitenzahl zwischen 1 und %(max)s ein."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr "Ungültige Seitenzahl."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Intern einen anderen Namen verwenden"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr "Klicken zum Schließen"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Du hast ungespeicherte Änderungen!"
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ Backend
|
||||
Badge
|
||||
Badges
|
||||
Bancontact
|
||||
BankID
|
||||
Banking
|
||||
barcodes
|
||||
Bcc
|
||||
@@ -302,7 +301,6 @@ Strg
|
||||
Stripe
|
||||
Stripes
|
||||
Strong
|
||||
Swish
|
||||
systemweiten
|
||||
Tab
|
||||
tag
|
||||
|
||||
+1856
-1997
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-24 08:47+0000\n"
|
||||
"POT-Creation-Date: 2024-04-26 09:26+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -627,23 +627,23 @@ msgstr ""
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+1874
-2049
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-08 13:19+0000\n"
|
||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
||||
"PO-Revision-Date: 2019-10-03 19:00+0000\n"
|
||||
"Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n"
|
||||
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -687,23 +687,23 @@ msgstr "Κανένας"
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Χρησιμοποιήστε διαφορετικό όνομα εσωτερικά"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr "Κάντε κλικ για να κλείσετε"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+1856
-1997
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-08 13:19+0000\n"
|
||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -627,23 +627,23 @@ msgstr ""
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+1866
-2040
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-08 13:19+0000\n"
|
||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
||||
"PO-Revision-Date: 2023-11-07 14:00+0000\n"
|
||||
"Last-Translator: Zona Vip <contacto@zonavip.mx>\n"
|
||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
@@ -657,23 +657,23 @@ msgstr "Ninguno"
|
||||
msgid "Selected only"
|
||||
msgstr "Solamente seleccionados"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Usar un nombre diferente internamente"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr "Click para cerrar"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "¡Tienes cambios sin guardar!"
|
||||
|
||||
|
||||
+1858
-2028
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-08 13:19+0000\n"
|
||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
||||
"PO-Revision-Date: 2021-11-10 05:00+0000\n"
|
||||
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
|
||||
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
@@ -652,23 +652,23 @@ msgstr ""
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Käytä toista nimeä sisäisesti"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr "Sulje klikkaamalla"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Sinulla on tallentamattomia muutoksia!"
|
||||
|
||||
|
||||
+1989
-2142
File diff suppressed because it is too large
Load Diff
@@ -6,9 +6,9 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: French\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-08 13:19+0000\n"
|
||||
"PO-Revision-Date: 2024-06-12 03:00+0000\n"
|
||||
"Last-Translator: simonD <simon.dalvy@gmail.com>\n"
|
||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
||||
"PO-Revision-Date: 2023-09-11 10: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"
|
||||
"Language: fr\n"
|
||||
@@ -16,7 +16,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 5.5.5\n"
|
||||
"X-Generator: Weblate 4.18.2\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -305,7 +305,7 @@ msgstr "Le code du billet est ambigu dans la liste"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
|
||||
msgid "Order not approved"
|
||||
msgstr "Commande non validée"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
|
||||
msgid "Checked-in Tickets"
|
||||
@@ -433,7 +433,7 @@ msgstr "est après"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:40
|
||||
msgid "="
|
||||
msgstr "="
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
|
||||
msgid "Product"
|
||||
@@ -444,9 +444,8 @@ msgid "Product variation"
|
||||
msgstr "Variation du produit"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:107
|
||||
#, fuzzy
|
||||
msgid "Gate"
|
||||
msgstr "Pont"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:111
|
||||
msgid "Current date and time"
|
||||
@@ -472,13 +471,13 @@ msgstr "Nombre d'entrées depuis minuit"
|
||||
#, fuzzy
|
||||
#| msgid "Number of previous entries"
|
||||
msgid "Number of previous entries since"
|
||||
msgstr "Nombre d'entrées précédentes depuis"
|
||||
msgstr "Nombre d'entrées précédentes"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:135
|
||||
#, fuzzy
|
||||
#| msgid "Number of previous entries"
|
||||
msgid "Number of previous entries before"
|
||||
msgstr "Nombre d'entrées précédentes avant"
|
||||
msgstr "Nombre d'entrées précédentes"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:139
|
||||
msgid "Number of days with a previous entry"
|
||||
@@ -643,7 +642,7 @@ msgstr ""
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:475
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||
msgid "Search query"
|
||||
msgstr "Recherche"
|
||||
msgstr "Requête de recherche"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:493
|
||||
msgid "All"
|
||||
@@ -657,24 +656,23 @@ msgstr "Aucun"
|
||||
msgid "Selected only"
|
||||
msgstr "Seuls les sélectionnés"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#, fuzzy
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr "Numéro de page invalide."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Utiliser un nom différent en interne"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr "Cliquez pour fermer"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Vous avez des modifications non sauvegardées !"
|
||||
|
||||
@@ -857,10 +855,9 @@ msgid "Not yet available"
|
||||
msgstr "Méthode de paiement non disponible"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
#, fuzzy
|
||||
msgctxt "widget"
|
||||
msgid "Not available anymore"
|
||||
msgstr "Méthode de paiement plus disponible"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
#, fuzzy
|
||||
@@ -868,7 +865,7 @@ msgstr "Méthode de paiement plus disponible"
|
||||
#| msgid "currently available: %s"
|
||||
msgctxt "widget"
|
||||
msgid "Currently not available"
|
||||
msgstr "Actuellement non disponible"
|
||||
msgstr "actuellement disponible : %s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
#, javascript-format
|
||||
@@ -966,7 +963,7 @@ msgstr "Continuer"
|
||||
#| msgid "Select variant %s"
|
||||
msgctxt "widget"
|
||||
msgid "Show variants"
|
||||
msgstr "Voir les variations"
|
||||
msgstr "Sélectionner les variations %s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||
#, fuzzy
|
||||
@@ -974,7 +971,7 @@ msgstr "Voir les variations"
|
||||
#| msgid "Select variant %s"
|
||||
msgctxt "widget"
|
||||
msgid "Hide variants"
|
||||
msgstr "Masquer les variations"
|
||||
msgstr "Sélectionner les variations %s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
msgctxt "widget"
|
||||
|
||||
+1858
-2017
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-08 13:19+0000\n"
|
||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
||||
"PO-Revision-Date: 2022-02-22 22:00+0000\n"
|
||||
"Last-Translator: Ismael Menéndez Fernández <ismael.menendez@balidea.com>\n"
|
||||
"Language-Team: Galician <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
@@ -659,23 +659,23 @@ msgstr "Ningún"
|
||||
msgid "Selected only"
|
||||
msgstr "Soamente seleccionados"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Usar un nome diferente internamente"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr "Click para cerrar"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Tes cambios sen gardar!"
|
||||
|
||||
|
||||
+1856
-1997
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-08 13:19+0000\n"
|
||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
||||
"PO-Revision-Date: 2021-09-24 13:54+0000\n"
|
||||
"Last-Translator: ofirtro <ofir.tro@gmail.com>\n"
|
||||
"Language-Team: Hebrew <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -635,23 +635,23 @@ msgstr ""
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+1856
-1997
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-08 13:19+0000\n"
|
||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
||||
"PO-Revision-Date: 2024-01-31 04:00+0000\n"
|
||||
"Last-Translator: Pavle Ergović <pavleergovic@gmail.com>\n"
|
||||
"Language-Team: Croatian <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
@@ -629,23 +629,23 @@ msgstr ""
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+1895
-2053
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-08 13:19+0000\n"
|
||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
||||
"PO-Revision-Date: 2020-01-24 08:00+0000\n"
|
||||
"Last-Translator: Prokaj Miklós <mixolid0@gmail.com>\n"
|
||||
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
@@ -675,23 +675,23 @@ msgstr "Semmi"
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Használj másik nevet"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr "Bezárásért kattints"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Mentetlen változtatások!"
|
||||
|
||||
|
||||
+1858
-2038
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-08 13:19+0000\n"
|
||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
||||
"PO-Revision-Date: 2023-09-20 14:01+0000\n"
|
||||
"Last-Translator: Mahdia Aliyy <mahdlyy.k@gmail.com>\n"
|
||||
"Language-Team: Indonesian <https://translate.pretix.eu/projects/pretix/"
|
||||
@@ -658,23 +658,23 @@ msgstr "Tidak ada"
|
||||
msgid "Selected only"
|
||||
msgstr "Hanya dipilih"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Gunakan nama yang berbeda secara internal"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr "Klik untuk menutup"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Anda memiliki perubahan yang belum disimpan!"
|
||||
|
||||
|
||||
+1858
-2028
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-08 13:19+0000\n"
|
||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
||||
"PO-Revision-Date: 2023-05-18 01:00+0000\n"
|
||||
"Last-Translator: M C <micasadmail@gmail.com>\n"
|
||||
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
@@ -653,23 +653,23 @@ msgstr "Nessuno"
|
||||
msgid "Selected only"
|
||||
msgstr "Solo i selezionati"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Utilizza un nome diverso internamente"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr "Clicca per chiudere"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Hai cambiamenti non salvati!"
|
||||
|
||||
|
||||
+1886
-2034
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-08 13:19+0000\n"
|
||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
||||
"PO-Revision-Date: 2022-03-15 00:00+0000\n"
|
||||
"Last-Translator: Yuriko Matsunami <y.matsunami@enobyte.com>\n"
|
||||
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
@@ -655,23 +655,23 @@ msgstr "ない"
|
||||
msgid "Selected only"
|
||||
msgstr "選択したもののみ"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr "内部で別の名前を使用してください"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr "クリックして閉じる"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "保存されていない変更があります!"
|
||||
|
||||
|
||||
+1856
-2006
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-08 13:19+0000\n"
|
||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -627,23 +627,23 @@ msgstr ""
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+1856
-1997
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user