Compare commits

..

2 Commits

Author SHA1 Message Date
Richard Schreiber 6d8d06e78f fix isort 2024-04-15 16:26:11 +02:00
Richard Schreiber 3bde9d8b0e Fix: show country name in countrycodes question in profile description 2024-04-15 16:26:11 +02:00
268 changed files with 134595 additions and 158612 deletions
+16 -16
View File
@@ -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
-18
View File
@@ -1,18 +0,0 @@
.. highlight:: none
.. _`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
-1
View File
@@ -14,4 +14,3 @@ for your needs.
manual_smallscale
dev_version
enterprise
community
@@ -120,7 +120,6 @@ Now we will install pretix itself. The following steps are to be executed as the
actually install pretix, we will create a virtual environment to isolate the python packages from your global
python installation::
# sudo -u pretix -s
$ python3 -m venv /var/pretix/venv
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install -U pip setuptools wheel
@@ -280,7 +279,6 @@ Updates
To upgrade to a new pretix release, pull the latest code changes and run the following commands::
# sudo -u pretix -s
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install -U --upgrade-strategy eager pretix gunicorn
(venv)$ python -m pretix migrate
-6
View File
@@ -103,12 +103,6 @@ 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
Counter. The number of successful backend logins.
pretix_logins_failed
Counter. The number of failed backend logins, labeled with ``reason``.
.. _metric types: https://prometheus.io/docs/concepts/metric_types/
.. _Prometheus: https://prometheus.io/
.. _cProfile: https://docs.python.org/3/library/profile.html
-1
View File
@@ -19,4 +19,3 @@ Contents:
permissions
logging
locking
timemachine
+1 -10
View File
@@ -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.
@@ -1,32 +0,0 @@
Time machine mode
=================
In test mode, pretix provides a "time machine" feature which allows event organizers
to test their shop as if it were a different date and time. To enable this feature, they can
click on the "time machine"-link in the test mode warning box on the event page.
Internally, this time machine mode is implemented by calling our custom :py:meth:`time_machine_now()`
function instead of :py:meth:`django.utils.timezone.now()` in all places where the fake time should be
taken into account. If you add code that uses the current date and time for checking whether some
product can be bought, you should use :py:meth:`time_machine_now`.
.. autofunction:: pretix.base.timemachine.time_machine_now
Background tasks
----------------
The time machine datetime is passed through the request flow via a thread-local variable (ContextVar).
Therefore, if you call a background task in the order process, where time_machine_now should be
respected, you need to pass it through manually as shown in the example below:
.. code-block:: python
@app.task()
def my_task(self, override_now_dt: datetime=None) -> None:
with time_machine_now_assigned(override_now_dt):
# ...do something that uses time_machine_now()
my_task.apply_async(kwargs={'override_now_dt': time_machine_now(default=None)})
.. autofunction:: pretix.base.timemachine.time_machine_now_assigned
@@ -90,10 +90,6 @@ as its first argument and can be used like this::
<a href="{% eventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
<a href="{% abseventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
To generate absolute URLs on the main domain, you can use the ``absurl`` template tag::
{% load eventurl %}
<a href="{% absmainurl "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}">Event settings</a>
Implementation details
----------------------
-10
View File
@@ -211,15 +211,5 @@ with the documentation a lot, you might find it useful to use sphinx-autobuild::
Then, go to http://localhost:8081 for a version of the documentation that automatically re-builds
whenever you change a source file.
Working with frontend assets
----------------------------
To update the frontend styles of shops with a custom styling, run the following commands inside
your virtual environment.::
python -m pretix collectstatic --noinput
python -m pretix updatestyles
.. _Django's documentation: https://docs.djangoproject.com/en/1.11/ref/django-admin/#runserver
.. _pretixdroid: https://github.com/pretix/pretixdroid
+2 -2
View File
@@ -31,7 +31,7 @@ pretix/
Additional code implementing our customized :ref:`URL handling <urlconf>`.
static/
Contains all static files (CSS/SASS, JavaScript, images) of pretix' core.
Contains all static files (CSS/SASS, JavaScript, images) of pretix' core
We use libsass as a preprocessor for CSS. Our own sass code is built in the same
step as Bootstrap and FontAwesome, so their mixins etc. are fully available.
@@ -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.
+5 -18
View File
@@ -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
+1 -1
View File
@@ -1,4 +1,4 @@
sphinx==7.3.*
sphinx==7.2.*
jinja2==3.1.*
sphinx-rtd-theme
sphinxcontrib-httpdomain
+1 -1
View File
@@ -1,5 +1,5 @@
-e ../
sphinx==7.3.*
sphinx==7.2.*
jinja2==3.1.*
sphinx-rtd-theme
sphinxcontrib-httpdomain
-113
View File
@@ -1,113 +0,0 @@
Android version support policy
==============================
Building software for Android always presents a struggle between keeping compatibility with older hardware to save cost
and utilizing feature of new Android versions to improve functionality, security and stability. To help you plan ahead,
we are publishing our intended schedule. This is to be understood as a minimum commitment, we will only drop support for
older versions if there is a technical reason to do so, not because the scheduled time has been reached.
.. warning:: This is a non-binding document. We will try our very best to not to deprecate support for Android versions
earlier than listed here, but for technical or economical reasons, it might become necessary to do so under
specific circumstances. Specifically, we might be forced to partially drop support for Android versions
earlier where we integrate third-party components into our software. Typical examples would be specific
payment terminal or printer types where we use a third-party component provided by the hardware vendor.
If we no longer support an Android version, it means that we will no longer publish new versions of the app supporting
that Android version. This means you are not getting new features or bug fixes, and at some point your app might stop
working with the pretix server.
pretixSCAN
----------
=========================== ==========================================================
Android Version Support schedule
=========================== ==========================================================
Android 14 Support planned until at least 12/2029.
Android 13 Support planned until at least 12/2028.
Android 12 Support planned until at least 12/2027.
Android 11 Support planned until at least 12/2026.
Android 10 Support planned until at least 12/2025.
Android 9 Support planned until at least 12/2025.
Android 8 Support planned until at least 12/2025.
Android 7 Support planned until at least 06/2025.
Android 6 Support planned until at least 06/2025.
Android 5 | Support planned until at least 06/2025.
| No support for COVID certificate verification.
Android 4 Support dropped.
=========================== ==========================================================
pretixPOS
---------
=========================== ==========================================================
Android Version Support schedule
=========================== ==========================================================
Android 14 | Support planned until at least 12/2029.
| Limited support for Swissbit microSD TSE (only tested devices).
Android 13 | Support planned until at least 12/2028.
| Limited support for Swissbit microSD TSE (only tested devices).
Android 12 | Support planned until at least 12/2027.
| Limited support for Swissbit microSD TSE (only tested devices).
Android 11 | Support planned until at least 12/2026.
| No support for Swissbit microSD TSE.
Android 10 Support planned until at least 12/2025.
Android 9 Support planned until at least 12/2025.
Android 8 | Support planned until at least 12/2025.
| Support for Stripe Terminal on some devices to be dropped 05/2024.
Android 7 | Support planned until at least 12/2024.
| Support for Stripe Terminal to be dropped 05/2024.
| No support for Cryptovision TSE.
Android 6 | Support planned until at least 12/2024.
| No support for Cryptovision TSE.
| No support for Fiskal Cloud.
| No support for Stripe Terminal.
Android 5 | Support planned until at least 12/2024.
| No support for Cryptovision TSE.
| No support for Fiskal Cloud.
| No support for Stripe Terminal.
| No support for SumUp.
| No support for COVID certificate verification.
Android 4 Support dropped.
=========================== ==========================================================
pretixPRINT
-----------
=========================== ==========================================================
Android Version Support schedule
=========================== ==========================================================
Android 14 Support planned until at least 12/2029.
Android 13 Support planned until at least 12/2028.
Android 12 Support planned until at least 12/2027.
Android 11 Support planned until at least 12/2026.
Android 10 Support planned until at least 12/2025.
Android 9 Support planned until at least 12/2025.
Android 8 Support planned until at least 12/2025.
Android 7 Support planned until at least 06/2025.
Android 6 Support planned until at least 06/2025.
Android 5 | Support planned until at least 06/2025.
| No support for Evolis printers on some devices.
Android 4.4 | Support planned until at least 06/2024.
| No support for USB printers.
| No support for Evolis printers.
Android 4 Support dropped.
=========================== ==========================================================
pretixLEAD
----------
=========================== ==========================================================
Android Version Support schedule
=========================== ==========================================================
Android 14 Support planned until at least 12/2029.
Android 13 Support planned until at least 12/2028.
Android 12 Support planned until at least 12/2027.
Android 11 Support planned until at least 12/2026.
Android 10 Support planned until at least 12/2025.
Android 9 Support planned until at least 12/2025.
Android 8 Support planned until at least 12/2025.
Android 7 Support planned until at least 12/2024.
Android 6 Support planned until at least 12/2024.
Android 5 Support planned until at least 12/2024.
Android 4 Support dropped.
=========================== ==========================================================
+5 -14
View File
@@ -17,8 +17,8 @@ and then click "Generate widget code".
You will obtain two code snippets that look *roughly* like the following. The first should be embedded into the
``<head>`` part of your website, if possible. If this inconvenient, you can put it in the ``<body>`` part as well::
<link rel="stylesheet" type="text/css" href="https://pretix.eu/demo/democon/widget/v1.css" crossorigin>
<script type="text/javascript" src="https://pretix.eu/widget/v1.en.js" async crossorigin></script>
<link rel="stylesheet" type="text/css" href="https://pretix.eu/demo/democon/widget/v1.css">
<script type="text/javascript" src="https://pretix.eu/widget/v1.en.js" async></script>
The second snippet should be embedded at the position where the widget should show up::
@@ -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
@@ -449,14 +449,5 @@ Further reading:
* `Stripe Payment Method Domain registration`_
Working with Cross-Origin-Embedder-Policy
-----------------------------------------
The pretix widget is unfortunately not compatible with ``Cross-Origin-Embedder-Policy: require-corp``. If you include
the ``crossorigin`` attributes on the ``<script>`` and ``<link>`` tag as shown above, the widget can show a calendar
or product list, but will not be able to open the checkout process in an iframe. If you also set
``Cross-Origin-Opener-Policy: same-origin``, the widget can auto-detect that it is running in an isolated enviroment
and will instead open the checkout process in a new tab.
.. _Let's Encrypt: https://letsencrypt.org/
.. _Stripe Payment Method Domain registration: https://stripe.com/docs/payments/payment-methods/pmd-registration
-1
View File
@@ -16,5 +16,4 @@ wanting to use pretix to sell tickets.
events/giftcards
faq
markdown
android-version-support
glossary
-3
View File
@@ -11,9 +11,6 @@ In many places of your shop, like frontpage texts, product descriptions and emai
since it is way easier to learn than languages like HTML but allows all basic formatting options required
for text in those places.
.. note:: Some fields that are used in one-line context only allow formatting that refers to individual words
(such as bold or italic font or a link) but do not allow block-level formatting like lists or headlines.
Formatting rules
----------------
+18 -18
View File
@@ -30,32 +30,32 @@ dependencies = [
"babel",
"BeautifulSoup4==4.12.*",
"bleach==5.0.*",
"celery==5.4.*",
"celery==5.3.*",
"chardet==5.2.*",
"cryptography>=3.4.2",
"css-inline==0.14.*",
"defusedcsv>=1.1.0",
"dj-static",
"Django[argon2]==4.2.*",
"django-bootstrap3==24.2",
"django-compressor==4.5",
"Django==4.2.*",
"django-bootstrap3==23.6.*",
"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",
"django-markup",
"django-oauth-toolkit==2.3.*",
"django-otp==1.5.*",
"django-otp==1.4.*",
"django-phonenumber-field==7.3.*",
"django-redis==5.4.*",
"django-scopes==2.0.*",
"django-statici18n==2.5.*",
"djangorestframework==3.15.*",
"django-statici18n==2.4.*",
"djangorestframework==3.14.*",
"dnspython==2.6.*",
"drf_ujson2==1.7.*",
"geoip2==4.*",
@@ -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",
@@ -90,9 +90,9 @@ dependencies = [
"pyuca",
"qrcode==7.4.*",
"redis==5.0.*",
"reportlab==4.2.*",
"reportlab==4.1.*",
"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.21.*",
"flake8==7.0.*",
"freezegun",
"isort==5.13.*",
"pep8-naming==0.14.*",
"pep8-naming==0.13.*",
"potypo",
"pytest-asyncio",
"pytest-cache",
@@ -126,8 +126,8 @@ dev = [
"pytest-mock==3.14.*",
"pytest-rerunfailures==14.*",
"pytest-sugar",
"pytest-xdist==3.6.*",
"pytest==8.2.*",
"pytest-xdist==3.5.*",
"pytest==8.1.*",
"responses",
]
+1 -1
View File
@@ -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.7.0.dev0"
__version__ = "2024.4.0.dev0"
+1 -8
View File
@@ -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')),
@@ -237,12 +235,7 @@ COMPRESS_FILTERS = {
)
}
CURRENCIES = [
c for c in currencies
if c.alpha_3 not in {
'XAG', 'XAU', 'XBA', 'XBB', 'XBC', 'XBD', 'XDR', 'XPD', 'XPT', 'XSU', 'XTS', 'XUA',
}
]
CURRENCIES = list(currencies)
CURRENCY_PLACES = {
# default is 2
'BIF': 0,
+2 -5
View File
@@ -472,8 +472,7 @@ class SubEventSerializer(I18nAwareModelSerializer):
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
'presale_start', 'presale_end', 'location', 'geo_lat', 'geo_lon', 'event', 'is_public',
'frontpage_text', 'seating_plan', 'item_price_overrides', 'variation_price_overrides',
'meta_data', 'seat_category_mapping', 'last_modified', 'best_availability_state',
'comment')
'meta_data', 'seat_category_mapping', 'last_modified', 'best_availability_state')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -684,9 +683,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 +734,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',
+1 -7
View File
@@ -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)
@@ -1589,9 +1586,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider:
payment_provider = 'free'
if order.total != Decimal('0.00') and order.event.currency == "XXX":
raise ValidationError('Paid products not supported without a valid currency.')
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID and not validated_data.get('require_approval'):
order.status = Order.STATUS_PAID
order.save()
-1
View File
@@ -89,7 +89,6 @@ class SettingsSerializer(serializers.Serializer):
except OSError: # pragma: no cover
logger.error('Deleting file %s failed.' % fname.name)
instance.delete(attr)
self.changed_data.append(attr)
else:
# file is unchanged
continue
+5 -14
View File
@@ -190,10 +190,7 @@ class EventViewSet(viewsets.ModelViewSet):
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
@transaction.atomic()
def perform_update(self, serializer):
original_data = self.get_serializer(instance=serializer.instance).data
current_live_value = serializer.instance.live
updated_live_value = serializer.validated_data.get('live', None)
current_plugins_value = serializer.instance.get_plugins()
@@ -201,11 +198,6 @@ class EventViewSet(viewsets.ModelViewSet):
super().perform_update(serializer)
if serializer.data == original_data:
# Performance optimization: If nothing was changed, we do not need to save or log anything.
# This costs us a few cycles on save, but avoids thousands of lines in our log.
return
if updated_live_value is not None and updated_live_value != current_live_value:
log_action = 'pretix.event.live.activated' if updated_live_value else 'pretix.event.live.deactivated'
serializer.instance.log_action(
@@ -630,12 +622,11 @@ class EventSettingsView(views.APIView):
s.is_valid(raise_exception=True)
with transaction.atomic():
s.save()
if s.changed_data:
self.request.event.log_action(
'pretix.event.settings', user=self.request.user, auth=self.request.auth, data={
k: v for k, v in s.validated_data.items()
}
)
self.request.event.log_action(
'pretix.event.settings', user=self.request.user, auth=self.request.auth, data={
k: v for k, v in s.validated_data.items()
}
)
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
regenerate_css.apply_async(args=(request.event.pk,))
s = EventSettingsSerializer(
+1 -6
View File
@@ -97,7 +97,6 @@ from pretix.base.signals import (
)
from pretix.base.templatetags.money import money_filter
from pretix.control.signals import order_search_filter_q
from pretix.helpers import OF_SELF
logger = logging.getLogger(__name__)
@@ -576,10 +575,8 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
return self.retrieve(request, [], **kwargs)
@action(detail=True, methods=['POST'])
@transaction.atomic()
def create_invoice(self, request, **kwargs):
order = self.get_object()
order = Order.objects.select_for_update(of=OF_SELF).get(pk=order.pk)
has_inv = order.invoices.exists() and not (
order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
and order.invoices.filter(is_cancellation=True).count() >= order.invoices.filter(is_cancellation=False).count()
@@ -1908,7 +1905,6 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
return Response(status=204)
@action(detail=True, methods=['POST'])
@transaction.atomic()
def reissue(self, request, **kwargs):
inv = self.get_object()
if inv.canceled:
@@ -1916,10 +1912,9 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
elif inv.shredded:
raise PermissionDenied('The invoice file is no longer stored on the server.')
else:
order = Order.objects.select_for_update(of=OF_SELF).get(pk=inv.order_id)
c = generate_cancellation(inv)
if inv.order.status != Order.STATUS_CANCELED:
inv = generate_invoice(order)
inv = generate_invoice(inv.order)
else:
inv = c
inv.order.log_action(
+1 -24
View File
@@ -39,12 +39,10 @@ from zipfile import ZipFile
from django import forms
from django.dispatch import receiver
from django.urls import reverse
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.models import QuestionAnswer
from ...control.forms.widgets import Select2
from ..exporter import BaseExporter
from ..signals import register_data_exporters
@@ -58,7 +56,7 @@ class AnswerFilesExporter(BaseExporter):
@property
def export_form_fields(self):
d = OrderedDict(
return OrderedDict(
[
('questions',
forms.ModelMultipleChoiceField(
@@ -71,32 +69,11 @@ class AnswerFilesExporter(BaseExporter):
)),
]
)
if self.event.has_subevents:
d['subevent'] = forms.ModelChoiceField(
label=pgettext_lazy('subevent', 'Date'),
queryset=self.event.subevents.all(),
required=False,
empty_label=pgettext_lazy('subevent', 'All dates')
)
d['subevent'].widget = Select2(
attrs={
'data-model-select2': 'event',
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
}),
'data-placeholder': pgettext_lazy('subevent', 'All dates')
}
)
d['subevent'].widget.choices = d['subevent'].choices
return d
def render(self, form_data: dict):
qs = QuestionAnswer.objects.filter(
orderposition__order__event=self.event,
).select_related('orderposition', 'orderposition__order', 'question')
if form_data.get('subevent'):
qs = qs.filter(orderposition__subevent=form_data.get('subevent'))
if form_data.get('questions'):
qs = qs.filter(question__in=form_data['questions'])
with tempfile.TemporaryDirectory() as d:
+5 -30
View File
@@ -32,12 +32,11 @@
# 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
from django import forms
from django.conf import settings
from django.db.models import (
Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef,
Q, Subquery, Sum, When,
@@ -55,7 +54,7 @@ from openpyxl.comments import Comment
from openpyxl.styles import Font, PatternFill
from pretix.base.models import (
Checkin, GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
OrderPosition, Question,
)
from pretix.base.models.orders import (
@@ -542,22 +541,6 @@ class OrderListExporter(MultiSheetListExporter):
).order_by()
qs = base_qs.annotate(
payment_providers=Subquery(p_providers, output_field=CharField()),
checked_in_lists=Subquery(
Checkin.objects.filter(
successful=True,
type=Checkin.TYPE_ENTRY,
position=OuterRef("pk"),
).order_by().values("position").annotate(
c=GroupConcat(
"list__name",
# These appear not to work properly on SQLite. Well, we don't support SQLite outside testing
# anyways.
ordered='sqlite' not in settings.DATABASES['default']['ENGINE'],
distinct='sqlite' not in settings.DATABASES['default']['ENGINE'],
delimiter=", "
)
).values("c")
),
).select_related(
'order', 'order__invoice_address', 'order__customer', 'item', 'variation',
'voucher', 'tax_rule'
@@ -622,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)
@@ -634,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'),
@@ -655,7 +636,6 @@ class OrderListExporter(MultiSheetListExporter):
_('Sales channel'), _('Order locale'),
_('E-mail address verified'),
_('External customer ID'),
_('Check-in lists'),
_('Payment providers'),
]
@@ -747,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
@@ -760,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, ''))
@@ -794,7 +770,6 @@ class OrderListExporter(MultiSheetListExporter):
_('Yes') if order.email_known_to_work else _('No'),
str(order.customer.external_identifier) if order.customer and order.customer.external_identifier else '',
]
row.append(op.checked_in_lists or "")
row.append(', '.join([
str(self.providers.get(p, p)) for p in sorted(set((op.payment_providers or '').split(',')))
if p and p != 'free'
-38
View File
@@ -39,7 +39,6 @@ from django import forms
from django.core.validators import URLValidator
from django.forms.models import ModelFormMetaclass
from django.utils.crypto import get_random_string
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from formtools.wizard.views import SessionWizardView
from hierarkey.forms import HierarkeyForm
@@ -86,43 +85,6 @@ class I18nInlineFormSet(i18nfield.forms.I18nInlineFormSet):
super().__init__(*args, **kwargs)
class MarkdownTextarea(forms.Textarea):
def _render(self, template_name, context, renderer=None):
return mark_safe(
'<div class="i18n-form-group">%s<div class="i18n-field-markdown-note">%s</div></div>' % (
super()._render(template_name, context, renderer=None),
_("You can use {markup_name} in this field.").format(
markup_name='<a href="https://docs.pretix.eu/en/latest/user/markdown.html" target="_blank">Markdown</a>'
)
)
)
class I18nMarkdownTextarea(i18nfield.forms.I18nTextarea):
def format_output(self, rendered_widgets) -> str:
rendered_widgets = rendered_widgets + [
'<div class="i18n-field-markdown-note">%s</div>' % (
_("You can use {markup_name} in this field.").format(
markup_name='<a href="https://docs.pretix.eu/en/latest/user/markdown.html" target="_blank">Markdown</a>'
)
)
]
return super().format_output(rendered_widgets)
class I18nMarkdownTextInput(i18nfield.forms.I18nTextInput):
def format_output(self, rendered_widgets) -> str:
rendered_widgets = rendered_widgets + [
'<div class="i18n-field-markdown-note">%s</div>' % (
_("You can use {markup_name} in this field.").format(
markup_name='<a href="https://docs.pretix.eu/en/latest/user/markdown.html" target="_blank">Markdown</a>'
)
)
]
return super().format_output(rendered_widgets)
SECRET_REDACTED = '*****'
+6 -7
View File
@@ -57,7 +57,7 @@ from django.forms.widgets import FILE_INPUT_CONTRADICTION
from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.timezone import get_current_timezone
from django.utils.timezone import get_current_timezone, now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries import countries
from django_countries.fields import Country, CountryField
@@ -86,7 +86,6 @@ from pretix.base.settings import (
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
)
from pretix.base.templatetags.rich_text import rich_text
from pretix.base.timemachine import time_machine_now
from pretix.control.forms import (
ExtFileField, ExtValidationMixin, SizeValidationMixin, SplitDateTimeField,
)
@@ -607,13 +606,13 @@ class BaseQuestionsForm(forms.Form):
if cartpos and item.validity_mode == Item.VALIDITY_MODE_DYNAMIC and item.validity_dynamic_start_choice:
if item.validity_dynamic_start_choice_day_limit:
max_date = time_machine_now().astimezone(event.timezone) + timedelta(days=item.validity_dynamic_start_choice_day_limit)
max_date = now().astimezone(event.timezone) + timedelta(days=item.validity_dynamic_start_choice_day_limit)
else:
max_date = None
min_date = time_machine_now()
min_date = now()
initial = None
if (item.require_membership or (pos.variation and pos.variation.require_membership)) and pos.used_membership:
if pos.used_membership.date_start >= time_machine_now():
if pos.used_membership.date_start >= now():
initial = min_date = pos.used_membership.date_start
max_date = min(max_date, pos.used_membership.date_end) if max_date else pos.used_membership.date_end
if item.validity_dynamic_duration_months or item.validity_dynamic_duration_days:
@@ -1035,7 +1034,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 +1170,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:
+16 -15
View File
@@ -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,18 +0,0 @@
# Generated by Django 4.2.10 on 2024-04-19 14:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0261_userknownloginsource"),
]
operations = [
migrations.AddField(
model_name="subevent",
name="comment",
field=models.TextField(null=True),
),
]
@@ -1,26 +0,0 @@
# Generated by Django 4.2.10 on 2024-04-09 07:32
from django.db import migrations
def change_currencies(apps, schema_editor):
Event = apps.get_model("pretixbase", "Event")
Event.objects.filter(
currency__in={
'XAG', 'XAU', 'XBA', 'XBB', 'XBC', 'XBD', 'XDR', 'XPD', 'XPT', 'XSU', 'XTS', 'XUA',
}
).update(currency='XXX')
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0262_subevent_comment"),
]
operations = [
migrations.RunPython(
change_currencies, migrations.RunPython.noop
)
]
@@ -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,
),
),
]
+3 -30
View File
@@ -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))
+1 -8
View File
@@ -66,7 +66,6 @@ class SubeventColumn(ImportColumn):
class MaxUsagesColumn(IntegerColumnMixin, ImportColumn):
identifier = 'max_usages'
verbose_name = gettext_lazy('Maximum usages')
default_value = None
initial = "static:1"
def static_choices(self):
@@ -74,11 +73,6 @@ class MaxUsagesColumn(IntegerColumnMixin, ImportColumn):
("1", "1")
]
def clean(self, value, previous_values):
if value is None and previous_values.get("code"):
raise ValidationError(_('The maximum number of usages must be set.'))
return super().clean(value, previous_values)
def assign(self, value, obj: Voucher, **kwargs):
obj.max_usages = value if value is not None else 1
@@ -86,7 +80,6 @@ class MaxUsagesColumn(IntegerColumnMixin, ImportColumn):
class MinUsagesColumn(IntegerColumnMixin, ImportColumn):
identifier = 'min_usages'
verbose_name = gettext_lazy('Minimum usages')
default_value = None
initial = "static:1"
def static_choices(self):
@@ -253,7 +246,7 @@ class QuotaColumn(ImportColumn):
raise ValidationError(_("You cannot specify a quota if you specified a product."))
matches = [
q for q in self.quotas
if str(q.pk) == value or q.name == value
if str(q.pk) == value or any((v and v == value) for v in i18n_flat(q.name))
]
if len(matches) == 0:
raise ValidationError(_("No matching variation was found."))
+3 -7
View File
@@ -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:
+1 -1
View File
@@ -285,7 +285,7 @@ class CheckinList(LoggedModel):
}
allowed_vars = {
'product', 'variation', 'now', 'now_isoweekday', 'entries_number', 'entries_today', 'entries_days',
'minutes_since_last_entry', 'minutes_since_first_entry', 'gate', 'entry_status',
'minutes_since_last_entry', 'minutes_since_first_entry', 'gate',
}
if not rules or not isinstance(rules, dict):
return rules
+1 -2
View File
@@ -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:
+18 -25
View File
@@ -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 (
@@ -68,7 +67,6 @@ from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.models.base import LoggedModel
from pretix.base.models.fields import MultiStringField
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.timemachine import time_machine_now
from pretix.base.validators import EventSlugBanlistValidator
from pretix.helpers.database import GroupConcat
from pretix.helpers.daterange import daterange
@@ -236,7 +234,7 @@ class EventMixin:
if not self.settings.waiting_list_enabled:
return False
if self.settings.waiting_list_auto_disable:
return self.settings.waiting_list_auto_disable.datetime(self) > time_machine_now()
return self.settings.waiting_list_auto_disable.datetime(self) > now()
return True
@property
@@ -245,11 +243,11 @@ class EventMixin:
Is true, when ``presale_end`` is set and in the past.
"""
if self.effective_presale_end:
return time_machine_now() > self.effective_presale_end
return now() > self.effective_presale_end
elif self.date_to:
return time_machine_now() > self.date_to
return now() > self.date_to
else:
return time_machine_now().astimezone(self.timezone).date() > self.date_from.astimezone(self.timezone).date()
return now().astimezone(self.timezone).date() > self.date_from.astimezone(self.timezone).date()
@property
def effective_presale_start(self):
@@ -269,7 +267,7 @@ class EventMixin:
Is true, when ``presale_end`` is not set or in the future and ``presale_start`` is not
set or in the past.
"""
if self.effective_presale_start and time_machine_now() < self.effective_presale_start:
if self.effective_presale_start and now() < self.effective_presale_start:
return False
return not self.presale_has_ended
@@ -317,11 +315,11 @@ class EventMixin:
q_variation = (
Q(active=True)
& Q(sales_channels__contains=channel)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()))
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& Q(item__active=True)
& Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=time_machine_now()))
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=time_machine_now()))
& Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=now()))
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=now()))
& Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False))
& Q(item__sales_channels__contains=channel)
& Q(item__require_bundling=False)
@@ -696,7 +694,7 @@ class Event(EventMixin, LoggedModel):
@property
def presale_has_ended(self):
if self.has_subevents:
return self.presale_end and time_machine_now() > self.presale_end
return self.presale_end and now() > self.presale_end
else:
return super().presale_has_ended
@@ -1027,7 +1025,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))
@@ -1189,8 +1187,8 @@ class Event(EventMixin, LoggedModel):
)
).filter(
Q(active=True) & Q(is_public=True) & (
Q(Q(date_to__isnull=True) & Q(date_from__gte=time_machine_now() - timedelta(hours=24)))
| Q(date_to__gte=time_machine_now() - timedelta(hours=24))
Q(Q(date_to__isnull=True) & Q(date_from__gte=now() - timedelta(hours=24)))
| Q(date_to__gte=now() - timedelta(hours=24))
)
) # order_by doesn't make sense with I18nField
if ordering in ("date_ascending", "date_descending"):
@@ -1239,9 +1237,6 @@ class Event(EventMixin, LoggedModel):
if self.has_paid_things and not self.has_payment_provider:
issues.append(_('You have configured at least one paid product but have not enabled any payment methods.'))
if self.has_paid_things and self.currency == "XXX":
issues.append(_('You have configured at least one paid product but have not configured a currency.'))
if not self.quotas.exists():
issues.append(_('You need to configure at least one quota to sell anything.'))
@@ -1463,15 +1458,13 @@ class SubEvent(EventMixin, LoggedModel):
)
frontpage_text = I18nTextField(
null=True, blank=True,
verbose_name=_("Frontpage text"),
verbose_name=_("Frontpage text")
)
seating_plan = models.ForeignKey('SeatingPlan', on_delete=models.PROTECT, null=True, blank=True,
related_name='subevents', verbose_name=_('Seating plan'))
comment = models.TextField(
verbose_name=_("Internal comment"),
null=True, blank=True
)
items = models.ManyToManyField('Item', through='SubEventItem')
variations = models.ManyToManyField('ItemVariation', through='SubEventItemVariation')
last_modified = models.DateTimeField(
auto_now=True, db_index=True
@@ -1508,7 +1501,7 @@ class SubEvent(EventMixin, LoggedModel):
disabled_items=Coalesce(
Subquery(
SubEventItem.objects.filter(
Q(disabled=True) | Q(available_from__gt=time_machine_now()) | Q(available_until__lt=time_machine_now()),
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()),
subevent=OuterRef('pk'),
).order_by().values('subevent').annotate(items=GroupConcat('item_id', delimiter=',')).values('items'),
output_field=models.TextField(),
@@ -1519,7 +1512,7 @@ class SubEvent(EventMixin, LoggedModel):
disabled_vars=Coalesce(
Subquery(
SubEventItemVariation.objects.filter(
Q(disabled=True) | Q(available_from__gt=time_machine_now()) | Q(available_until__lt=time_machine_now()),
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()),
subevent=OuterRef('pk'),
).order_by().values('subevent').annotate(items=GroupConcat('variation_id', delimiter=',')).values('items'),
output_field=models.TextField(),
+16 -29
View File
@@ -55,7 +55,7 @@ from django.db.models import Q
from django.utils import formats
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.timezone import is_naive, make_aware
from django.utils.timezone import is_naive, make_aware, now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries.fields import Country
from django_scopes import ScopedManager
@@ -65,7 +65,6 @@ from pretix.base.models import fields
from pretix.base.models.base import LoggedModel
from pretix.base.models.fields import MultiStringField
from pretix.base.models.tax import TaxedPrice
from pretix.base.timemachine import time_machine_now
from ...helpers.images import ImageSizeValidator
from ..media import MEDIA_TYPES
@@ -122,16 +121,6 @@ class ItemCategory(LoggedModel):
return _('{category} (Add-On products)').format(category=str(name))
return str(name)
def get_category_type_display(self):
if self.is_addon:
return _('Add-On products')
else:
return None
@property
def category_type(self):
return 'addon' if self.is_addon else 'normal'
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
if self.event:
@@ -203,7 +192,7 @@ class SubEventItem(models.Model):
self.subevent.event.cache.clear()
def is_available(self, now_dt: datetime=None) -> bool:
now_dt = now_dt or time_machine_now()
now_dt = now_dt or now()
if self.disabled:
return False
if self.available_from and self.available_from > now_dt:
@@ -259,7 +248,7 @@ class SubEventItemVariation(models.Model):
self.subevent.event.cache.clear()
def is_available(self, now_dt: datetime=None) -> bool:
now_dt = now_dt or time_machine_now()
now_dt = now_dt or now()
if self.disabled:
return False
if self.available_from and self.available_from > now_dt:
@@ -274,8 +263,8 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False):
# IMPORTANT: If this is updated, also update the ItemVariation query
# in models/event.py: EventMixin.annotated()
Q(active=True)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()) | Q(available_from_mode='info'))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()) | Q(available_until_mode='info'))
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()) | Q(available_from_mode='info'))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()) | Q(available_until_mode='info'))
& Q(sales_channels__contains=channel) & Q(require_bundling=False)
)
if not allow_addons:
@@ -454,8 +443,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(
@@ -794,7 +782,7 @@ class Item(LoggedModel):
return t
def is_available_by_time(self, now_dt: datetime=None) -> bool:
now_dt = now_dt or time_machine_now()
now_dt = now_dt or now()
if self.available_from and self.available_from > now_dt:
return False
if self.available_until and self.available_until < now_dt:
@@ -806,13 +794,13 @@ class Item(LoggedModel):
Returns whether this item is available according to its ``active`` flag
and its ``available_from`` and ``available_until`` fields
"""
now_dt = now_dt or time_machine_now()
now_dt = now_dt or now()
if not self.active or not self.is_available_by_time(now_dt):
return False
return True
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
now_dt = now_dt or time_machine_now()
now_dt = now_dt or now()
subevent_item = subevent and subevent.item_overrides.get(self.pk)
if not self.active:
return 'active'
@@ -969,11 +957,11 @@ class Item(LoggedModel):
return self.validity_fixed_from, self.validity_fixed_until
elif self.validity_mode == Item.VALIDITY_MODE_DYNAMIC:
tz = override_tz or self.event.timezone
requested_start = requested_start or time_machine_now()
requested_start = requested_start or now()
if enforce_start_limit and not self.validity_dynamic_start_choice:
requested_start = time_machine_now()
requested_start = now()
if enforce_start_limit and self.validity_dynamic_start_choice_day_limit is not None:
requested_start = min(requested_start, time_machine_now() + timedelta(days=self.validity_dynamic_start_choice_day_limit))
requested_start = min(requested_start, now() + timedelta(days=self.validity_dynamic_start_choice_day_limit))
valid_until = requested_start.astimezone(tz)
@@ -1097,8 +1085,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(
@@ -1303,7 +1290,7 @@ class ItemVariation(models.Model):
return ItemVariation.objects.filter(item=self.item).count() == 1
def is_available_by_time(self, now_dt: datetime=None) -> bool:
now_dt = now_dt or time_machine_now()
now_dt = now_dt or now()
if self.available_from and self.available_from > now_dt:
return False
if self.available_until and self.available_until < now_dt:
@@ -1315,13 +1302,13 @@ class ItemVariation(models.Model):
Returns whether this item is available according to its ``active`` flag
and its ``available_from`` and ``available_until`` fields
"""
now_dt = now_dt or time_machine_now()
now_dt = now_dt or now()
if not self.active or not self.is_available_by_time(now_dt):
return False
return True
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
now_dt = now_dt or time_machine_now()
now_dt = now_dt or now()
subevent_var = subevent and subevent.var_overrides.get(self.pk)
if not self.active:
return 'active'
+3 -3
View File
@@ -23,6 +23,7 @@ from django.db import models
from django.db.models import Count, OuterRef, Subquery, Value
from django.db.models.functions import Coalesce
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled
from i18nfield.fields import I18nCharField
@@ -30,7 +31,6 @@ from i18nfield.fields import I18nCharField
from pretix.base.models import Customer
from pretix.base.models.base import LoggedModel
from pretix.base.models.organizer import Organizer
from pretix.base.timemachine import time_machine_now
from pretix.helpers.names import build_name
@@ -165,13 +165,13 @@ class Membership(models.Model):
def is_valid(self, ev=None, ticket_valid_from=None, valid_from_not_chosen=False):
if valid_from_not_chosen:
return not self.canceled and self.date_end >= time_machine_now()
return not self.canceled and self.date_end >= now()
elif ticket_valid_from:
dt = ticket_valid_from
elif ev:
dt = ev.date_from
else:
dt = time_machine_now()
dt = now()
return not self.canceled and dt >= self.date_start and dt <= self.date_end
+12 -87
View File
@@ -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
@@ -81,7 +80,6 @@ from pretix.base.models import Customer, User
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import allow_ticket_download, order_gracefully_delete
from pretix.base.timemachine import time_machine_now
from ...helpers import OF_SELF
from ...helpers.countries import CachedCountries, FastCountryField
@@ -106,34 +104,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 +222,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 +284,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")
@@ -712,7 +681,7 @@ class Order(LockModel, LoggedModel):
for op in positions:
if op.issued_gift_cards.all():
return False
if self.user_change_deadline and time_machine_now() > self.user_change_deadline:
if self.user_change_deadline and now() > self.user_change_deadline:
return False
return (
@@ -744,7 +713,7 @@ class Order(LockModel, LoggedModel):
return False
if op.granted_memberships.with_usages().filter(usages__gt=0):
return False
if self.user_cancel_deadline and time_machine_now() > self.user_cancel_deadline:
if self.user_cancel_deadline and now() > self.user_cancel_deadline:
return False
if self.status == Order.STATUS_PAID:
@@ -881,11 +850,8 @@ 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:
if modify_deadline is not None and now() > modify_deadline:
return False
positions = list(
@@ -937,7 +903,7 @@ class Order(LockModel, LoggedModel):
return self.event.settings.ticket_download and (
self.event.settings.ticket_download_date is None
or self.ticket_download_date is None
or time_machine_now() > self.ticket_download_date
or now() > self.ticket_download_date
) and (
self.status == Order.STATUS_PAID
or (
@@ -1009,7 +975,7 @@ class Order(LockModel, LoggedModel):
return error_messages['require_approval']
term_last = self.payment_term_last
if term_last and not ignore_date:
if time_machine_now() > term_last:
if now() > term_last:
return error_messages['late_lastdate']
if self.status == self.STATUS_PENDING:
@@ -1032,7 +998,7 @@ class Order(LockModel, LoggedModel):
'voucher_budget': _('The voucher "{voucher}" no longer has sufficient budget.'),
'voucher_usages': _('The voucher "{voucher}" has been used in the meantime.'),
}
now_dt = now_dt or time_machine_now()
now_dt = now_dt or now()
positions = list(self.positions.all().select_related('item', 'variation', 'seat', 'voucher'))
quota_cache = {}
v_budget = {}
@@ -1253,10 +1219,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 +2513,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
@@ -2610,9 +2535,9 @@ class OrderPosition(AbstractPosition):
if cartpos.item.validity_mode:
valid_from, valid_until = cartpos.item.compute_validity(
requested_start=(
max(cartpos.requested_valid_from, time_machine_now())
max(cartpos.requested_valid_from, now())
if cartpos.requested_valid_from and cartpos.item.validity_dynamic_start_choice
else time_machine_now()
else now()
),
enforce_start_limit=True,
override_tz=order.event.timezone,
@@ -3138,9 +3063,9 @@ class CartPosition(AbstractPosition):
def predicted_validity(self):
return self.item.compute_validity(
requested_start=(
max(self.requested_valid_from, time_machine_now())
max(self.requested_valid_from, now())
if self.requested_valid_from and self.item.validity_dynamic_start_choice
else time_machine_now()
else now()
),
override_tz=self.event.timezone,
)
+5 -6
View File
@@ -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')
)
+8 -9
View File
@@ -57,7 +57,7 @@ from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from i18nfield.strings import LazyI18nString
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms import I18nMarkdownTextarea, PlaceholderValidator
from pretix.base.forms import PlaceholderValidator
from pretix.base.models import (
CartPosition, Event, GiftCard, InvoiceAddress, Order, OrderPayment,
OrderRefund, Quota, TaxRule,
@@ -67,7 +67,6 @@ from pretix.base.settings import SettingsSandbox
from pretix.base.signals import register_payment_providers
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.rich_text import rich_text
from pretix.base.timemachine import time_machine_now
from pretix.helpers import OF_SELF
from pretix.helpers.countries import CachedCountries
from pretix.helpers.format import format_map
@@ -1186,14 +1185,14 @@ class ManualPayment(BasePaymentProvider):
label=_('Payment process description during checkout'),
help_text=_('This text will be shown during checkout when the user selects this payment method. '
'It should give a short explanation on this payment method.'),
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)),
('email_instructions', I18nFormField(
label=_('Payment process description in order confirmation emails'),
help_text=_('This text will be included for the {payment_info} placeholder in order confirmation '
'mails. It should instruct the user on how to proceed with the payment. You can use '
'the placeholders {order}, {amount}, {currency} and {amount_with_currency}.'),
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
validators=[PlaceholderValidator(['{order}', '{amount}', '{currency}', '{amount_with_currency}'])],
)),
('pending_description', I18nFormField(
@@ -1201,7 +1200,7 @@ class ManualPayment(BasePaymentProvider):
help_text=_('This text will be shown on the order confirmation page for pending orders. '
'It should instruct the user on how to proceed with the payment. You can use '
'the placeholders {order}, {amount}, {currency} and {amount_with_currency}.'),
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
validators=[PlaceholderValidator(['{order}', '{amount}', '{currency}', '{amount_with_currency}'])],
)),
('invoice_immediately',
@@ -1326,7 +1325,7 @@ class GiftCardPayment(BasePaymentProvider):
(
"public_description",
I18nFormField(
label=_("Payment method description"), widget=I18nMarkdownTextarea, required=False
label=_("Payment method description"), widget=I18nTextarea, required=False
),
),
]
@@ -1442,7 +1441,7 @@ class GiftCardPayment(BasePaymentProvider):
if not gc.testmode and self.event.testmode:
messages.error(request, _("Only test gift cards can be used in test mode."))
return
if gc.expires and gc.expires < time_machine_now():
if gc.expires and gc.expires < now():
messages.error(request, _("This gift card is no longer valid."))
return
if gc.value <= Decimal("0.00"):
@@ -1492,7 +1491,7 @@ class GiftCardPayment(BasePaymentProvider):
if not gc.testmode and payment.order.testmode:
messages.error(request, _("Only test gift cards can be used in test mode."))
return
if gc.expires and gc.expires < time_machine_now():
if gc.expires and gc.expires < now():
messages.error(request, _("This gift card is no longer valid."))
return
if gc.value <= Decimal("0.00"):
@@ -1540,7 +1539,7 @@ class GiftCardPayment(BasePaymentProvider):
raise PaymentException(_("This gift card can only be used in test mode."))
if not gc.testmode and payment.order.testmode:
raise PaymentException(_("Only test gift cards can be used in test mode."))
if gc.expires and gc.expires < time_machine_now():
if gc.expires and gc.expires < now():
raise PaymentException(_("This gift card is no longer valid."))
trans = gc.transactions.create(
+84 -144
View File
@@ -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,81 +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)),
import_outline=False,
excluded_fields=("/Annots", "/B")
)
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),
import_outline=False,
excluded_fields=("/Annots", "/B")
)
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),
@@ -1154,66 +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),
import_outline=False,
excluded_fields=("/Annots", "/B")
)
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
-2
View File
@@ -257,8 +257,6 @@ def assign_ticket_secret(event, position, force_invalidate_if_revokation_list_us
kwargs['valid_from'] = position.valid_from
if 'valid_until' in params:
kwargs['valid_until'] = position.valid_until
if 'order_datetime' in params:
kwargs['order_datetime'] = position.order.datetime
secret = gen.generate_secret(
item=position.item,
variation=position.variation,
+27 -28
View File
@@ -74,7 +74,6 @@ from pretix.base.services.tasks import ProfiledEventTask
from pretix.base.settings import PERSON_NAME_SCHEMES, LazyI18nStringList
from pretix.base.signals import validate_cart_addons
from pretix.base.templatetags.rich_text import rich_text
from pretix.base.timemachine import time_machine_now, time_machine_now_assigned
from pretix.celery_app import app
from pretix.presale.signals import (
checkout_confirm_messages, fee_calculation_for_cart,
@@ -279,7 +278,7 @@ class CartManager:
sales_channel='web'):
self.event = event
self.cart_id = cart_id
self.real_now_dt = now()
self.now_dt = now()
self._operations = []
self._quota_diff = Counter()
self._voucher_use_diff = Counter()
@@ -306,10 +305,10 @@ class CartManager:
return self._seated_cache[item, subevent]
def _calculate_expiry(self):
self._expiry = self.real_now_dt + timedelta(minutes=self.event.settings.get('reservation_time', as_type=int))
self._expiry = self.now_dt + timedelta(minutes=self.event.settings.get('reservation_time', as_type=int))
def _check_presale_dates(self):
if self.event.presale_start and time_machine_now(self.real_now_dt) < self.event.presale_start:
if self.event.presale_start and self.now_dt < self.event.presale_start:
raise CartError(error_messages['not_started'])
if self.event.presale_has_ended:
raise CartError(error_messages['ended'])
@@ -320,13 +319,13 @@ class CartManager:
tlv.datetime(self.event).date(),
time(hour=23, minute=59, second=59)
), self.event.timezone)
if term_last < time_machine_now(self.real_now_dt):
if term_last < self.now_dt:
raise CartError(error_messages['payment_ended'])
def _extend_expiry_of_valid_existing_positions(self):
# Extend this user's cart session to ensure all items in the cart expire at the same time
# We can extend the reservation of items which are not yet expired without risk
self.positions.filter(expires__gt=self.real_now_dt).update(expires=self._expiry)
self.positions.filter(expires__gt=self.now_dt).update(expires=self._expiry)
def _delete_out_of_timeframe(self):
err = None
@@ -334,12 +333,12 @@ class CartManager:
if not cp.pk:
continue
if cp.subevent and cp.subevent.presale_start and time_machine_now(self.real_now_dt) < cp.subevent.presale_start:
if cp.subevent and cp.subevent.presale_start and self.now_dt < cp.subevent.presale_start:
err = error_messages['some_subevent_not_started']
cp.addons.all().delete()
cp.delete()
if cp.subevent and cp.subevent.presale_end and time_machine_now(self.real_now_dt) > cp.subevent.presale_end:
if cp.subevent and cp.subevent.presale_end and self.now_dt > cp.subevent.presale_end:
err = error_messages['some_subevent_ended']
cp.addons.all().delete()
cp.delete()
@@ -351,7 +350,7 @@ class CartManager:
tlv.datetime(cp.subevent).date(),
time(hour=23, minute=59, second=59)
), self.event.timezone)
if term_last < time_machine_now(self.real_now_dt):
if term_last < self.now_dt:
err = error_messages['some_subevent_ended']
cp.addons.all().delete()
cp.delete()
@@ -450,7 +449,7 @@ class CartManager:
if op.subevent and not op.subevent.active:
raise CartError(error_messages['inactive_subevent'])
if op.subevent and op.subevent.presale_start and time_machine_now(self.real_now_dt) < op.subevent.presale_start:
if op.subevent and op.subevent.presale_start and self.now_dt < op.subevent.presale_start:
raise CartError(error_messages['not_started'])
if op.subevent and op.subevent.presale_has_ended:
@@ -473,7 +472,7 @@ class CartManager:
tlv.datetime(op.subevent).date(),
time(hour=23, minute=59, second=59)
), self.event.timezone)
if term_last < time_machine_now(self.real_now_dt):
if term_last < self.now_dt:
raise CartError(error_messages['payment_ended'])
if isinstance(op, self.AddOperation):
@@ -510,7 +509,7 @@ class CartManager:
)
if not self.event.settings.seating_choice:
requires_seat = Value(0, output_field=IntegerField())
expired = self.positions.filter(expires__lte=self.real_now_dt).select_related(
expired = self.positions.filter(expires__lte=self.now_dt).select_related(
'item', 'variation', 'voucher', 'addon_to', 'addon_to__item'
).annotate(
requires_seat=requires_seat
@@ -691,7 +690,7 @@ class CartManager:
# than either of the possible default assumptions.
predicted_redeemed_after = (
voucher.redeemed +
CartPosition.objects.filter(voucher=voucher, expires__gte=self.real_now_dt).count() +
CartPosition.objects.filter(voucher=voucher, expires__gte=self.now_dt).count() +
self._voucher_use_diff[voucher] +
voucher_use_diff[voucher]
)
@@ -983,7 +982,7 @@ class CartManager:
current_num = len(current_addons[cp].get(k, []))
if input_num < current_num:
for a in current_addons[cp][k][:current_num - input_num]:
if a.expires > self.real_now_dt:
if a.expires > self.now_dt:
quotas = list(a.quotas)
for quota in quotas:
@@ -997,7 +996,7 @@ class CartManager:
def _get_voucher_availability(self):
vouchers_ok, self._voucher_depend_on_cart = _get_voucher_availability(
self.event, self._voucher_use_diff, self.real_now_dt,
self.event, self._voucher_use_diff, self.now_dt,
exclude_position_ids=[
op.position.id for op in self._operations if isinstance(op, self.ExtendOperation)
]
@@ -1102,7 +1101,7 @@ class CartManager:
shared_lock_objects=[self.event]
)
vouchers_ok = self._get_voucher_availability()
quotas_ok = _get_quota_availability(self._quota_diff, self.real_now_dt)
quotas_ok = _get_quota_availability(self._quota_diff, self.now_dt)
err = None
new_cart_positions = []
deleted_positions = set()
@@ -1119,7 +1118,7 @@ class CartManager:
for iop, op in enumerate(self._operations):
if isinstance(op, self.RemoveOperation):
if op.position.expires > self.real_now_dt:
if op.position.expires > self.now_dt:
for q in op.position.quotas:
quotas_ok[q] += 1
addons = op.position.addons.all()
@@ -1396,7 +1395,7 @@ class CartManager:
err = self.extend_expired_positions() or err
err = err or self._check_min_per_voucher()
self.real_now_dt = now()
self.now_dt = now()
self._extend_expiry_of_valid_existing_positions()
err = self._perform_operations() or err
@@ -1488,7 +1487,7 @@ def get_fees(event, request, total, invoice_address, payments, positions):
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, locale='en',
invoice_address: int=None, widget_data=None, sales_channel='web', override_now_dt: datetime=None) -> None:
invoice_address: int=None, widget_data=None, sales_channel='web') -> None:
"""
Adds a list of items to a user's cart.
:param event: The event ID in question
@@ -1496,7 +1495,7 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
:param cart_id: Session ID of a guest
:raises CartError: On any error that occurred
"""
with language(locale), time_machine_now_assigned(override_now_dt):
with language(locale):
ia = False
if invoice_address:
try:
@@ -1518,14 +1517,14 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='en', sales_channel='web') -> None:
"""
Removes a list of items from a user's cart.
:param event: The event ID in question
:param voucher: A voucher code
:param session: Session ID of a guest
"""
with language(locale), time_machine_now_assigned(override_now_dt):
with language(locale):
try:
try:
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
@@ -1538,14 +1537,14 @@ def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='e
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def remove_cart_position(self, event: Event, position: int, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
def remove_cart_position(self, event: Event, position: int, cart_id: str=None, locale='en', sales_channel='web') -> None:
"""
Removes a list of items from a user's cart.
:param event: The event ID in question
:param position: A cart position ID
:param session: Session ID of a guest
"""
with language(locale), time_machine_now_assigned(override_now_dt):
with language(locale):
try:
try:
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
@@ -1558,13 +1557,13 @@ def remove_cart_position(self, event: Event, position: int, cart_id: str=None, l
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel='web') -> None:
"""
Removes a list of items from a user's cart.
:param event: The event ID in question
:param session: Session ID of a guest
"""
with language(locale), time_machine_now_assigned(override_now_dt):
with language(locale):
try:
try:
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
@@ -1578,14 +1577,14 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, locale='en',
invoice_address: int=None, sales_channel='web', override_now_dt: datetime=None) -> None:
invoice_address: int=None, sales_channel='web') -> None:
"""
Removes a list of items from a user's cart.
:param event: The event ID in question
:param addons: A list of dicts with the keys addon_to, item, variation
:param session: Session ID of a guest
"""
with language(locale), time_machine_now_assigned(override_now_dt):
with language(locale):
ia = False
if invoice_address:
try:
+2 -38
View File
@@ -42,8 +42,8 @@ from dateutil.tz import datetime_exists
from django.core.files import File
from django.db import IntegrityError, transaction
from django.db.models import (
BooleanField, Case, Count, ExpressionWrapper, F, IntegerField, Max, Min,
OuterRef, Q, Subquery, TextField, Value, When,
BooleanField, Count, ExpressionWrapper, F, IntegerField, Max, Min,
OuterRef, Q, Subquery, Value,
)
from django.db.models.functions import Coalesce, TruncDate
from django.dispatch import receiver
@@ -273,14 +273,6 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
var_texts[vname] = _('Only allowed before {datetime}').format(datetime=compare_to_text)
elif operator == 'isAfter':
var_texts[vname] = _('Only allowed after {datetime}').format(datetime=compare_to_text)
elif var == 'entry_status':
var_weights[vname] = (20, 0)
if operator == '==' and rhs[0] == 'present':
var_texts[vname] = _('Attendee is checked out')
elif operator == '==' and rhs[0] == 'absent':
var_texts[vname] = _('Attendee is already checked in')
else:
var_texts[vname] = f'{var} not {operator} {rhs}'
elif var == 'product' or var == 'variation':
var_weights[vname] = (1000, 0)
var_texts[vname] = _('Ticket type not allowed')
@@ -515,13 +507,6 @@ class LazyRuleVars:
day=TruncDate('datetime', tzinfo=tz)
).values('day').distinct().count()
@cached_property
def entry_status(self):
last_checkin = self._position.checkins.filter(list=self._clist).order_by('datetime').last()
if not last_checkin or last_checkin.type == Checkin.TYPE_EXIT:
return "absent"
return "present"
@cached_property
def minutes_since_last_entry(self):
tz = self._clist.event.timezone
@@ -584,8 +569,6 @@ class SQLLogic:
'entries_days_since', 'entries_days_before'}
def operation_to_expression(self, rule):
if isinstance(rule, str):
return Value(rule)
if not isinstance(rule, dict):
return rule
@@ -787,25 +770,6 @@ class SQLLogic:
Value(-1),
output_field=IntegerField()
)
elif values[0] == 'entry_status':
sq_last_checkin = Subquery(
Checkin.objects.filter(
position_id=OuterRef('pk'),
list_id=self.list.pk,
).order_by('-datetime').values('type')[:1]
)
return Case(
When(
condition=Equal(
sq_last_checkin,
Value(Checkin.TYPE_ENTRY)
),
then=Value("present"),
),
default=Value("absent"),
output_field=TextField()
)
else:
raise ValueError(f'Unknown operator {operator}')
+1 -2
View File
@@ -470,8 +470,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
# just "ABC-123.pdf", but we only do so if our currently selected language allows to do this
# as ASCII text. For example, we would not want a "فاتورة_" prefix for our filename since this
# has shown to cause deliverability problems of the email and deliverability wins.
with language(order.locale if order else inv.locale, event.settings.region if event else None):
filename = pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf'
filename = pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf'
if not re.match("^[a-zA-Z0-9-_%./,&:# ]+$", filename):
filename = inv.number.replace(' ', '_') + '.pdf'
filename = re.sub("[^a-zA-Z0-9-_.]+", "_", filename)
+2 -2
View File
@@ -25,13 +25,13 @@ from typing import List, Optional
from dateutil.relativedelta import relativedelta
from django.core.exceptions import ValidationError
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from pretix.base.models import (
AbstractPosition, CartPosition, Customer, Event, Item, Membership, Order,
OrderPosition, SubEvent,
)
from pretix.base.timemachine import time_machine_now
from pretix.helpers import OF_SELF
@@ -48,7 +48,7 @@ def membership_validity(item: Item, subevent: Optional[SubEvent], event: Event):
else:
# Always start at start of day
date_start = time_machine_now().astimezone(tz).replace(hour=0, minute=0, second=0, microsecond=0)
date_start = now().astimezone(tz).replace(hour=0, minute=0, second=0, microsecond=0)
date_end = date_start
if item.grant_membership_duration_months:
-2
View File
@@ -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,
+15 -41
View File
@@ -99,10 +99,9 @@ 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
from pretix.helpers import OF_SELF
from pretix.helpers.models import modelcopy
@@ -200,7 +199,6 @@ error_messages = {
),
'addon_no_multi': gettext_lazy('You can select every add-on from the category %(cat)s for the product %(base)s at most once.'),
'addon_already_checked_in': gettext_lazy('You cannot remove the position %(addon)s since it has already been checked in.'),
'currency_XXX': gettext_lazy('Paid products not supported without a valid currency.'),
}
logger = logging.getLogger(__name__)
@@ -254,7 +252,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)
@@ -299,7 +297,6 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, valid_if_p
auth=auth,
data={
'expires': order.expires,
'force': force,
'state_change': was_expired
}
)
@@ -649,11 +646,10 @@ def _check_date(event: Event, now_dt: datetime):
raise OrderError(error_messages['ended'])
def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: datetime, positions: List[CartPosition],
address: InvoiceAddress = None,
def _check_positions(event: Event, now_dt: datetime, positions: List[CartPosition], address: InvoiceAddress=None,
sales_channel='web', customer=None):
err = None
_check_date(event, time_machine_now_dt)
_check_date(event, now_dt)
products_seen = Counter()
q_avail = Counter()
@@ -731,7 +727,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
delete(cp)
continue
if cp.subevent and cp.subevent.presale_start and time_machine_now_dt < cp.subevent.presale_start:
if cp.subevent and cp.subevent.presale_start and now_dt < cp.subevent.presale_start:
err = err or error_messages['some_subevent_not_started']
delete(cp)
break
@@ -743,7 +739,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
tlv.datetime(cp.subevent).date(),
time(hour=23, minute=59, second=59)
), event.timezone)
if term_last < time_machine_now_dt:
if term_last < now_dt:
err = err or error_messages['some_subevent_ended']
delete(cp)
break
@@ -789,19 +785,19 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
delete(cp)
continue
if cp.subevent and cp.item.pk in cp.subevent.item_overrides and not cp.subevent.item_overrides[cp.item.pk].is_available(time_machine_now_dt):
if cp.subevent and cp.item.pk in cp.subevent.item_overrides and not cp.subevent.item_overrides[cp.item.pk].is_available(now_dt):
err = err or error_messages['unavailable']
delete(cp)
continue
if cp.subevent and cp.variation and cp.variation.pk in cp.subevent.var_overrides and \
not cp.subevent.var_overrides[cp.variation.pk].is_available(time_machine_now_dt):
not cp.subevent.var_overrides[cp.variation.pk].is_available(now_dt):
err = err or error_messages['unavailable']
delete(cp)
continue
if cp.voucher:
if cp.voucher.valid_until and cp.voucher.valid_until < time_machine_now_dt:
if cp.voucher.valid_until and cp.voucher.valid_until < now_dt:
err = err or error_messages['voucher_expired']
delete(cp)
continue
@@ -1133,9 +1129,6 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
id__in=position_ids, event=event
)
if shown_total is not None and Decimal(shown_total) > Decimal("0.00") and event.currency == "XXX":
raise OrderError(error_messages['currency_XXX'])
validate_order.send(
event,
payment_provider=payment_requests[0]['provider'] if payment_requests else None, # only for backwards compatibility
@@ -1165,8 +1158,7 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
warnings = []
any_payment_failed = False
real_now_dt = now()
time_machine_now_dt = time_machine_now(real_now_dt)
now_dt = now()
err_out = None
with transaction.atomic(durable=True):
positions = list(
@@ -1178,15 +1170,14 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
if len(position_ids) != len(positions):
raise OrderError(error_messages['internal'])
try:
_check_positions(event, real_now_dt, time_machine_now_dt, positions,
address=addr, sales_channel=sales_channel, customer=customer)
_check_positions(event, now_dt, positions, address=addr, sales_channel=sales_channel, customer=customer)
except OrderError as e:
err_out = e # Don't raise directly to make sure transaction is committed, as it might have deleted things
else:
if 'sleep-after-quota-check' in debugflags_var.get():
sleep(2)
order, payment_objs = _create_order(event, email, positions, real_now_dt, payment_requests,
order, payment_objs = _create_order(event, email, positions, now_dt, payment_requests,
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel,
shown_total=shown_total, customer=customer, valid_if_pending=valid_if_pending)
@@ -2017,20 +2008,6 @@ class OrderChangeManager:
for a in current_addons[cp][k][:current_num - input_num]:
if a.canceled:
continue
is_unavailable = (
# If an item is no longer available due to time, it should usually also be no longer
# user-removable, because e.g. the stock has already been ordered.
# We always pass has_voucher=True because if a product now requires a voucher, it usually does
# not mean it should be unremovable for others.
# This also prevents accidental removal through the UI because a hidden product will no longer
# be part of the input.
(a.variation and a.variation.unavailability_reason(has_voucher=True, subevent=a.subevent))
or (a.variation and self.order.sales_channel not in a.variation.sales_channels)
or a.item.unavailability_reason(has_voucher=True, subevent=a.subevent)
or self.order.sales_channel not in item.sales_channels
)
if is_unavailable:
continue
if a.checkins.filter(list__consider_tickets_used=True).exists():
raise OrderError(
error_messages['addon_already_checked_in'] % {
@@ -2148,9 +2125,6 @@ class OrderChangeManager:
)
def _check_paid_to_free(self):
if self.event.currency == 'XXX' and self.order.total + self._totaldiff > Decimal("0.00"):
raise OrderError(error_messages['currency_XXX'])
if self.order.total == 0 and (self._totaldiff < 0 or (self.split_order and self.split_order.total > 0)) and not self.order.require_approval:
if not self.order.fees.exists() and not self.order.positions.exists():
# The order is completely empty now, so we cancel it.
@@ -2853,8 +2827,8 @@ class OrderChangeManager:
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
def perform_order(self, event: Event, payments: List[dict], positions: List[str],
email: str=None, locale: str=None, address: int=None, meta_info: dict=None,
sales_channel: str='web', shown_total=None, customer=None, override_now_dt: datetime=None):
with language(locale), time_machine_now_assigned(override_now_dt):
sales_channel: str='web', shown_total=None, customer=None):
with language(locale):
try:
try:
return _perform_order(event, payments, positions, email, locale, address, meta_info,
-34
View File
@@ -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:
+3 -3
View File
@@ -25,6 +25,7 @@ from typing import List, Optional, Tuple
from django import forms
from django.db.models import Q
from django.utils.timezone import now
from pretix.base.decimal import round_decimal
from pretix.base.models import (
@@ -32,7 +33,6 @@ from pretix.base.models import (
)
from pretix.base.models.event import Event, SubEvent
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
from pretix.base.timemachine import time_machine_now
def get_price(item: Item, variation: ItemVariation = None,
@@ -167,8 +167,8 @@ def apply_discounts(event: Event, sales_channel: str,
new_prices = {}
discount_qs = event.discounts.filter(
Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()),
Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()),
Q(available_from__isnull=True) | Q(available_from__lte=now()),
Q(available_until__isnull=True) | Q(available_until__gte=now()),
sales_channels__contains=sales_channel,
active=True,
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
-2
View File
@@ -29,7 +29,6 @@ from django.conf import settings
from django.db import models
from django.db.models import (
Case, Count, F, Func, Max, OuterRef, Q, Subquery, Sum, Value, When,
prefetch_related_objects,
)
from django.utils.timezone import now
@@ -447,7 +446,6 @@ class QuotaAvailability:
self.results[q] = Quota.AVAILABILITY_RESERVED, 0
def _compute_waitinglist(self, quotas, q_items, q_vars, size_left):
prefetch_related_objects(quotas, "event", "event__organizer")
quotas = [
q for q in quotas
if not q.event.settings.waiting_list_auto_disable or q.event.settings.waiting_list_auto_disable.datetime(q.subevent or q.event) > now()
+1 -4
View File
@@ -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 -30
View File
@@ -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
+22 -47
View File
@@ -64,7 +64,7 @@ from pretix.api.serializers.fields import (
ListMultipleChoiceField, UploadedFileField,
)
from pretix.api.serializers.i18n import I18nField, I18nURLField
from pretix.base.forms import I18nMarkdownTextarea, I18nURLFormField
from pretix.base.forms import I18nURLFormField
from pretix.base.models.tax import VAT_ID_COUNTRIES, TaxRule
from pretix.base.reldate import (
RelativeDateField, RelativeDateTimeField, RelativeDateWrapper,
@@ -602,7 +602,7 @@ DEFAULTS = {
'serializer_class': I18nField,
'form_kwargs': dict(
label=_("Invoice address explanation"),
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown above the invoice address form during checkout.")
)
@@ -801,7 +801,7 @@ DEFAULTS = {
'serializer_class': I18nField,
'form_kwargs': dict(
label=_("End of presale text"),
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown above the ticket shop once the designated sales timeframe for this event "
"is over. You can use it to describe other options to get a ticket, such as a box office.")
@@ -813,7 +813,7 @@ DEFAULTS = {
'form_class': I18nFormField,
'serializer_class': I18nField,
'form_kwargs': dict(
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
widget_kwargs={'attrs': {
'rows': 3,
}},
@@ -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')),
@@ -1460,7 +1459,7 @@ DEFAULTS = {
'serializer_class': I18nField,
'form_kwargs': dict(
label=_("Phone number explanation"),
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("If you ask for a phone number, explain why you do so and what you will use the phone number for.")
)
@@ -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': {
@@ -1901,7 +1876,7 @@ DEFAULTS = {
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Voluntary lower refund explanation"),
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown in between the explanation of how the refunds work and the slider "
"which your customers can use to choose the amount they would like to receive. You can use it "
@@ -1983,7 +1958,7 @@ DEFAULTS = {
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Terms of cancellation"),
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown when cancellation is allowed for a paid order. Leave empty if you "
"want pretix to automatically generate the terms of cancellation based on your settings.")
@@ -1996,7 +1971,7 @@ DEFAULTS = {
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Terms of cancellation"),
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown when cancellation is allowed for an unpaid or free order. Leave empty "
"if you want pretix to automatically generate the terms of cancellation based on your settings.")
@@ -2085,7 +2060,7 @@ DEFAULTS = {
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Event description"),
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
help_text=_(
"You can use this to share information with your attendees, such as travel information or the link to a digital event. "
"If you keep it empty, we will put a link to the event shop, the admission time, and your organizer name in there. "
@@ -3016,7 +2991,7 @@ Your {organizer} team""")) # noqa: W291
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Frontpage text"),
widget=I18nMarkdownTextarea,
widget=I18nTextarea
)
},
'event_info_text': {
@@ -3038,7 +3013,7 @@ Your {organizer} team""")) # noqa: W291
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Banner text (top)"),
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown above every page of your shop. Please only use this for "
"very important messages.")
@@ -3051,7 +3026,7 @@ Your {organizer} team""")) # noqa: W291
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Banner text (bottom)"),
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown below every page of your shop. Please only use this for "
"very important messages.")
@@ -3064,7 +3039,7 @@ Your {organizer} team""")) # noqa: W291
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Voucher explanation"),
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown next to the input for a voucher code. You can use it e.g. to explain "
"how to obtain a voucher code.")
@@ -3077,7 +3052,7 @@ Your {organizer} team""")) # noqa: W291
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Attendee data explanation"),
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown above the questions asked for every personalized product. You can use it e.g. to explain "
"why you need information from them.")
@@ -3093,7 +3068,7 @@ Your {organizer} team""")) # noqa: W291
help_text=_("This message will be shown after an order has been created successfully. It will be shown in additional "
"to the default text."),
widget_kwargs={'attrs': {'rows': '2'}},
widget=I18nMarkdownTextarea,
widget=I18nTextarea
)
},
'checkout_phone_helptext': {
@@ -3104,7 +3079,7 @@ Your {organizer} team""")) # noqa: W291
'form_kwargs': dict(
label=_("Help text of the phone number field"),
widget_kwargs={'attrs': {'rows': '2'}},
widget=I18nMarkdownTextarea,
widget=I18nTextarea
)
},
'checkout_email_helptext': {
@@ -3118,7 +3093,7 @@ Your {organizer} team""")) # noqa: W291
'form_kwargs': dict(
label=_("Help text of the email field"),
widget_kwargs={'attrs': {'rows': '2'}},
widget=I18nMarkdownTextarea,
widget=I18nTextarea
)
},
'order_import_settings': {
@@ -3248,7 +3223,7 @@ Your {organizer} team""")) # noqa: W291
'form_class': I18nFormField,
'form_kwargs': dict(
label=_('Homepage text'),
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
help_text=_('This will be displayed on the organizer homepage.')
)
},
@@ -3305,7 +3280,7 @@ Your {organizer} team""")) # noqa: W291
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Dialog text"),
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '3', 'data-display-dependency': '#id_settings-cookie_consent'}},
)
},
@@ -3320,7 +3295,7 @@ Your {organizer} team""")) # noqa: W291
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Secondary dialog text"),
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '3', 'data-display-dependency': '#id_settings-cookie_consent'}},
)
},
@@ -3444,7 +3419,7 @@ def concatenation_for_salutation(d):
salutation = pgettext("person_name_salutation", salutation)
given_name = None
return " ".join(str(p) for p in filter(None, (salutation, title, given_name, family_name)))
return " ".join(filter(None, (salutation, title, given_name, family_name)))
def get_name_parts_localized(name_parts, key):
@@ -1,7 +1,7 @@
{% load thumb %}
{% if widget.is_initial %}{{ widget.initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %}
<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}">
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}{% if widget.value.is_img %}<br><a href="{{ widget.value.url }}" data-lightbox="{{ widget.name }}"><img src="{{ widget.value.name|thumb:"200x100" }}" /></a>{% endif %}<br>
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}{% if widget.value.is_img %}<br><a href="{{ widget.value.url }}" data-lightbox="{{ widget.name }}"><img src="{{ widget.value|thumb:"200x100" }}" /></a>{% endif %}<br>
{{ widget.input_text }}:{% endif %}
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
{% if widget.cachedfile %}<input type="hidden" name="{{ widget.hidden_name }}" value="{{ widget.cachedfile.id }}">{% endif %}
-86
View File
@@ -1,86 +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 contextvars
from contextlib import contextmanager
from dateutil.parser import parse
from django.utils.timezone import now
timemachine_now_var = contextvars.ContextVar('timemachine_now', default=None)
@contextmanager
def time_machine_now_assigned_from_request(request):
if hasattr(request, 'event') and f'timemachine_now_dt:{request.event.pk}' in request.session and \
request.event.testmode and has_time_machine_permission(request, request.event):
request.now_dt = parse(request.session[f'timemachine_now_dt:{request.event.pk}'])
request.now_dt_is_fake = True
else:
request.now_dt = now()
request.now_dt_is_fake = False
try:
timemachine_now_var.set(request.now_dt if request.now_dt_is_fake else None)
yield
finally:
timemachine_now_var.set(None)
def time_machine_now(default=False):
"""
Return the datetime to use as current datetime for checking order restrictions in event
index and checkout flow.
:param default: Value to return if time machine mode is disabled. By default the current datetime is used.
"""
if default is False:
default = now()
return timemachine_now_var.get() or default
@contextmanager
def time_machine_now_assigned(now_dt):
"""
Use this context manager to assign current datetime for time machine mode. Useful e.g. for background tasks.
:param now_dt: The datetime value to assign. May be `None` to disable time machine.
"""
try:
timemachine_now_var.set(now_dt)
yield
finally:
timemachine_now_var.set(None)
def has_time_machine_permission(request, event):
permission = 'can_change_event_settings'
return (
request.user.is_authenticated and
request.user.has_event_permission(request.organizer, request.event, permission, request=request)
) 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)
)
+6 -4
View File
@@ -46,11 +46,11 @@ from pretix.base.settings import GlobalSettingsObject
from pretix.control.navigation import (
get_event_navigation, get_global_navigation, get_organizer_navigation,
)
from pretix.helpers.i18n import (
from ..helpers.i18n import (
get_javascript_format, get_javascript_output_format, get_moment_locale,
)
from pretix.multidomain.urlreverse import get_event_domain
from ..multidomain.urlreverse import get_event_domain
from .signals import html_head, nav_topbar
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
@@ -106,7 +106,7 @@ def _default_context(request):
else:
ctx['complain_testmode_orders'] = False
if (request.event.testmode or not request.event.live) and ctx['has_domain']:
if not request.event.live and ctx['has_domain']:
child_sess = request.session.get('child_session_{}'.format(request.event.pk))
s = SessionStore()
if not child_sess or not s.exists(child_sess):
@@ -114,8 +114,10 @@ def _default_context(request):
s.create()
ctx['new_session'] = s.session_key
request.session['child_session_{}'.format(request.event.pk)] = s.session_key
request.session['event_access'] = True
else:
ctx['new_session'] = child_sess
request.session['event_access'] = True
if request.GET.get('subevent', ''):
# Do not use .get() for lazy evaluation
+28 -31
View File
@@ -55,14 +55,12 @@ from django.utils.timezone import get_current_timezone_name
from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
from django_countries.fields import LazyTypedChoiceField
from i18nfield.forms import (
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextInput,
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput,
)
from pytz import common_timezones
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms import (
I18nMarkdownTextarea, I18nModelForm, PlaceholderValidator, SettingsForm,
)
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
from pretix.base.models import Event, Organizer, TaxRule, Team
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
@@ -580,7 +578,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',
@@ -991,7 +988,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_signature = I18nFormField(
label=_("Signature"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
help_text=_("This will be attached to every email. Available placeholders: {event}"),
validators=[PlaceholderValidator(['{event}'])],
widget_kwargs={'attrs': {
@@ -1014,7 +1011,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_placed = I18nFormField(
label=_("Text sent to order contact address"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
mail_send_order_placed_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
@@ -1030,7 +1027,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_placed_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
mail_subject_order_paid = I18nFormField(
@@ -1041,7 +1038,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_paid = I18nFormField(
label=_("Text sent to order contact address"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
mail_send_order_paid_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
@@ -1057,7 +1054,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_paid_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
mail_subject_order_free = I18nFormField(
@@ -1068,7 +1065,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_free = I18nFormField(
label=_("Text sent to order contact address"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
mail_send_order_free_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
@@ -1084,7 +1081,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_free_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
mail_subject_order_changed = I18nFormField(
@@ -1095,7 +1092,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_changed = I18nFormField(
label=_("Text"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
mail_subject_resend_link = I18nFormField(
label=_("Subject (sent by admin)"),
@@ -1110,7 +1107,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_resend_link = I18nFormField(
label=_("Text (sent by admin)"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
mail_subject_resend_all_links = I18nFormField(
label=_("Subject (requested by user)"),
@@ -1120,7 +1117,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_resend_all_links = I18nFormField(
label=_("Text (requested by user)"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
mail_days_order_expire_warning = forms.IntegerField(
label=_("Number of days"),
@@ -1132,7 +1129,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_expire_warning = I18nFormField(
label=_("Text (if order will expire automatically)"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
mail_subject_order_expire_warning = I18nFormField(
label=_("Subject (if order will expire automatically)"),
@@ -1142,7 +1139,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_pending_warning = I18nFormField(
label=_("Text (if order will not expire automatically)"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
mail_subject_order_pending_warning = I18nFormField(
label=_("Subject (if order will not expire automatically)"),
@@ -1157,7 +1154,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_incomplete_payment = I18nFormField(
label=_("Text"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
help_text=_("This email only applies to payment methods that can receive incomplete payments, "
"such as bank transfer."),
)
@@ -1169,7 +1166,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_payment_failed = I18nFormField(
label=_("Text"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
mail_subject_waiting_list = I18nFormField(
label=_("Subject"),
@@ -1179,7 +1176,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_waiting_list = I18nFormField(
label=_("Text"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
mail_subject_order_canceled = I18nFormField(
label=_("Subject"),
@@ -1189,12 +1186,12 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_canceled = I18nFormField(
label=_("Text"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
mail_text_order_custom_mail = I18nFormField(
label=_("Text"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
mail_subject_download_reminder = I18nFormField(
label=_("Subject sent to order contact address"),
@@ -1204,7 +1201,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_download_reminder = I18nFormField(
label=_("Text sent to order contact address"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
mail_send_download_reminder_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
@@ -1220,7 +1217,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_download_reminder_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
mail_days_download_reminder = forms.IntegerField(
label=_("Number of days"),
@@ -1237,7 +1234,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_placed_require_approval = I18nFormField(
label=_("Text for received order"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
mail_subject_order_approved = I18nFormField(
label=_("Subject for approved order"),
@@ -1247,7 +1244,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_approved = I18nFormField(
label=_("Text for approved order"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
help_text=_("This will only be sent out for non-free orders. Free orders will receive the free order "
"template from below instead."),
)
@@ -1265,7 +1262,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_approved_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
help_text=_("This will only be sent out for non-free orders. Free orders will receive the free order "
"template from below instead."),
)
@@ -1277,7 +1274,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_approved_free = I18nFormField(
label=_("Text for approved free order"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
help_text=_("This will only be sent out for free orders. Non-free orders will receive the non-free order "
"template from above instead."),
)
@@ -1295,7 +1292,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_approved_free_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
help_text=_("This will only be sent out for free orders. Non-free orders will receive the non-free order "
"template from above instead."),
)
@@ -1307,7 +1304,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_text_order_denied = I18nFormField(
label=_("Text for denied order"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
base_context = {
'mail_text_order_placed': ['event', 'order', 'payments'],
@@ -1740,7 +1737,7 @@ class ItemMetaPropertyForm(forms.ModelForm):
class ConfirmTextForm(I18nForm):
text = I18nFormField(
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
)
+1 -1
View File
@@ -1231,7 +1231,7 @@ class SubEventFilterForm(FilterForm):
if fdata.get('query'):
query = fdata.get('query')
qs = qs.filter(
Q(name__icontains=i18ncomp(query)) | Q(location__icontains=query) | Q(comment__icontains=query)
Q(name__icontains=i18ncomp(query)) | Q(location__icontains=query)
)
if fdata.get('date_until'):
+4 -6
View File
@@ -36,12 +36,10 @@ from collections import OrderedDict
from django import forms
from django.utils.translation import gettext_lazy as _
from i18nfield.forms import I18nFormField, I18nTextInput
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from pretix import settings
from pretix.base.forms import (
I18nMarkdownTextarea, SecretKeySettingsField, SettingsForm,
)
from pretix.base.forms import SecretKeySettingsField, SettingsForm
from pretix.base.settings import GlobalSettingsObject
from pretix.base.signals import register_global_settings
@@ -69,12 +67,12 @@ class GlobalSettingsForm(SettingsForm):
help_text=_("Will be included as the link in the additional footer text.")
)),
('banner_message', I18nFormField(
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
required=False,
label=_("Global message banner"),
)),
('banner_message_detail', I18nFormField(
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
required=False,
label=_("Global message banner detail text"),
)),
+1 -14
View File
@@ -56,7 +56,7 @@ from django_scopes.forms import (
from i18nfield.forms import I18nFormField, I18nTextarea
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms import I18nFormSet, I18nMarkdownTextarea, I18nModelForm
from pretix.base.forms import I18nFormSet, I18nModelForm
from pretix.base.forms.widgets import DatePickerWidget
from pretix.base.models import (
Item, ItemCategory, ItemVariation, Question, QuestionOption, Quota,
@@ -82,9 +82,6 @@ class CategoryForm(I18nModelForm):
'description',
'is_addon'
]
widgets = {
'description': I18nMarkdownTextarea,
}
class QuestionForm(I18nModelForm):
@@ -191,7 +188,6 @@ class QuestionForm(I18nModelForm):
attrs={'class': 'scrolling-multiple-choice'}
),
'dependency_values': forms.SelectMultiple,
'help_text': I18nMarkdownTextarea,
}
field_classes = {
'valid_datetime_min': SplitDateTimeField,
@@ -698,14 +694,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',
@@ -835,7 +823,6 @@ class ItemUpdateForm(I18nModelForm):
'max_per_order': forms.widgets.NumberInput(attrs={'min': 0}),
'min_per_order': forms.widgets.NumberInput(attrs={'min': 0}),
'checkin_text': forms.TextInput(),
'description': I18nMarkdownTextarea,
}
+6 -9
View File
@@ -47,14 +47,11 @@ from django.utils.translation import (
gettext_lazy as _, gettext_noop, pgettext_lazy,
)
from django_scopes.forms import SafeModelChoiceField
from i18nfield.forms import I18nFormField, I18nTextInput
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from i18nfield.strings import LazyI18nString
from pretix.base.email import get_available_placeholders
from pretix.base.forms import (
I18nMarkdownTextarea, I18nModelForm, MarkdownTextarea,
PlaceholderValidator,
)
from pretix.base.forms import I18nModelForm, PlaceholderValidator
from pretix.base.forms.questions import WrappedPhoneNumberPrefixWidget
from pretix.base.forms.widgets import (
DatePickerWidget, SplitDateTimePickerWidget, format_placeholders_help_text,
@@ -702,7 +699,7 @@ class OrderMailForm(forms.Form):
self.fields['message'] = forms.CharField(
label=_("Message"),
required=True,
widget=MarkdownTextarea,
widget=forms.Textarea,
initial=order.event.settings.mail_text_order_custom_mail.localize(order.locale),
)
self.fields['attach_invoices'].queryset = order.invoices.all()
@@ -719,7 +716,7 @@ class OrderPositionMailForm(OrderMailForm):
self.fields['message'] = forms.CharField(
label=_("Message"),
required=True,
widget=MarkdownTextarea,
widget=forms.Textarea,
initial=self.order.event.settings.mail_text_order_custom_mail.localize(self.order.locale),
)
self._set_field_placeholders('message', ['event', 'order', 'position'])
@@ -886,7 +883,7 @@ class EventCancelForm(FormPlaceholderMixin, forms.Form):
)
self.fields['send_message'] = I18nFormField(
label=_('Message'),
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
required=True,
widget_kwargs={'attrs': {'data-display-dependency': '#id_send'}},
locales=self.event.settings.get('locales'),
@@ -913,7 +910,7 @@ class EventCancelForm(FormPlaceholderMixin, forms.Form):
)
self.fields['send_waitinglist_message'] = I18nFormField(
label=_('Message'),
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
required=True,
locales=self.event.settings.get('locales'),
widget_kwargs={'attrs': {'data-display-dependency': '#id_send_waitinglist'}},
+6 -8
View File
@@ -48,7 +48,7 @@ from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes.forms import SafeModelChoiceField
from i18nfield.forms import (
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextInput,
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput,
)
from phonenumber_field.formfields import PhoneNumberField
from pytz import common_timezones
@@ -56,9 +56,7 @@ from pytz import common_timezones
from pretix.api.models import WebHook
from pretix.api.webhooks import get_all_webhook_events
from pretix.base.customersso.oidc import oidc_validate_and_complete_config
from pretix.base.forms import (
I18nMarkdownTextarea, I18nModelForm, PlaceholderValidator, SettingsForm,
)
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
from pretix.base.forms.questions import (
NamePartsFormField, WrappedPhoneNumberPrefixWidget, get_country_by_locale,
get_phone_prefix,
@@ -526,7 +524,7 @@ class MailSettingsForm(SettingsForm):
mail_text_signature = I18nFormField(
label=_("Signature"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
help_text=_("This will be attached to every email."),
validators=[PlaceholderValidator([])],
widget_kwargs={'attrs': {
@@ -545,7 +543,7 @@ class MailSettingsForm(SettingsForm):
mail_text_customer_registration = I18nFormField(
label=_("Text"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
mail_subject_customer_email_change = I18nFormField(
label=_("Subject"),
@@ -555,7 +553,7 @@ class MailSettingsForm(SettingsForm):
mail_text_customer_email_change = I18nFormField(
label=_("Text"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
mail_subject_customer_reset = I18nFormField(
label=_("Subject"),
@@ -565,7 +563,7 @@ class MailSettingsForm(SettingsForm):
mail_text_customer_reset = I18nFormField(
label=_("Text"),
required=False,
widget=I18nMarkdownTextarea,
widget=I18nTextarea,
)
base_context = {
+1 -3
View File
@@ -68,7 +68,6 @@ class SubEventForm(I18nModelForm):
'presale_end',
'location',
'frontpage_text',
'comment',
'geo_lat',
'geo_lon',
]
@@ -137,7 +136,7 @@ class SubEventBulkEditForm(I18nModelForm):
self.fields[k].widget.attrs['placeholder'] = ''
self.fields[k].one_required = False
for k in ('geo_lat', 'geo_lon', 'comment'):
for k in ('geo_lat', 'geo_lon'):
# scalar fields
if k in self.mixed_values:
self.fields[k].widget.attrs['placeholder'] = '[{}]'.format(_('Selection contains various values'))
@@ -167,7 +166,6 @@ class SubEventBulkEditForm(I18nModelForm):
'name',
'location',
'frontpage_text',
'comment',
'geo_lat',
'geo_lon',
'is_public',
+10 -12
View File
@@ -45,9 +45,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes.forms import SafeModelChoiceField
from pretix.base.email import get_available_placeholders
from pretix.base.forms import (
I18nModelForm, MarkdownTextarea, PlaceholderValidator,
)
from pretix.base.forms import I18nModelForm, PlaceholderValidator
from pretix.base.forms.widgets import format_placeholders_help_text
from pretix.base.models import Item, Voucher
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
@@ -90,10 +88,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 +99,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 +232,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'):
@@ -273,7 +271,7 @@ class VoucherBulkForm(VoucherForm):
)
send_message = forms.CharField(
label=_("Message"),
widget=MarkdownTextarea(attrs={'data-display-dependency': '#id_send'}),
widget=forms.Textarea(attrs={'data-display-dependency': '#id_send'}),
required=False,
initial=_('Hello,\n\n'
'with this email, we\'re sending you one or more vouchers for {event}:\n\n{voucher_list}\n\n'
@@ -366,14 +364,14 @@ class VoucherBulkForm(VoucherForm):
raise ValidationError(_('CSV input contains an unknown field with the header "{header}".').format(header=unknown_fields[0]))
for i, row in enumerate(reader):
try:
EmailValidator()(row['email'].strip())
EmailValidator()(row['email'])
except ValidationError as err:
raise ValidationError(_('{value} is not a valid email address.').format(value=row['email'].strip())) from err
raise ValidationError(_('{value} is not a valid email address.').format(value=row['email'])) from err
try:
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:
-5
View File
@@ -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'),
},
]
})
-307
View File
@@ -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,
@@ -28,15 +28,7 @@
<dt>{% trans "Receipt number" context "terminal_zvt" %}</dt>
<dd>{{ payment_info.payment_data.receiptNumber }}</dd>
<dt>{% trans "Card type" context "terminal_zvt" %}</dt>
<dd>
{% if payment_info.payment_data.cardName %}
{{ payment_info.payment_data.cardName }}
{% elif payment_info.payment_data.cardType %}
{{ payment_info.payment_data.cardType }}
{% else %}
{% trans "Unknown" %}
{% endif %}
</dd>
<dd>{{ payment_info.payment_data.cardName|default_if_none:payment_info.payment_data.cardType }}</dd>
<dt>{% trans "Card expiration" context "terminal_zvt" %}</dt>
<dd>{{ payment_info.payment_data.expiry }}</dd>
{% endif %}
@@ -46,7 +46,7 @@
{% endfor %}
</ul>
</div>
<div class="text-right">
<div class="test-right">
<button type="submit" class="btn btn-primary btn-lg btn-save" disabled>
{% trans "Go live" %}
</button>
@@ -82,10 +82,10 @@
<p>
{% trans "Your shop is currently in test mode. All orders are not persistent and can be deleted at any point." %}
</p>
<div class="checkbox">
<label>
<div class="form-inline">
<label class="checkbox">
<input type="checkbox" name="delete" value="yes" />
<b>{% trans "Permanently delete all orders created in test mode" %}</b>
{% trans "Permanently delete all orders created in test mode" %}
</label>
</div>
<div class="text-right">
@@ -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,9 +0,0 @@
{% load eventurl %}
{% load static %}
<form action="{% eventurl request.event "presale:event.auth" %}{% if request.GET.next %}?next={{ request.GET.next }}{% endif %}" method="post">
<input type="hidden" value="{{ new_session }}" name="session">
<button type="submit">
Continue
</button>
</form>
<script src="{% static "pretixcontrol/js/send_form.js" %}"></script>
@@ -19,8 +19,8 @@
section of your website:
{% endblocktrans %}
</p>
<pre>&lt;link rel="stylesheet" type="text/css" href="{% abseventurl request.event "presale:event.widget.css" %}" crossorigin&gt;
&lt;script type="text/javascript" src="{{ urlprefix }}{% url "presale:widget.js" lang=form.cleaned_data.language %}" async crossorigin&gt;&lt;/script&gt;</pre>
<pre>&lt;link rel="stylesheet" type="text/css" href="{% abseventurl request.event "presale:event.widget.css" %}"&gt;
&lt;script type="text/javascript" src="{{ urlprefix }}{% url "presale:widget.js" lang=form.cleaned_data.language %}" async&gt;&lt;/script&gt;</pre>
<p>
{% blocktrans trimmed %}
Then, copy the following code to the place of your website where you want the widget to show up:
@@ -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 %}
@@ -32,6 +32,7 @@
<tr>
<th>{% trans "Product categories" %}</th>
<th class="action-col-2"></th>
<th class="action-col-2"></th>
</tr>
</thead>
<tbody data-dnd-url="{% url "control:event.items.categories.reorder" organizer=request.event.organizer.slug event=request.event.slug %}">
@@ -40,16 +41,18 @@
<td>
<strong><a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}">{{ c.internal_name|default:c.name }}</a></strong>
</td>
<td>
<button formaction="{% url "control:event.items.categories.up" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button formaction="{% url "control:event.items.categories.down" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container"></span>
</td>
<td class="text-right flip">
<button title="{% trans "Move up" %}" formaction="{% url "control:event.items.categories.up" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button title="{% trans "Move down" %}" formaction="{% url "control:event.items.categories.down" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
<a title="{% trans "Edit" %}" href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ c.id }}"
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>
</a>
<a title="{% trans "Delete" %}" href="{% url "control:event.items.categories.delete" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
<a href="{% url "control:event.items.categories.delete" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
</td>
</tr>
{% endfor %}
@@ -56,7 +56,8 @@
<th>{% trans "Internal name" %}</th>
<th></th>
<th></th>
<th colspan="2">{% trans "Products" %}</th>
<th>{% trans "Products" %}</th>
<th class="action-col-2"></th>
<th class="action-col-2"></th>
</tr>
</thead>
@@ -101,10 +102,9 @@
{% endif %}
{% endif %}
</td>
<td {% if d.benefit_same_products %}colspan="2"{% endif %}>
{% if not d.benefit_same_products %}{% trans "Condition:" %}{% endif %}
<td>
{% if d.condition_all_products %}
<ul><li><em>{% trans "All" %}</em></li></ul>
<em>{% trans "All" %}</em>
{% else %}
<ul>
{% for item in d.condition_limit_products.all %}
@@ -115,28 +115,18 @@
</ul>
{% endif %}
</td>
{% if not d.benefit_same_products %}
<td>
{% trans "Applies to:" %}
<ul>
{% for item in d.benefit_limit_products.all %}
<li>
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a>
</li>
{% endfor %}
</ul>
</td>
{% endif %}
<td class="text-right flip">
<td>
<button formaction="{% url "control:event.items.discounts.up" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-default btn-sm sortable-up" title="{% trans "Move up" %}"
class="btn btn-default btn-sm sortable-up"
{% if forloop.counter0 == 0 and not page_obj.has_previous %}
disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button formaction="{% url "control:event.items.discounts.down" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-default btn-sm sortable-down" title="{% trans "Move down" %}"
class="btn btn-default btn-sm sortable-down"
{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}>
<i class="fa fa-arrow-down"></i></button>
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
<span class="dnd-container"></span>
</td>
<td class="text-right flip">
<a href="{% url "control:event.items.discounts.edit" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ d.id }}"
@@ -1,15 +1,13 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load money %}
{% block title %}{% trans "Products" %}{% endblock %}
{% block inside %}
{% blocktrans asvar s_taxes %}taxes{% endblocktrans %}
<h1>{% trans "Products" %}</h1>
<p>
{% blocktrans trimmed %}
Below, you find a list of all available products. You can click on a product name to inspect and change
product details. You can also use the buttons on the right to change the order of products or move
products to a different category.
product details. You can also use the buttons on the right to change the order of products within a
give category.
{% endblocktrans %}
</p>
{% if items|length == 0 %}
@@ -31,7 +29,7 @@
<form method="post">
{% csrf_token %}
<div class="table-responsive">
<table class="table table-condensed table-hover table-items">
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "Product name" %}</th>
@@ -39,24 +37,16 @@
<th class="iconcol"></th>
<th class="iconcol"></th>
<th class="iconcol"></th>
<th class="text-right flip">{% trans "Default price" %}</th>
<th>{% trans "Category" %}</th>
<th class="action-col-2"><span class="sr-only">Move</span></th>
<th class="action-col-2"><span class="sr-only">Edit</span></th>
</tr>
</thead>
{% for c, items in cat_list %}
{% if c %}
<tbody>
<tr class="sortable-disabled"><th colspan="9" scope="colgroup" class="text-muted">
{{ c.name }}{% if c.category_type != "normal" %} <span class="font-normal">({{ c.get_category_type_display }})</span>{% endif %}
<a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" title="{% trans "Edit" %}"><span class="fa fa-edit fa-fw"></span></a>
</th></tr>
</tbody>
{% endif %}
<tbody data-dnd-url="{% url "control:event.items.reorder" organizer=request.event.organizer.slug event=request.event.slug category=c.id|default:0 %}"
data-dnd-group="items">
{% for i in items %}
{% if forloop.counter0 == 0 and i.category %}{% endif %}
{% regroup items by category as cat_list %}
{% for c in cat_list %}
<tbody data-dnd-url="{% url "control:event.items.reorder" organizer=request.event.organizer.slug event=request.event.slug %}">
{% for i in c.list %}
{% if forloop.counter0 == 0 and i.category %}<tr class="sortable-disabled"><th colspan="8" scope="colgroup" class="text-muted">{{ i.category }}</th></tr>{% endif %}
<tr data-dnd-id="{{ i.id }}" {% if not i.active %}class="row-muted"{% endif %}>
<td><strong>
{% if not i.active %}<strike>{% endif %}
@@ -102,15 +92,15 @@
</td>
<td>
{% if i.var_count %}
<span class="fa fa-bars fa-fw text-muted" data-toggle="tooltip" title="{% trans "Product with variations" %}"></span>
<span class="fa fa-th-large fa-fw text-muted" data-toggle="tooltip" title="{% trans "Product with variations" %}"></span>
{% endif %}
</td>
<td>
{% if i.category.is_addon %}
<span class="fa fa-plus-square fa-fw text-muted" data-toggle="tooltip"
<span class="fa fa-puzzle-piece fa-fw text-muted" data-toggle="tooltip"
title="{% trans "Only available as an add-on product" %}"></span>
{% elif i.require_bundling %}
<span class="fa fa-plus-square fa-fw text-muted" data-toggle="tooltip"
<span class="fa fa-puzzle-piece fa-fw text-muted" data-toggle="tooltip"
title="{% trans "Only available as part of a bundle" %}"></span>
{% elif i.hide_without_voucher %}
<span class="fa fa-tags fa-fw text-muted" data-toggle="tooltip"
@@ -120,29 +110,16 @@
title="{% trans "Can only be bought using a voucher" %}"></span>
{% endif %}
</td>
<td class="text-right flip">
{% if i.free_price %}
<span class="fa fa-edit fa-fw text-muted" data-toggle="tooltip" title="{% trans "Free price input" %}">
</span>
{% endif %}
{{ i.default_price|money:request.event.currency }}
{% if i.original_price %}<strike class="text-muted">{{ i.original_price|money:request.event.currency }}</strike>{% endif %}
{% if i.tax_rule and i.default_price %}
<br/>
<small class="text-muted">
{% blocktrans trimmed with rate=i.tax_rule.rate|floatformat:-2 taxname=i.tax_rule.name|default:s_taxes %}
incl. {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
{% endif %}
<td>{% if i.category %}{{ i.category.name }}{% endif %}</td>
<td>
<button formaction="{% url "control:event.items.up" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button formaction="{% url "control:event.items.down" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container"></span>
</td>
<td class="text-right flip col-actions">
<button title="{% trans "Move up" %}" formaction="{% url "control:event.items.up" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button title="{% trans "Move down" %}" formaction="{% url "control:event.items.down" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm" title="{% trans "Edit" %}"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ i.id }}" class="btn btn-default btn-sm" title="{% trans "Clone" %}" data-toggle="tooltip"><i class="fa fa-copy"></i></a>
<a href="{% url "control:event.items.delete" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-danger btn-sm" title="{% trans "Delete" %}"><i class="fa fa-trash"></i></a>
<a href="{% url "control:event.items.delete" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
</td>
</tr>
{% endfor %}
@@ -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>
@@ -51,9 +51,9 @@
{% endif %}
</td>
<td>
<button title="{% trans "Move up" %}" formaction="{% url "control:organizer.property.up" organizer=request.organizer.slug property=p.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button title="{% trans "Move down" %}" formaction="{% url "control:organizer.property.down" organizer=request.organizer.slug property=p.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
<button formaction="{% url "control:organizer.property.up" organizer=request.organizer.slug property=p.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button formaction="{% url "control:organizer.property.down" organizer=request.organizer.slug property=p.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container"></span>
</td>
<td class="text-right flip">
<a href="{% url "control:organizer.property.edit" organizer=request.organizer.slug property=p.id %}"
@@ -70,7 +70,6 @@
</div>
{% bootstrap_field form.frontpage_text layout="bulkedit" %}
{% bootstrap_field form.is_public layout="bulkedit" %}
{% bootstrap_field form.comment layout="bulkedit" %}
{% if meta_forms %}
<div class="form-group metadata-group">
<label class="col-md-3 control-label">{% trans "Meta data" %}</label>
@@ -29,7 +29,6 @@
{% bootstrap_field form.date_admission layout="control" %}
{% bootstrap_field form.frontpage_text layout="control" %}
{% bootstrap_field form.is_public layout="control" %}
{% bootstrap_field form.comment layout="control" %}
{% if meta_forms %}
<div class="form-group metadata-group">
<label class="col-md-3 control-label">{% trans "Meta data" %}</label>
@@ -138,9 +138,6 @@
<small class="text-muted">&middot; {{ k }}: {{ v }}</small>
{% endif %}
{% endfor %}
{% if s.comment %}
<br>{{ s.comment|linebreaksbr }}
{% endif %}
</small>
</td>
<td>
@@ -173,8 +170,6 @@
{% endif %}
</td>
<td class="text-right flip">
<a href="{% url "control:event.orders" organizer=request.event.organizer.slug event=request.event.slug %}?subevent={{ s.id }}" class="btn btn-default btn-sm" title="{% trans "Show orders" %}"><i class="fa fa-shopping-cart" aria-hidden="true"></i></a>
<a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<div class="btn-group {% if forloop.revcounter0 < 2 %}dropup{% endif %}">
<button type="button" class="btn btn-default btn-sm dropdown-toggle"
@@ -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>
+1 -3
View File
@@ -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'),
@@ -248,7 +247,6 @@ urlpatterns = [
re_path(r'^widgets.json$', dashboards.event_index_widgets_lazy, name='event.index.widgets'),
re_path(r'^logs/embed$', dashboards.event_index_log_lazy, name='event.index.logs'),
re_path(r'^live/$', event.EventLive.as_view(), name='event.live'),
re_path(r'^transfer_session/$', event.EventTransferSession.as_view(), name='event.transfer_session'),
re_path(r'^logs/$', event.EventLog.as_view(), name='event.log'),
re_path(r'^delete/$', event.EventDelete.as_view(), name='event.delete'),
re_path(r'^comment/$', event.EventComment.as_view(),
@@ -293,7 +291,7 @@ urlpatterns = [
re_path(r'^items/(?P<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
re_path(r'^items/(?P<item>\d+)/up$', item.item_move_up, name='event.items.up'),
re_path(r'^items/(?P<item>\d+)/down$', item.item_move_down, name='event.items.down'),
re_path(r'^items/reorder/(?P<category>\d+)/$', item.reorder_items, name='event.items.reorder'),
re_path(r'^items/reorder$', item.reorder_items, name='event.items.reorder'),
re_path(r'^items/(?P<item>\d+)/delete$', item.ItemDelete.as_view(), name='event.items.delete'),
re_path(r'^items/typeahead/meta/$', typeahead.item_meta_values, name='event.items.meta.typeahead'),
re_path(r'^items/select2$', typeahead.items_select2, name='event.items.select2'),
+12 -22
View File
@@ -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,
@@ -1022,11 +1017,6 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
})
class EventTransferSession(EventPermissionRequiredMixin, TemplateView):
permission = 'can_change_event_settings'
template_name = 'pretixcontrol/event/transfer_session.html'
class EventDelete(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, FormView):
permission = 'can_change_event_settings'
template_name = 'pretixcontrol/event/delete.html'
+1 -26
View File
@@ -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
+11 -14
View File
@@ -35,7 +35,6 @@
import json
from collections import OrderedDict, namedtuple
from itertools import groupby
from json.decoder import JSONDecodeError
from django.contrib import messages
@@ -114,8 +113,6 @@ class ItemList(ListView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['sales_channels'] = get_all_sales_channels()
items_by_category = {cat: list(items) for cat, items in groupby(ctx['items'], lambda item: item.category)}
ctx['cat_list'] = [(cat, items_by_category.get(cat, [])) for cat in [None, *self.request.event.categories.all()]]
return ctx
@@ -172,7 +169,7 @@ def item_move_down(request, organizer, event, item):
@transaction.atomic
@event_permission_required("can_change_items")
@require_http_methods(["POST"])
def reorder_items(request, organizer, event, category):
def reorder_items(request, organizer, event):
try:
ids = json.loads(request.body.decode('utf-8'))['ids']
except (JSONDecodeError, KeyError, ValueError):
@@ -183,21 +180,23 @@ def reorder_items(request, organizer, event, category):
if len(input_items) != len(ids):
raise Http404(_("Some of the provided object ids are invalid."))
if int(category):
target_category = request.event.categories.get(id=category)
else:
target_category = None
item_categories = {i.category_id for i in input_items}
if len(item_categories) > 1:
raise Http404(_("You cannot reorder items spanning different categories."))
# get first and only category
item_category = next(iter(item_categories))
if len(input_items) != request.event.items.filter(category=item_category).count():
raise Http404(_("Not all objects have been selected."))
for i in input_items:
pos = ids.index(str(i.pk))
if pos != i.position or target_category != i.category: # Save unneccessary UPDATE queries
if pos != i.position: # Save unneccessary UPDATE queries
i.position = pos
i.category = target_category
i.save(update_fields=['position', 'category_id'])
i.save(update_fields=['position'])
i.log_action(
'pretix.event.item.reordered', user=request.user, data={
'position': i,
'category': target_category and target_category.pk,
}
)
@@ -1324,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:
+276 -296
View File
@@ -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
@@ -129,7 +129,6 @@ from pretix.control.forms.rrule import RRuleForm
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import order_search_forms
from pretix.control.views import PaginationMixin
from pretix.helpers import OF_SELF
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.format import format_map
from pretix.helpers.safedownload import check_token
@@ -1104,9 +1103,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 +1375,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()
@@ -1578,22 +1562,20 @@ class OrderInvoiceCreate(OrderView):
permission = 'can_change_orders'
def post(self, *args, **kwargs):
with transaction.atomic():
order = Order.objects.select_for_update(of=OF_SELF).get(pk=self.order.pk)
has_inv = order.invoices.exists() and not (
order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
and order.invoices.filter(is_cancellation=True).count() >= order.invoices.filter(is_cancellation=False).count()
)
if self.request.event.settings.get('invoice_generate') not in ('admin', 'user', 'paid', 'True') or not invoice_qualified(order):
messages.error(self.request, _('You cannot generate an invoice for this order.'))
elif has_inv:
messages.error(self.request, _('An invoice for this order already exists.'))
else:
inv = generate_invoice(order)
order.log_action('pretix.event.order.invoice.generated', user=self.request.user, data={
'invoice': inv.pk
})
messages.success(self.request, _('The invoice has been generated.'))
has_inv = self.order.invoices.exists() and not (
self.order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
and self.order.invoices.filter(is_cancellation=True).count() >= self.order.invoices.filter(is_cancellation=False).count()
)
if self.request.event.settings.get('invoice_generate') not in ('admin', 'user', 'paid', 'True') or not invoice_qualified(self.order):
messages.error(self.request, _('You cannot generate an invoice for this order.'))
elif has_inv:
messages.error(self.request, _('An invoice for this order already exists.'))
else:
inv = generate_invoice(self.order)
self.order.log_action('pretix.event.order.invoice.generated', user=self.request.user, data={
'invoice': inv.pk
})
messages.success(self.request, _('The invoice has been generated.'))
return redirect(self.get_order_url())
def get(self, *args, **kwargs):
@@ -1675,27 +1657,25 @@ class OrderInvoiceReissue(OrderView):
permission = 'can_change_orders'
def post(self, *args, **kwargs):
with transaction.atomic():
order = Order.objects.select_for_update(of=OF_SELF).get(pk=self.order.pk)
try:
inv = order.invoices.get(pk=kwargs.get('id'))
except Invoice.DoesNotExist:
messages.error(self.request, _('Unknown invoice.'))
try:
inv = self.order.invoices.get(pk=kwargs.get('id'))
except Invoice.DoesNotExist:
messages.error(self.request, _('Unknown invoice.'))
else:
if inv.canceled:
messages.error(self.request, _('The invoice has already been canceled.'))
elif inv.shredded:
messages.error(self.request, _('The invoice has been cleaned of personal data.'))
else:
if inv.canceled:
messages.error(self.request, _('The invoice has already been canceled.'))
elif inv.shredded:
messages.error(self.request, _('The invoice has been cleaned of personal data.'))
c = generate_cancellation(inv)
if self.order.status not in (Order.STATUS_CANCELED, Order.STATUS_EXPIRED):
inv = generate_invoice(self.order)
else:
c = generate_cancellation(inv)
if order.status not in (Order.STATUS_CANCELED, Order.STATUS_EXPIRED):
inv = generate_invoice(order)
else:
inv = c
order.log_action('pretix.event.order.invoice.reissued', user=self.request.user, data={
'invoice': inv.pk
})
messages.success(self.request, _('The invoice has been reissued.'))
inv = c
self.order.log_action('pretix.event.order.invoice.reissued', user=self.request.user, data={
'invoice': inv.pk
})
messages.success(self.request, _('The invoice has been reissued.'))
return redirect(self.get_order_url())
def get(self, *args, **kwargs): # NOQA
-1
View File
@@ -1440,7 +1440,6 @@ class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormVie
'name',
'location',
'frontpage_text',
'comment',
'geo_lat',
'geo_lon',
'is_public',
+12 -11
View File
@@ -64,8 +64,7 @@ def casual_reads():
class GroupConcat(Aggregate):
function = 'group_concat'
template = '%(function)s(%(distinct)s%(field)s, "%(separator)s")'
allow_distinct = True
template = '%(function)s(%(field)s, "%(separator)s")'
def __init__(self, *expressions, ordered=False, **extra):
self.ordered = ordered
@@ -74,17 +73,19 @@ class GroupConcat(Aggregate):
extra.update({'separator': ','})
super().__init__(*expressions, **extra)
def as_postgresql(self, compiler, connection, **extra_context):
def as_postgresql(self, compiler, connection):
if self.ordered:
template = "%(function)s(%(distinct)s%(field)s::text, '%(separator)s' ORDER BY %(field)s::text ASC)"
return super().as_sql(
compiler, connection,
function='string_agg',
template="%(function)s(%(field)s::text, '%(separator)s' ORDER BY %(field)s ASC)",
)
else:
template = "%(function)s(%(distinct)s%(field)s::text, '%(separator)s')"
return super().as_sql(
compiler, connection,
function='string_agg',
template=template,
**extra_context,
)
return super().as_sql(
compiler, connection,
function='string_agg',
template="%(function)s(%(field)s::text, '%(separator)s')",
)
class ReplicaRouter:
+8 -6
View File
@@ -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)
-1
View File
@@ -34,7 +34,6 @@ class Equal(Func):
arg_joiner = ' = '
arity = 2
function = ''
conditional = True
class GreaterThan(Func):
+2 -6
View File
@@ -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
+1 -1
View File
@@ -102,7 +102,7 @@ def assert_session_valid(request):
if 'pinned_country' in request.session:
if request.session.get('pinned_country') != country:
logger.info(f"Backend session for user {request.user.pk} terminated due to country change. "
f"Old country: \"{request.session.get('pinned_country')}\" New country: \"{country}\"")
f"Old country: \"{request.session.get('pinned_countres')}\" New country: \"{country}\"")
raise SessionInvalid()
else:
request.session['pinned_country'] = country

Some files were not shown because too many files have changed in this diff Show More