Compare commits

..

2 Commits

Author SHA1 Message Date
Raphael Michel
663de9239d Bump to 4.17.1 2023-03-06 14:37:42 +01:00
Raphael Michel
ef7dd65194 [SECURITY] Enforce session validation on oauth authorize endpoint 2023-03-06 14:37:27 +01:00
404 changed files with 150749 additions and 315976 deletions

View File

@@ -6,7 +6,7 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
directory: "/src"
schedule:
interval: "daily"
versioning-strategy: increase

View File

@@ -1,49 +0,0 @@
name: Build
on:
push:
branches: [ master ]
paths-ignore:
- 'doc/**'
- 'src/pretix/locale/**'
pull_request:
branches: [ master ]
paths-ignore:
- 'doc/**'
- 'src/pretix/locale/**'
permissions:
contents: read # to fetch code (actions/checkout)
env:
FORCE_COLOR: 1
jobs:
test:
runs-on: ubuntu-22.04
name: Packaging
strategy:
matrix:
python-version: ["3.11"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install system dependencies
run: sudo apt update && sudo apt install gettext unzip
- name: Install Python dependencies
run: pip3 install -U setuptools build pip check-manifest
- name: Run check-manifest
run: check-manifest
- name: Run build
run: python -m build
- name: Check files
run: unzip -l dist/pretix*whl | grep node_modules || exit 1

View File

@@ -38,6 +38,7 @@ jobs:
run: sudo apt update && sudo apt install gettext
- name: Install Dependencies
run: pip3 install -e ".[dev]"
working-directory: ./src
- name: Compile messages
run: python manage.py compilemessages
working-directory: ./src
@@ -63,6 +64,7 @@ jobs:
run: sudo apt update && sudo apt install enchant-2 hunspell hunspell-de-de aspell-en aspell-de
- name: Install Dependencies
run: pip3 install -e ".[dev]"
working-directory: ./src
- name: Spellcheck translations
run: potypo
working-directory: ./src

View File

@@ -36,6 +36,7 @@ jobs:
${{ runner.os }}-pip-
- name: Install Dependencies
run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
working-directory: ./src
- name: Run isort
run: isort -c .
working-directory: ./src
@@ -56,6 +57,7 @@ jobs:
${{ runner.os }}-pip-
- name: Install Dependencies
run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
working-directory: ./src
- name: Run flake8
run: flake8 .
working-directory: ./src

View File

@@ -64,6 +64,7 @@ jobs:
run: sudo apt update && sudo apt install gettext mariadb-client
- name: Install Python dependencies
run: pip3 install --ignore-requires-python -e ".[dev]" mysqlclient psycopg2-binary # We ignore that flake8 needs newer python as we don't run flake8 during tests
working-directory: ./src
- name: Run checks
run: python manage.py check
working-directory: ./src

2
.gitignore vendored
View File

@@ -1,6 +1,4 @@
env/
build/
dist/
.coverage
htmlcov/
.ropeproject

View File

@@ -5,8 +5,8 @@ tests:
- virtualenv env
- source env/bin/activate
- pip install -U pip wheel setuptools
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
- cd src
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
- python manage.py check
- make all compress
- py.test --reruns 3 -n 3 tests
@@ -21,16 +21,15 @@ pypi:
- virtualenv env
- source env/bin/activate
- pip install -U pip wheel setuptools check-manifest twine
- cd src
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
- python setup.py sdist
- pip install dist/pretix-*.tar.gz
- python -m pretix migrate
- python -m pretix check
- cd src
- make npminstall
- cd ..
- check-manifest
- python -m build
- make npminstall
- python setup.py sdist bdist_wheel
- twine check dist/*
- twine upload dist/*
tags:

View File

@@ -19,8 +19,6 @@ RUN apt-get update && \
python3-dev \
sudo \
supervisor \
libmaxminddb0 \
libmaxminddb-dev \
zlib1g-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
@@ -41,6 +39,18 @@ RUN apt-get update && \
ENV LC_ALL=C.UTF-8 \
DJANGO_SETTINGS_MODULE=production_settings
# To copy only the requirements files needed to install from PIP
COPY src/setup.py /pretix/src/setup.py
RUN pip3 install -U \
pip \
setuptools \
wheel && \
cd /pretix/src && \
PRETIX_DOCKER_BUILD=TRUE pip3 install \
-e ".[memcached,mysql]" \
gunicorn django-extensions ipython && \
rm -rf ~/.cache/pip
COPY deployment/docker/pretix.bash /usr/local/bin/pretix
COPY deployment/docker/supervisord /etc/supervisord
COPY deployment/docker/supervisord.all.conf /etc/supervisord.all.conf
@@ -48,19 +58,9 @@ COPY deployment/docker/supervisord.web.conf /etc/supervisord.web.conf
COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf
COPY deployment/docker/nginx-max-body-size.conf /etc/nginx/conf.d/nginx-max-body-size.conf
COPY deployment/docker/production_settings.py /pretix/src/production_settings.py
COPY pyproject.toml /pretix/pyproject.toml
COPY _build /pretix/_build
COPY src /pretix/src
RUN pip3 install -U \
pip \
setuptools \
wheel && \
cd /pretix && \
PRETIX_DOCKER_BUILD=TRUE pip3 install \
-e ".[memcached,mysql]" \
gunicorn django-extensions ipython && \
rm -rf ~/.cache/pip
RUN cd /pretix/src && python setup.py install
RUN chmod +x /usr/local/bin/pretix && \
rm /etc/nginx/sites-enabled/default && \

View File

@@ -1,48 +0,0 @@
include LICENSE
include README.rst
include src/Makefile
include _build/backend.py
global-include *.proto
recursive-include src/pretix/static *
recursive-include src/pretix/static.dist *
recursive-include src/pretix/locale *
recursive-include src/pretix/helpers/locale *
recursive-include src/pretix/base/templates *
recursive-include src/pretix/control/templates *
recursive-include src/pretix/presale/templates *
recursive-include src/pretix/plugins/banktransfer/templates *
recursive-include src/pretix/plugins/banktransfer/static *
recursive-include src/pretix/plugins/manualpayment/templates *
recursive-include src/pretix/plugins/manualpayment/static *
recursive-include src/pretix/plugins/paypal/templates *
recursive-include src/pretix/plugins/paypal/static *
recursive-include src/pretix/plugins/paypal2/templates *
recursive-include src/pretix/plugins/paypal2/static *
recursive-include src/pretix/plugins/src/pretixdroid/templates *
recursive-include src/pretix/plugins/src/pretixdroid/static *
recursive-include src/pretix/plugins/sendmail/templates *
recursive-include src/pretix/plugins/statistics/templates *
recursive-include src/pretix/plugins/statistics/static *
recursive-include src/pretix/plugins/stripe/templates *
recursive-include src/pretix/plugins/stripe/static *
recursive-include src/pretix/plugins/ticketoutputpdf/templates *
recursive-include src/pretix/plugins/ticketoutputpdf/static *
recursive-include src/pretix/plugins/badges/templates *
recursive-include src/pretix/plugins/badges/static *
recursive-include src/pretix/plugins/returnurl/templates *
recursive-include src/pretix/plugins/returnurl/static *
recursive-include src/pretix/plugins/webcheckin/templates *
recursive-include src/pretix/plugins/webcheckin/static *
recursive-include src *.cfg
recursive-include src *.csv
recursive-include src *.gitkeep
recursive-include src *.jpg
recursive-include src *.json
recursive-include src *.py
recursive-include src *.svg
recursive-include src *.txt
recursive-include src Makefile
recursive-exclude doc *
recursive-exclude deployment *
recursive-exclude res *

View File

@@ -1,12 +0,0 @@
import tomli
from setuptools import build_meta as _orig
from setuptools.build_meta import *
def get_requires_for_build_wheel(config_settings=None):
with open("pyproject.toml", "rb") as f:
p = tomli.load(f)
return [
*_orig.get_requires_for_build_wheel(config_settings),
*p['project']['dependencies']
]

View File

@@ -481,18 +481,3 @@ You can configure the maximum file size for uploading various files::
; Max upload size for other files in MiB, defaults to 10 MiB
; This includes all file upload type order questions
max_size_other = 100
GeoIP
-----
pretix can optionally make use of a GeoIP database for some features. It needs a file in ``mmdb`` format, for example
`GeoLite2`_ or `GeoAcumen`_::
[geoip]
path=/var/geoipdata/
filename_country=GeoLite2-Country.mmdb
.. _GeoAcumen: https://github.com/geoacumen/geoacumen-country
.. _GeoLite2: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data

View File

@@ -16,7 +16,7 @@ Manual installation
You can use ``pip`` to update pretix directly to the development branch. Then, upgrade as usual::
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install -U "git+https://github.com/pretix/pretix.git#egg=pretix"
(venv)$ pip3 install -U "git+https://github.com/pretix/pretix.git#egg=pretix&subdirectory=src"
(venv)$ python -m pretix migrate
(venv)$ python -m pretix rebuild
(venv)$ python -m pretix updatestyles

View File

@@ -51,7 +51,7 @@ For our standard docker installation, create the database and user like this::
# sudo -u postgres createuser -P pretix
# sudo -u postgres createdb -O pretix pretix
Make sure that your database listens on the network. If PostgreSQL on the same same host as docker, but not inside a docker container, we recommend that you listen on the Docker interface by changing the following line in ``/etc/postgresql/<version>/main/postgresql.conf``::
Make sure that your database listens on the network. If PostgreSQL on the same same host as docker, but not inside a docker container, we recommend that you just listen on the Docker interface by changing the following line in ``/etc/postgresql/<version>/main/postgresql.conf``::
listen_addresses = 'localhost,172.17.0.1'
@@ -153,89 +153,4 @@ And you're done! After you've verified everything has been copied correctly, you
.. note:: Don't forget to update your backup process to back up your PostgreSQL database instead of your MySQL database now.
Troubleshooting
---------------
Peer authentication failed
""""""""""""""""""""""""""
Sometimes you might see an error message like this::
django.db.utils.OperationalError: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL: Peer authentication failed for user "pretix"
It is important to understand that PostgreSQL by default offers two types of authentication:
- **Peer authentication**, which works automatically based on the Linux user you are working as. This requires that
the connection is made through a local socket (empty ``host=`` in ``pretix.cfg``) and the name of the PostgreSQL user
and the Linux user are identical.
- Typically, you might run into this error if you accidentally execute ``python -m pretix`` commands as root instead
of the ``pretix`` user.
- **Password authentication**, which requires a username and password and works over network connections. To force
password authentication instead of peer authentication, set ``host=127.0.0.1`` in ``pretix.cfg``.
- You can alter the password on a PostgreSQL shell using the command ``ALTER USER pretix WITH PASSWORD '***';``.
When creating a user with the ``createuser`` command, pass option ``-P`` to set a new password.
- Even with password authentication, PostgreSQL by default only allows local connections. To allow remote connections,
you need to adjust both the ``listen_address`` configuration parameter as well as the ``pg_hba.conf`` file (see above
for an example with the docker networking setup).
Database error: relation does not exist
"""""""""""""""""""""""""""""""""""""""
If you see an error like this::
2023-04-17T19:20:47.744023Z ERROR Database error 42P01: relation "public.pretix_foobar" does not exist
QUERY: ALTER TABLE public.pretix_foobar DROP CONSTRAINT IF EXISTS pretix_foobar_order_id_57e2cb41_fk_pretixbas CASCADE;
2023-04-17T19:20:47.744023Z FATAL Failed to create the schema, see above.
The reason is most likely that in the past, you installed a pretix plugin that you no longer have installed. However,
the database still contains tables of that plugin. If you want to keep the data, reinstall the plugin and re-run the
``migrate`` step from above. If you want to get rid of the data, manually drop the table mentioned in the error message
from your MySQL database::
# mysql -u root pretix
mysql> DROP TABLE pretix_foobar;
Then, retry. You might see a new error message with a new table, which you can handle the same way.
Cleaning out a failed attempt
"""""""""""""""""""""""""""""
You might want to clean your PostgreSQL database before you try again after an error. You can do so like this::
# sudo -u postgres psql pretix
pretix=# DROP SCHEMA public CASCADE;
pretix=# CREATE SCHEMA public;
pretix=# ALTER SCHEMA public OWNER TO pretix;
``pgloader`` crashes with heap exhaustion error
"""""""""""""""""""""""""""""""""""""""""""""""
On some larger databases, we've seen ``pgloader`` crash with error messages similar to this::
Heap exhausted during garbage collection: 16 bytes available, 48 requested.
Or this::
2021-01-04T21:31:17.367000Z ERROR A SB-KERNEL::HEAP-EXHAUSTED-ERROR condition without bindings for heap statistics. (If
you did not expect to see this message, please report it.
2021-01-04T21:31:17.382000Z ERROR The value
NIL
is not of type
NUMBER
when binding SB-KERNEL::X
The ``pgloader`` version distributed for Debian and Ubuntu is compiled with the ``SBCL`` compiler. If compiled with
``CCL``, these bugs go away. Unfortunately, it is pretty hard to compile ``pgloader`` manually with ``CCL``. If you
run into this, we therefore recommend using the docker container provided by the ``pgloader`` maintainers::
sudo docker run --rm -v /tmp:/tmp --network host -it dimitri/pgloader:ccl.latest pgloader /tmp/pretix.load
As peer authentication is not available from inside the container, this requires you to use password-based authentication
in PostgreSQL (see above).
.. _PostgreSQL repositories: https://wiki.postgresql.org/wiki/Apt

View File

@@ -25,7 +25,7 @@ and what you should think of.
Scaling reasons
---------------
There are two main reasons for scaling up a pretix installation beyond a single server:
There's mainly two reasons to scale up a pretix installation beyond a single server:
* **Availability:** Distributing pretix over multiple servers can allow you to survive failure of one or more single machines, leading to a higher uptime and reliability of your system.
@@ -92,7 +92,7 @@ them from a different URL <config-urls>`.
pretix-web
""""""""""
The ``pretix-web`` process does not carry any internal state and can be easily started on as many machines as you like, and you can
The ``pretix-web`` process does not carry any internal state can be easily started on as many machines as you like, and you can
use the load balancing features of your frontend web server to redirect to all of them.
You can adjust the number of processes in the ``gunicorn`` command line, and we recommend choosing roughly two times the number
@@ -154,7 +154,7 @@ files, otherwise you **will** run into errors with the user interface.
The easiest solution for this is probably to store them on a NFS server that you mount
on each of the other servers.
Since we use Django's file storage mechanism internally, you can in theory also use an object-storage solution like Amazon S3, Ceph, or Minio to store these files, although we currently do not expose this through pretix' configuration file and this would require you to ship your own variant of ``pretix/settings.py`` and reference it through the ``DJANGO_SETTINGS_MODULE`` environment variable.
Since we use Django's file storage mechanism internally, you can in theory also use a object-storage solution like Amazon S3, Ceph, or Minio to store these files, although we currently do not expose this through pretix' configuration file and this would require you to ship your own variant of ``pretix/settings.py`` and reference it through the ``DJANGO_SETTINGS_MODULE`` environment variable.
At pretix.eu, we use a custom-built `object storage cluster`_.
@@ -171,12 +171,12 @@ you configure, so make sure to set this memory usage as high as you can afford.
memory available allows your database to make more use of caching, which is usually good.
Scaling your database to multiple machines needs to be treated with great caution. It's a
good idea to have a replica of your database for availability reasons. In case your primary
good to have a replica of your database for availability reasons. In case your primary
database server fails, you can easily switch over to the replica and continue working.
However, using database replicas for performance gain is much more complicated. When using
However, using database replicas for performance gains is much more complicated. When using
replicated database systems, you are always trading in consistency or availability to get
additional performance and the consequences of this can be subtle. It is important
additional performance and the consequences of this can be subtle and it is important
that you have a deep understanding of the semantics of your replication mechanism.
.. warning::
@@ -187,7 +187,7 @@ that you have a deep understanding of the semantics of your replication mechanis
As an example, if you buy a ticket, pretix first needs to calculate how many tickets
are left to sell. If this calculation is done on a database replica that lags behind
even for fractions of a second, the decision to allow selling the ticket will be made
on stale data and you can end up with more tickets sold than configured. Similarly,
on out-of-data data and you can end up with more tickets sold than configured. Similarly,
you could imagine situations leading to double payments etc.
If you do have a replica, you *can* tell pretix about it :ref:`in your configuration <config-replica>`.
@@ -204,9 +204,9 @@ redis
While redis is a very important part that glues together some of the components, it isn't used
heavily and can usually handle a fairly large pretix installation easily on a single modern
CPU core.
Having some memory available is good, e.g. if lots of tasks queue up during a traffic peak, but we wouldn't expect ever needing more than a gigabyte of it.
Having some memory available is good in case of e.g. lots of tasks queuing up during a traffic peak, but we wouldn't expect ever needing more than a gigabyte of it.
Feel free to set up a redis cluster for availability but you probably won't need it for performance.
Feel free to set up a redis cluster for availability but you won't need it for performance in a long time.
The limitations
---------------
@@ -228,9 +228,9 @@ if you add more hardware.
If you have an unlimited number of tickets, we can apply fewer locking and we've reached **approx.
1500 orders per minute per event** in benchmarks, although even more should be possible.
We're working on reducing the number of cases in which this is relevant and thereby improve the possible
We're working to reduce the number of cases in which this is relevant and thereby improve the possible
throughput. If you want to use pretix for an event with 10,000+ tickets that are likely to be sold out
within minutes, please get in touch to discuss possible solutions. We'll work something out for you!
.. _object storage cluster: https://behind.pretix.eu/2018/03/20/high-available-cdn/
.. _object storage cluster: https://behind.pretix.eu/2018/03/20/high-available-cdn/

View File

@@ -225,3 +225,4 @@ You can get three response codes:
"subevent": 23,
"checkinlist": 5
}

View File

@@ -13,10 +13,6 @@ failed scans.
The endpoints listed on this page have been added.
.. versionchanged:: 4.18
The ``source_type`` parameter has been added.
.. _`rest-checkin-redeem`:
Checking a ticket in
@@ -32,7 +28,6 @@ Checking a ticket in
passed needs to be from a distinct event.
:<json string secret: Scanned QR code corresponding to the ``secret`` attribute of a ticket.
:<json string source_type: Type of source the ``secret`` was obtained form. Defaults to ``"barcode"``.
:<json array lists: List of check-in list IDs to search on. No two check-in lists may be from the same event.
:<json string type: Send ``"exit"`` for an exit and ``"entry"`` (default) for an entry.
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
@@ -77,7 +72,6 @@ Checking a ticket in
{
"secret": "M5BO19XmFwAjLd4nDYUAL9ISjhti0e9q",
"source_type": "barcode",
"lists": [1],
"force": false,
"ignore_unpaid": false,
@@ -219,8 +213,8 @@ Checking a ticket in
* ``revoked`` - Ticket code has been revoked.
* ``error`` - Internal error.
In case of reason ``rules`` and ``invalid_time``, there might be an additional response field ``reason_explanation``
with a human-readable description of the violated rules. However, that field can also be missing or be ``null``.
In case of reason ``rules``, there might be an additional response field ``reason_explanation`` with a human-readable
description of the violated rules. However, that field can also be missing or be ``null``.
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 201: no error

View File

@@ -753,8 +753,8 @@ Order position endpoints
* ``ambiguous`` - Multiple tickets match scan, rejected.
* ``revoked`` - Ticket code has been revoked.
In case of reason ``rules`` or ``invalid_time``, there might be an additional response field ``reason_explanation``
with a human-readable description of the violated rules. However, that field can also be missing or be ``null``.
In case of reason ``rules``, there might be an additional response field ``reason_explanation`` with a human-readable
description of the violated rules. However, that field can also be missing or be ``null``.
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch

View File

@@ -137,7 +137,6 @@ Endpoints
:query page: The page number in case of a multi-page result set, default is 1
:query is_public: If set to ``true``/``false``, only events with a matching value of ``is_public`` are returned.
:query live: If set to ``true``/``false``, only events with a matching value of ``live`` are returned.
:query testmode: If set to ``true``/``false``, only events with a matching value of ``testmode`` are returned.
:query has_subevents: If set to ``true``/``false``, only events with a matching value of ``has_subevents`` are returned.
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned. Event series are never (always) returned.
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned. Event series are never (always) returned.
@@ -547,9 +546,6 @@ Therefore, we're also not including a list of the options here, but instead reco
to see available options. The ``explain=true`` flag enables a verbose mode that provides you with human-readable
information about the properties.
Note that some settings are read-only, e.g. because they can be read on event level but currently only be changed on
organizer level.
.. note:: Please note that this is not a complete representation of all event settings. You will find more settings
in the web interface.
@@ -596,7 +592,6 @@ organizer level.
{
"value": "https://pretix.eu",
"label": "Imprint URL",
"readonly": false,
"help_text": "This should point e.g. to a part of your website that has your contact details and legal information."
}
},
@@ -610,10 +605,6 @@ organizer level.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. versionchanged:: 4.18
The ``readonly`` flag has been added.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/settings/
Updates event settings. Note that ``PUT`` is not allowed here, only ``PATCH``.

View File

@@ -20,12 +20,6 @@ currency string Currency of the
testmode boolean Whether this is a test gift card
expires datetime Expiry date (or ``null``)
conditions string Special terms and conditions for this card (or ``null``)
owner_ticket integer Internal ID of an order position that is the "owner" of
this gift card and can view all transactions. When setting
this field, you can also give the ``secret`` of an order
position.
issuer string Organizer slug of the organizer who created this gift
card and is responsible for it.
===================================== ========================== =======================================================
The gift card transaction resource contains the following public fields:
@@ -41,17 +35,8 @@ value money (string) Transaction amo
event string Event slug, if the gift card was used in the web shop (or ``null``)
order string Order code, if the gift card was used in the web shop (or ``null``)
text string Custom text of the transaction (or ``null``)
info object Additional data about the transaction (or ``null``)
acceptor string Organizer slug of the organizer who created this transaction
(can be ``null`` for all transactions performed before
this field was added.)
===================================== ========================== =======================================================
.. versionchanged:: 4.20
The ``owner_ticket`` and ``issuer`` attributes of the gift card and the ``info`` and ``acceptor`` attributes of the
gift card transaction resource have been added.
Endpoints
---------
@@ -87,8 +72,6 @@ Endpoints
"testmode": false,
"expires": null,
"conditions": null,
"owner_ticket": null,
"issuer": "bigevents",
"value": "13.37"
}
]
@@ -98,10 +81,6 @@ Endpoints
:query string secret: Only show gift cards with the given secret.
:query boolean testmode: Filter for gift cards that are (not) in test mode.
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
:query string expand: If you pass ``"owner_ticket"``, the respective field will be shown as a nested value instead of just an ID.
The nested objects are identical to the respective resources, except that the ``owner_ticket``
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
matching easier. The parameter can be given multiple times.
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
@@ -134,8 +113,6 @@ Endpoints
"testmode": false,
"expires": null,
"conditions": null,
"owner_ticket": null,
"issuer": "bigevents",
"value": "13.37"
}
@@ -180,16 +157,10 @@ Endpoints
"currency": "EUR",
"expires": null,
"conditions": null,
"owner_ticket": null,
"issuer": "bigevents",
"value": "13.37"
}
:param organizer: The ``slug`` field of the organizer to create a gift card for
:query string expand: If you pass ``"owner_ticket"``, the respective field will be shown as a nested value instead of just an ID.
The nested objects are identical to the respective resources, except that the ``owner_ticket``
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
matching easier. The parameter can be given multiple times.
:statuscode 201: no error
:statuscode 400: The gift card could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
@@ -234,8 +205,6 @@ Endpoints
"currency": "EUR",
"expires": null,
"conditions": null,
"owner_ticket": null,
"issuer": "bigevents",
"value": "14.00"
}
@@ -281,8 +250,6 @@ Endpoints
"testmode": false,
"expires": null,
"conditions": null,
"owner_ticket": null,
"issuer": "bigevents",
"value": "15.37"
}
@@ -326,11 +293,7 @@ Endpoints
"value": "50.00",
"event": "democon",
"order": "FXQYW",
"text": null,
"acceptor": "bigevents",
"info": {
"created_by": "plugin1"
}
"text": null
}
]
}

View File

@@ -32,7 +32,6 @@ at :ref:`plugin-docs`.
membershiptypes
memberships
giftcards
reusablemedia
carts
teams
devices

View File

@@ -1,211 +0,0 @@
Item Meta Properties
====================
Resource description
--------------------
An Item Meta Property is used to include (event internally relevant) meta information with every item (product). This
could be internal categories like booking positions.
The Item Meta Properties resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Unique ID for this property
name string Name of the property
default string Value of the default option
required boolean If ``true``, this property will have to be assigned a
value in all items of the related event
allowed_values list List of all permitted values for this property,
or ``null`` for no limitation
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/item_meta_properties/
Returns a list of all Item Meta Properties within a given event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/item_meta_properties/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"name": "Color",
"default": "red",
"required": true,
"allowed_values": ["red", "green", "blue"]
}
]
}
:param organizer: The ``slug`` field of the organizer
:param event: The ``slug`` field of the event
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/item_meta_properties/(id)/
Returns information on one property, identified by its id.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/item_meta_properties/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
{
"id": 1,
"name": "Color",
"default": "red",
"required": true,
"allowed_values": ["red", "green", "blue"]
}
:param organizer: The ``slug`` field of the organizer
:param event: The ``slug`` field of the event
:param id: The ``id`` field of the item meta property to retrieve
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/item_meta_properties/
Creates a new item meta property
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/item_meta_properties/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"name": "ref-code",
"default": "abcde",
"required": true,
"allowed_values": null
}
**Example response**:
.. sourcecode:: http
{
"id": 2,
"name": "ref-code",
"default": "abcde",
"required": true,
"allowed_values": null
}
:param organizer: The ``slug`` field of the organizer
:param event: The ``slug`` field of the event
:statuscode 201: no error
:statuscode 400: The item meta property could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/item_meta_properties/(id)/
Update an item meta property. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide
all fields of the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the
fields that you want to change.
You can change all fields of the resource except the ``id`` field.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/item_meta_properties/2/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"required": false
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 2,
"name": "ref-code",
"default": "abcde",
"required": false,
"allowed_values": []
}
:param organizer: The ``slug`` field of the organizer
:param event: The ``slug`` field of the event
:param id: The ``id`` field of the item meta property to modify
:statuscode 200: no error
:statuscode 400: The property could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/item_meta_properties/(id)/
Delete an item meta property.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/item_meta_properties/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer
:param event: The ``slug`` field of the event
:param id: The ``id`` field of the item meta property to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource.

View File

@@ -108,9 +108,6 @@ generate_tickets boolean If ``false``,
allow_waitinglist boolean If ``false``, no waiting list will be shown for this
product when it is sold out.
issue_giftcard boolean If ``true``, buying this product will yield a gift card.
media_policy string Policy on how to handle reusable media (experimental feature).
Possible values are ``null``, ``"new"``, ``"reuse"``, and ``"reuse_or_new"``.
media_type string Type of reusable media to work on (experimental feature). See :ref:`rest-reusablemedia` for possible choices.
show_quota_left boolean Publicly show how many tickets are still available.
If this is ``null``, the event default is used.
has_variations boolean Shows whether or not this item has variations.
@@ -192,10 +189,6 @@ meta_data object Values set fo
The ``validity_*`` attributes have been added.
.. versionchanged:: 4.18
The ``media_policy`` and ``media_type`` attributes have been added.
Notes
-----
@@ -251,8 +244,6 @@ Endpoints
"admission": false,
"personalized": false,
"issue_giftcard": false,
"media_policy": null,
"media_type": null,
"meta_data": {},
"position": 0,
"picture": null,
@@ -382,8 +373,6 @@ Endpoints
"admission": false,
"personalized": false,
"issue_giftcard": false,
"media_policy": null,
"media_type": null,
"meta_data": {},
"position": 0,
"picture": null,
@@ -494,8 +483,6 @@ Endpoints
"admission": false,
"personalized": false,
"issue_giftcard": false,
"media_policy": null,
"media_type": null,
"meta_data": {},
"position": 0,
"picture": null,
@@ -593,8 +580,6 @@ Endpoints
"admission": false,
"personalized": false,
"issue_giftcard": false,
"media_policy": null,
"media_type": null,
"meta_data": {},
"position": 0,
"picture": null,
@@ -724,8 +709,6 @@ Endpoints
"admission": false,
"personalized": false,
"issue_giftcard": false,
"media_policy": null,
"media_type": null,
"meta_data": {},
"position": 0,
"picture": null,

View File

@@ -910,7 +910,6 @@ Creating orders
* ``valid_from`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
* ``valid_until`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
* ``requested_valid_from`` (optional, can be set **instead** of ``valid_from`` and ``valid_until`` to signal a user choice for the start time that may or may not be respected)
* ``use_reusable_medium`` (optional, causes the new ticket to take over the given reusable medium, identified by its ID)
* ``answers``
* ``question``

View File

@@ -157,7 +157,6 @@ information about the properties.
{
"value": "calendar",
"label": "Default overview style",
"readonly": false,
"help_text": "If your event series has more than 50 dates in the future, only the month or week calendar can be used."
}
},

View File

@@ -63,7 +63,6 @@ valid_date_max date Maximum value f
valid_datetime_min datetime Minimum value for date and time questions (optional)
valid_datetime_max datetime Maximum value for date and time questions (optional)
valid_file_portrait boolean Turn on file validation for portrait photos
valid_string_length_max integer Maximum length for string questions (optional)
dependency_question integer Internal ID of a different question. The current
question will only be shown if the question given in
this attribute is set to the value given in
@@ -123,7 +122,6 @@ Endpoints
"valid_date_max": null,
"valid_datetime_min": null,
"valid_datetime_max": null,
"valid_string_length_max": null,
"valid_file_portrait": false,
"dependency_question": null,
"dependency_value": null,
@@ -203,7 +201,6 @@ Endpoints
"valid_datetime_min": null,
"valid_datetime_max": null,
"valid_file_portrait": false,
"valid_string_length_max": null,
"dependency_question": null,
"dependency_value": null,
"dependency_values": [],
@@ -305,7 +302,6 @@ Endpoints
"valid_datetime_min": null,
"valid_datetime_max": null,
"valid_file_portrait": false,
"valid_string_length_max": null,
"options": [
{
"id": 1,
@@ -388,7 +384,6 @@ Endpoints
"valid_datetime_min": null,
"valid_datetime_max": null,
"valid_file_portrait": false,
"valid_string_length_max": null,
"options": [
{
"id": 1,

View File

@@ -1,317 +0,0 @@
.. _`rest-reusablemedia`:
Reusable media
==============
Reusable media represent things, typically physical tokens like plastic cards or NFC wristbands, which can represent
other entities inside the system. For example, a medium can link to an order position or to a gift card and can be used
in their place. Later, the medium might be reused for a different ticket.
Resource description
--------------------
The reusable medium resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the medium
type string Type of medium, e.g. ``"barcode"`` or ``"nfc_uid"``.
identifier string Unique identifier of the medium. The format depends on the ``type``.
active boolean Whether this medium may be used.
created datetime Date of creation
updated datetime Date of last modification
expires datetime Expiry date (or ``null``)
customer string Identifier of a customer account this medium belongs to.
linked_orderposition integer Internal ID of a ticket this medium is linked to.
linked_giftcard integer Internal ID of a gift card this medium is linked to.
info object Additional data, content depends on the ``type``. Consider
this internal to the system and don't use it for your own data.
notes string Internal notes and comments (or ``null``)
===================================== ========================== =======================================================
Existing media types are:
- ``barcode``
- ``nfc_uid``
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/reusablemedia/
Returns a list of all media issued by a given organizer.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/reusablemedia/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z",
"type": "barcode",
"active": True,
"expires": None,
"customer": None,
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
"info": {}
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1.
:query string identifier: Only show media with the given identifier. Note that you should use the lookup endpoint described below for most use cases.
:query string type: Only show media with the given type.
:query boolean active: Only show media that are (not) active.
:query string customer: Only show media linked to the given customer.
:query string created_since: Only show media created since a given date.
:query string updated_since: Only show media updated since a given date.
:query integer linked_orderposition: Only show media linked to the given ticket.
:query integer linked_giftcard: Only show media linked to the given gift card.
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_giftcard.owner_ticket"``, ``"linked_orderposition"``,
or ``"customer"``, the respective field will be shown as a nested value instead of just an ID.
The nested objects are identical to the respective resources, except that order positions
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
matching easier. The parameter can be given multiple times.
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/reusablemedia/(id)/
Returns information on one medium, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/reusablemedia/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z",
"type": "barcode",
"active": True,
"expires": None,
"customer": None,
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
"info": {}
}
:param organizer: The ``slug`` field of the organizer to fetch
:param id: The ``id`` field of the medium to fetch
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_giftcard.owner_ticket"``, ``"linked_orderposition"``,
or ``"customer"``, the respective field will be shown as a nested value instead of just an ID.
The nested objects are identical to the respective resources, except that order positions
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
matching easier. The parameter can be given multiple times.
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/reusablemedia/lookup/
Look up a new reusable medium by its identifier. In some cases, this might lead to the automatic creation of a new
medium behind the scenes.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/reusablemedia/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"identifier": "ABCDEFGH",
"type": "barcode",
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z",
"type": "barcode",
"active": True,
"expires": None,
"customer": None,
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
"info": {}
}
:param organizer: The ``slug`` field of the organizer to look up a medium for
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
field will be shown as a nested value instead of just an ID. The nested objects are identical to
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
can be given multiple times.
:statuscode 201: no error
:statuscode 400: The medium could not be looked up due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
.. http:post:: /api/v1/organizers/(organizer)/reusablemedia/
Creates a new reusable medium.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/reusablemedia/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"identifier": "ABCDEFGH",
"type": "barcode",
"active": True,
"expires": None,
"customer": None,
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
"info": {}
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z",
"type": "barcode",
"active": True,
"expires": None,
"customer": None,
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
"info": {}
}
:param organizer: The ``slug`` field of the organizer to create a medium for
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
field will be shown as a nested value instead of just an ID. The nested objects are identical to
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
can be given multiple times.
:statuscode 201: no error
:statuscode 400: The medium could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/reusablemedia/(id)/
Update a reusable medium. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
You can change all fields of the resource except the ``id``, ``identifier`` and ``type`` fields.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/reusablemedia/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"linked_orderposition": 13
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z",
"type": "barcode",
"active": True,
"expires": None,
"customer": None,
"linked_orderposition": 13,
"linked_giftcard": None,
"notes": None,
"info": {}
}
:param organizer: The ``slug`` field of the organizer to modify
:param id: The ``id`` field of the medium to modify
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
field will be shown as a nested value instead of just an ID. The nested objects are identical to
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
can be given multiple times.
:statuscode 200: no error
:statuscode 400: The medium could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.

View File

@@ -26,7 +26,6 @@ can_create_events boolean
can_change_teams boolean
can_change_organizer_settings boolean
can_manage_customers boolean
can_manage_reusable_media boolean
can_manage_gift_cards boolean
can_change_event_settings boolean
can_change_items boolean
@@ -37,10 +36,6 @@ can_change_vouchers boolean
can_checkin_orders boolean
===================================== ========================== =======================================================
.. versionchanged:: 4.18
The ``can_manage_reusable_media`` permission has been added.
Team member resource
--------------------

View File

@@ -47,8 +47,6 @@ tag string A string that i
comment string An internal comment on the voucher
subevent integer ID of the date inside an event series this voucher belongs to (or ``null``).
show_hidden_items boolean Only if set to ``true``, this voucher allows to buy products with the property ``hide_without_voucher``. Defaults to ``true``.
all_addons_included boolean If set to ``true``, all add-on products for the product purchased with this voucher are included in the base price.
all_bundles_included boolean If set to ``true``, all bundled products for the product purchased with this voucher are added without their designated price.
===================================== ========================== =======================================================
@@ -97,9 +95,6 @@ Endpoints
"comment": "",
"seat": null,
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
}
]
}
@@ -166,10 +161,7 @@ Endpoints
"tag": "testvoucher",
"comment": "",
"seat": null,
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
"subevent": null
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -206,10 +198,7 @@ Endpoints
"quota": null,
"tag": "testvoucher",
"comment": "",
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
"subevent": null
}
**Example response**:
@@ -236,10 +225,7 @@ Endpoints
"tag": "testvoucher",
"comment": "",
"seat": null,
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
"subevent": null
}
:param organizer: The ``slug`` field of the organizer to create a voucher for
@@ -278,10 +264,7 @@ Endpoints
"quota": null,
"tag": "testvoucher",
"comment": "",
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
"subevent": null
},
{
"code": "ASDKLJCYXCASDASD",
@@ -296,10 +279,7 @@ Endpoints
"quota": null,
"tag": "testvoucher",
"comment": "",
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
"subevent": null
},
**Example response**:
@@ -373,10 +353,7 @@ Endpoints
"tag": "testvoucher",
"comment": "",
"seat": null,
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false
"subevent": null
}
:param organizer: The ``slug`` field of the organizer to modify

View File

@@ -47,7 +47,6 @@ The following values for ``action_types`` are valid with pretix core:
* ``pretix.event.order.refund.done``
* ``pretix.event.order.refund.canceled``
* ``pretix.event.order.refund.failed``
* ``pretix.event.order.payment.confirmed``
* ``pretix.event.order.approved``
* ``pretix.event.order.denied``
* ``pretix.event.checkin``

View File

@@ -13,7 +13,7 @@ Core
.. automodule:: pretix.base.signals
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter,
register_ticket_secret_generators, gift_card_transaction_display
register_ticket_secret_generators
Order events
""""""""""""
@@ -21,7 +21,7 @@ Order events
There are multiple signals that will be sent out in the ordering cycle:
.. automodule:: pretix.base.signals
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
:members: validate_cart, validate_cart_addons, validate_order, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
Check-ins
"""""""""

View File

@@ -58,11 +58,11 @@ If you do not have a recent installation of ``nodejs``, install it now::
To make sure it is on your path variable, close and reopen your terminal. Now, install the Python-level dependencies of pretix::
cd src/
pip3 install -e ".[dev]"
Next, you need to copy the SCSS files from the source folder to the STATIC_ROOT directory::
cd src/
python manage.py collectstatic --noinput
Then, create the local database::
@@ -150,13 +150,6 @@ Add this to your ``src/pretix.cfg``::
Then execute ``python -m smtpd -n -c DebuggingServer localhost:1025``.
Working with periodic tasks
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Periodic tasks (like sendmail rules) are run when an external scheduler (like cron)
triggers the ``runperiodic`` command.
To run periodic tasks, execute ``python manage.py runperiodic``.
Working with translations
^^^^^^^^^^^^^^^^^^^^^^^^^
If you want to translate new strings that are not yet known to the translation system,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 177 KiB

View File

@@ -38,27 +38,27 @@ else
endif
"Is the order in status PAID or PENDING\nand is the position not canceled?" --> if "" then
-right->[no && !force] "Return error CANCELED"
-right->[no] "Return error CANCELED"
else
-down->[yes || force] "Is one or more block set on the ticket?"
-down->[yes] "Is one or more block set on the ticket?"
--> if "" then
-right->[no && !force] "Return error BLOCKED"
-right->[no] "Return error BLOCKED"
else
-down->[yes || force] "If this is not an exit, is the valid_from/valid_until\nconstraint on the ticket fulfilled?"
-down->[yes] "If this is not an exit, is the valid_from/valid_until\nconstraint on the ticket fulfilled?"
--> if "" then
-right->[no && !force] "Return error INVALID_TIME"
-right->[no] "Return error INVALID_TIME"
else
-down->[yes || force] "Is the product part of the check-in list?"
-down->[yes] "Is the product part of the check-in list?"
--> if "" then
-right->[no && !force] "Return error PRODUCT"
-right->[no] "Return error PRODUCT"
else
-down->[yes || force] "Is the subevent part of the check-in list?"
-down->[yes] "Is the subevent part of the check-in list?"
--> if "" then
-right->[no && !force] "Return error PRODUCT "
-right->[no] "Return error PRODUCT "
else
-down->[yes] "Is the order in status PAID?"
-down->[yes] "Is the order in status PAID\nor is this a forced upload?"
--> if "" then
-right->[no && !force] "Is Order.require_approval set?"
-right->[no] "Is Order.require_approval set?"
--> if "" then
-->[no] "Is Order.valid_if_pending set?"
--> if "" then
@@ -80,7 +80,7 @@ else
-->[yes] "Return error UNPAID "
endif
else
-down->[yes || force] "Is this an entry or exit?\nIs the upload forced?"
-down->[yes] "Is this an entry or exit?\nIs the upload forced?"
endif
endif
endif

View File

@@ -1,10 +1,10 @@
sphinx==7.0.*
sphinx==6.1.*
jinja2==3.1.*
sphinx-rtd-theme
sphinxcontrib-httpdomain
sphinxcontrib-images
sphinxcontrib-jquery
sphinxcontrib-spelling==8.*
sphinxcontrib-spelling==7.*
sphinxemoji
pygments-markdown-lexer
pyenchant==3.2.*

View File

@@ -1,11 +1,11 @@
-e ../
sphinx==7.0.*
-e ../src/
sphinx==6.1.*
jinja2==3.1.*
sphinx-rtd-theme
sphinxcontrib-httpdomain
sphinxcontrib-images
sphinxcontrib-jquery
sphinxcontrib-spelling==8.*
sphinxcontrib-spelling==7.*
sphinxemoji
pygments-markdown-lexer
pyenchant==3.2.*

View File

@@ -201,10 +201,6 @@ record for the subdomain ``pretix._domainkey`` with the following contents::
Then, please contact support@pretix.eu and we will enable DKIM for your domain on our mail servers.
.. note:: Many SMTP servers impose rate limits on the sent emails, such as a maximum number of emails sent per hour.
These SMTP servers are often not suitable for use with pretix, in case you want to send an email to many
hundreds or thousands of ticket buyers. Depending on how the rate limit is implemented, emails might be lost
in this case, as pretix only retries email delivery for a certain time period.
.. _Sender Policy Framework: https://en.wikipedia.org/wiki/Sender_Policy_Framework
.. _SPF specification: http://www.open-spf.org/SPF_Record_Syntax

View File

@@ -318,10 +318,7 @@ Currently, the following attributes are understood by pretix itself:
* If ``data-consent="…"`` is given, the cookie consent mechanism will be initialized with consent for the given cookie
providers. All other providers will be disabled, no consent dialog will be shown. This is useful if you already
asked the user for consent and don't want them to be asked again. Example: ``data-consent="facebook,google_analytics"``
When using the pretix-tracking plugin, the following values are supported::
``adform, facebook, gosquared, google_ads, google_analytics, hubspot, linkedin, matomo, twitter``
asked the user for consent and don't want them to be asked again. Example: ``data-consent="facebook,google_analytics"``
Any configured pretix plugins might understand more data fields. For example, if the appropriate plugins on pretix
Hosted or pretix Enterprise are active, you can pass the following fields:

View File

@@ -1,164 +0,0 @@
[project]
name = "pretix"
dynamic = ["version"]
description = "Reinventing presales, one ticket at a time"
readme = "README.rst"
requires-python = ">=3.9"
license = {file = "LICENSE"}
keywords = ["tickets", "web", "shop", "ecommerce"]
authors = [
{name = "pretix team", email = "support@pretix.eu"},
]
maintainers = [
{name = "pretix team", email = "support@pretix.eu"},
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Intended Audience :: Other Audience",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Environment :: Web Environment",
"License :: OSI Approved :: GNU Affero General Public License v3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Framework :: Django :: 3.2",
]
dependencies = [
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
"babel",
"BeautifulSoup4==4.12.*",
"bleach==5.0.*",
"celery==5.2.*",
"chardet==5.1.*",
"cryptography>=3.4.2",
"css-inline==0.8.*",
"defusedcsv>=1.1.0",
"dj-static",
"Django==3.2.*,>=3.2.18",
"django-bootstrap3==23.1.*",
"django-compressor==4.3.*",
"django-countries==7.5.*",
"django-filter==23.2",
"django-formset-js-improved==0.5.0.3",
"django-formtools==2.4.1",
"django-hierarkey==1.1.*",
"django-hijack==3.3.*",
"django-i18nfield==1.9.*,>=1.9.4",
"django-libsass==0.9",
"django-localflavor==4.0",
"django-markup",
"django-mysql",
"django-oauth-toolkit==2.2.*",
"django-otp==1.1.*",
"django-phonenumber-field==7.1.*",
"django-redis==5.2.*",
"django-scopes==2.0.*",
"django-statici18n==2.3.*",
"djangorestframework==3.14.*",
"dnspython==2.3.*",
"drf_ujson2==1.7.*",
"geoip2==4.*",
"importlib_metadata==6.6.*", # Polyfill, we can probably drop this once we require Python 3.10+
"isoweek",
"jsonschema",
"kombu==5.2.*",
"libsass==0.22.*",
"lxml",
"markdown==3.4.3", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
"mt-940==4.23.*",
"oauthlib==3.2.*",
"openpyxl==3.1.*",
"packaging",
"paypalrestsdk==1.13.*",
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.6.*",
"phonenumberslite==8.13.*",
"Pillow==9.5.*",
"pretix-plugin-build",
"protobuf==4.23.*",
"psycopg2-binary",
"pycountry",
"pycparser==2.21",
"pycryptodome==3.18.*",
"pypdf==3.8.*",
"python-bidi==0.4.*", # Support for Arabic in reportlab
"python-dateutil==2.8.*",
"python-u2flib-server==4.*",
"pytz",
"pyuca",
"qrcode==7.4.*",
"redis==4.5.*,>=4.5.4",
"reportlab==4.0.*",
"requests==2.30.*",
"sentry-sdk==1.15.*",
"sepaxml==2.6.*",
"slimit",
"static3==0.7.*",
"stripe==5.4.*",
"text-unidecode==1.*",
"tlds>=2020041600",
"tqdm==4.*",
"vat_moss_forked==2020.3.20.0.11.0",
"vobject==0.9.*",
"webauthn==0.4.*",
"zeep==4.2.*"
]
[project.optional-dependencies]
memcached = ["pylibmc"]
mysql = ["mysqlclient"]
dev = [
"coverage",
"coveralls",
"flake8==6.0.*",
"freezegun",
"isort==5.12.*",
"pep8-naming==0.13.*",
"potypo",
"pycodestyle==2.10.*",
"pyflakes==3.0.*",
"pytest-cache",
"pytest-cov",
"pytest-django==4.*",
"pytest-mock==3.10.*",
"pytest-rerunfailures==11.*",
"pytest-sugar",
"pytest-xdist==3.2.*",
"pytest==7.3.*",
"responses",
]
[project.entry-points."distutils.commands"]
build = "pretix._build:CustomBuild"
build_ext = "pretix._build:CustomBuildExt"
[build-system]
build-backend = "backend"
backend-path = ["_build"]
requires = [
"setuptools",
"setuptools-rust",
"wheel",
"importlib_metadata",
"tomli",
]
[project.urls]
homepage = "https://pretix.eu"
documentation = "https://docs.pretix.eu"
repository = "https://github.com/pretix/pretix.git"
changelog = "https://pretix.eu/about/en/blog/"
[tool.setuptools]
include-package-data = true
[tool.setuptools.dynamic]
version = {attr = "pretix.__version__"}
[tool.setuptools.packages.find]
where = ["src"]
include = ["pretix*"]
namespaces = false

View File

@@ -1,40 +0,0 @@
[check-manifest]
ignore =
env/**
doc/*
deployment/*
res/*
src/.update-locales
src/Makefile
src/manage.py
src/pretix/icons/*
src/pretix/static.dist/**
src/pretix/static/jsi18n/**
src/requirements.txt
src/requirements/*
src/tests/*
src/tests/api/*
src/tests/base/*
src/tests/control/*
src/tests/testdummy/*
src/tests/templates/*
src/tests/presale/*
src/tests/doc/*
src/tests/helpers/*
src/tests/media/*
src/tests/multidomain/*
src/tests/plugins/*
src/tests/plugins/badges/*
src/tests/plugins/banktransfer/*
src/tests/plugins/paypal/*
src/tests/plugins/paypal2/*
src/tests/plugins/pretixdroid/*
src/tests/plugins/stripe/*
src/tests/plugins/sendmail/*
src/tests/plugins/ticketoutputpdf/*
.*
CODE_OF_CONDUCT.md
CONTRIBUTING.md
Dockerfile
SECURITY.md

View File

@@ -1,49 +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 sys
from pathlib import Path
import setuptools
sys.path.append(str(Path.cwd() / 'src'))
def _CustomBuild(*args, **kwargs):
from pretix._build import CustomBuild
return CustomBuild(*args, **kwargs)
def _CustomBuildExt(*args, **kwargs):
from pretix._build import CustomBuildExt
return CustomBuildExt(*args, **kwargs)
cmdclass = {
'build': _CustomBuild,
'build_ext': _CustomBuildExt,
}
if __name__ == "__main__":
setuptools.setup(
cmdclass=cmdclass,
)

33
src/MANIFEST.in Normal file
View File

@@ -0,0 +1,33 @@
include LICENSE
include README.rst
global-include *.proto
recursive-include pretix/static *
recursive-include pretix/static.dist *
recursive-include pretix/locale *
recursive-include pretix/helpers/locale *
recursive-include pretix/base/templates *
recursive-include pretix/control/templates *
recursive-include pretix/presale/templates *
recursive-include pretix/plugins/banktransfer/templates *
recursive-include pretix/plugins/banktransfer/static *
recursive-include pretix/plugins/manualpayment/templates *
recursive-include pretix/plugins/manualpayment/static *
recursive-include pretix/plugins/paypal/templates *
recursive-include pretix/plugins/paypal/static *
recursive-include pretix/plugins/paypal2/templates *
recursive-include pretix/plugins/paypal2/static *
recursive-include pretix/plugins/pretixdroid/templates *
recursive-include pretix/plugins/pretixdroid/static *
recursive-include pretix/plugins/sendmail/templates *
recursive-include pretix/plugins/statistics/templates *
recursive-include pretix/plugins/statistics/static *
recursive-include pretix/plugins/stripe/templates *
recursive-include pretix/plugins/stripe/static *
recursive-include pretix/plugins/ticketoutputpdf/templates *
recursive-include pretix/plugins/ticketoutputpdf/static *
recursive-include pretix/plugins/badges/templates *
recursive-include pretix/plugins/badges/static *
recursive-include pretix/plugins/returnurl/templates *
recursive-include pretix/plugins/returnurl/static *
recursive-include pretix/plugins/webcheckin/templates *
recursive-include pretix/plugins/webcheckin/static *

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__ = "4.20.2"
__version__ = "4.17.1"

View File

@@ -1,251 +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 django.conf.locale
from pycountry import currencies
from django.utils.translation import gettext_lazy as _ # NOQA
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
USE_I18N = True
USE_L10N = True
USE_TZ = True
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize',
'pretix.base',
'pretix.control',
'pretix.presale',
'pretix.multidomain',
'pretix.api',
'pretix.helpers',
'rest_framework',
'djangoformsetjs',
'compressor',
'bootstrap3',
'pretix.plugins.banktransfer',
'pretix.plugins.stripe',
'pretix.plugins.paypal',
'pretix.plugins.paypal2',
'pretix.plugins.ticketoutputpdf',
'pretix.plugins.sendmail',
'pretix.plugins.statistics',
'pretix.plugins.reports',
'pretix.plugins.checkinlists',
'pretix.plugins.pretixdroid',
'pretix.plugins.badges',
'pretix.plugins.manualpayment',
'pretix.plugins.returnurl',
'pretix.plugins.webcheckin',
'django_countries',
'oauth2_provider',
'phonenumber_field',
'statici18n',
]
FORMAT_MODULE_PATH = [
'pretix.helpers.formats',
]
ALL_LANGUAGES = [
('en', _('English')),
('de', _('German')),
('de-informal', _('German (informal)')),
('ar', _('Arabic')),
('zh-hans', _('Chinese (simplified)')),
('cs', _('Czech')),
('da', _('Danish')),
('nl', _('Dutch')),
('nl-informal', _('Dutch (informal)')),
('fr', _('French')),
('fi', _('Finnish')),
('gl', _('Galician')),
('el', _('Greek')),
('it', _('Italian')),
('lv', _('Latvian')),
('pl', _('Polish')),
('pt-pt', _('Portuguese (Portugal)')),
('pt-br', _('Portuguese (Brazil)')),
('ro', _('Romanian')),
('ru', _('Russian')),
('es', _('Spanish')),
('tr', _('Turkish')),
('uk', _('Ukrainian')),
]
LANGUAGES_OFFICIAL = {
'en', 'de', 'de-informal'
}
LANGUAGES_RTL = {
'ar', 'hw'
}
LANGUAGES_INCUBATING = {
'pl', 'fi', 'pt-br', 'gl',
}
LOCALE_PATHS = [
os.path.join(os.path.dirname(__file__), 'locale'),
]
EXTRA_LANG_INFO = {
'de-informal': {
'bidi': False,
'code': 'de-informal',
'name': 'German (informal)',
'name_local': 'Deutsch',
'public_code': 'de',
},
'nl-informal': {
'bidi': False,
'code': 'nl-informal',
'name': 'Dutch (informal)',
'name_local': 'Nederlands',
'public_code': 'nl',
},
'fr': {
'bidi': False,
'code': 'fr',
'name': 'French',
'name_local': 'Français'
},
'lv': {
'bidi': False,
'code': 'lv',
'name': 'Latvian',
'name_local': 'Latviešu'
},
'pt-pt': {
'bidi': False,
'code': 'pt-pt',
'name': 'Portuguese',
'name_local': 'Português',
},
}
django.conf.locale.LANG_INFO.update(EXTRA_LANG_INFO)
template_loaders = (
'django.template.loaders.filesystem.Loader',
'pretix.helpers.template_loaders.AppLoader',
)
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, 'templates'),
],
'OPTIONS': {
'context_processors': [
'django.contrib.auth.context_processors.auth',
'django.template.context_processors.debug',
'django.template.context_processors.i18n',
'django.template.context_processors.media',
"django.template.context_processors.request",
'django.template.context_processors.static',
'django.template.context_processors.tz',
'django.contrib.messages.context_processors.messages',
'pretix.base.context.contextprocessor',
'pretix.control.context.contextprocessor',
'pretix.presale.context.contextprocessor',
],
'loaders': template_loaders
},
},
]
STATIC_ROOT = os.path.join(os.path.dirname(__file__), 'static.dist')
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder',
)
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'pretix/static')
] if os.path.exists(os.path.join(BASE_DIR, 'pretix/static')) else []
STATICI18N_ROOT = os.path.join(BASE_DIR, "pretix/static")
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
# if os.path.exists(os.path.join(DATA_DIR, 'static')):
# STATICFILES_DIRS.insert(0, os.path.join(DATA_DIR, 'static'))
COMPRESS_PRECOMPILERS = (
('text/x-scss', 'django_libsass.SassCompiler'),
('text/vue', 'pretix.helpers.compressor.VueCompiler'),
)
COMPRESS_OFFLINE_CONTEXT = {
'basetpl': 'empty.html',
}
COMPRESS_ENABLED = True
COMPRESS_OFFLINE = True
COMPRESS_FILTERS = {
'css': (
# CssAbsoluteFilter is incredibly slow, especially when dealing with our _flags.scss
# However, we don't need it if we consequently use the static() function in Sass
# 'compressor.filters.css_default.CssAbsoluteFilter',
'compressor.filters.cssmin.rCSSMinFilter',
),
'js': (
'compressor.filters.jsmin.JSMinFilter',
)
}
CURRENCIES = list(currencies)
CURRENCY_PLACES = {
# default is 2
'BIF': 0,
'CLP': 0,
'DJF': 0,
'GNF': 0,
'JPY': 0,
'KMF': 0,
'KRW': 0,
'MGA': 0,
'PYG': 0,
'RWF': 0,
'VND': 0,
'VUV': 0,
'XAF': 0,
'XOF': 0,
'XPF': 0,
}
PRETIX_EMAIL_NONE_VALUE = 'none@well-known.pretix.eu'
PRETIX_PRIMARY_COLOR = '#8E44B3'
# pretix includes caching options for some special situations where full HTML responses are cached. This might be
# stressful for some cache setups so it is enabled by default and currently can't be enabled through pretix.cfg
CACHE_LARGE_VALUES_ALLOWED = False
CACHE_LARGE_VALUES_ALIAS = 'default'

View File

@@ -1,82 +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 shutil
import subprocess
from setuptools.command.build import build
from setuptools.command.build_ext import build_ext
here = os.path.abspath(os.path.dirname(__file__))
npm_installed = False
def npm_install():
global npm_installed
if not npm_installed:
# keep this in sync with Makefile!
node_prefix = os.path.join(here, 'static.dist', 'node_prefix')
os.makedirs(node_prefix, exist_ok=True)
shutil.copytree(os.path.join(here, 'static', 'npm_dir'), node_prefix, dirs_exist_ok=True)
subprocess.check_call('npm install', shell=True, cwd=node_prefix)
npm_installed = True
class CustomBuild(build):
def run(self):
if "src" not in os.listdir(".") or "pretix" not in os.listdir("src"):
# Only run this command on the pretix module, not on other modules even if it's registered globally
# in some cases
return build.run(self)
if "PRETIX_DOCKER_BUILD" in os.environ:
return # this is a hack to allow calling this file early in our docker build to make use of caching
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix._build_settings")
os.environ.setdefault("PRETIX_IGNORE_CONFLICTS", "True")
import django
django.setup()
from django.conf import settings
from django.core import management
settings.COMPRESS_ENABLED = True
settings.COMPRESS_OFFLINE = True
npm_install()
management.call_command('compilemessages', verbosity=1)
management.call_command('compilejsi18n', verbosity=1)
management.call_command('collectstatic', verbosity=1, interactive=False)
management.call_command('compress', verbosity=1)
build.run(self)
class CustomBuildExt(build_ext):
def run(self):
if "src" not in os.listdir(".") or "pretix" not in os.listdir("src"):
# Only run this command on the pretix module, not on other modules even if it's registered globally
# in some cases
return build_ext.run(self)
if "PRETIX_DOCKER_BUILD" in os.environ:
return # this is a hack to allow calling this file early in our docker build to make use of caching
npm_install()
build_ext.run(self)

View File

@@ -1,48 +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/>.
#
"""
This file contains settings that we need at wheel require time. All settings that we only need at runtime are set
in settings.py.
"""
from ._base_settings import * # NOQA
ENTROPY = {
'order_code': 5,
'customer_identifier': 7,
'ticket_secret': 32,
'voucher_code': 16,
'giftcard_secret': 12,
}
MAIL_FROM_ORGANIZERS = 'invalid@invalid'
FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT = 10
FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT = 10
FILE_UPLOAD_MAX_SIZE_IMAGE = 10
DEFAULT_CURRENCY = 'EUR'
SECRET_KEY = "build-time-secret-key"
HAS_REDIS = False
STATIC_URL = '/static/'
HAS_MEMCACHED = False
HAS_CELERY = False
HAS_GEOIP = False
SENTRY_ENABLED = False

View File

@@ -81,7 +81,6 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
('GET', 'api-v1:checkinrpc.search'),
('GET', 'api-v1:reusablemedium-list'),
)
@@ -201,7 +200,6 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('DELETE', 'api-v1:cartposition-detail'),
('GET', 'api-v1:giftcard-list'),
('POST', 'api-v1:giftcard-transact'),
('PATCH', 'api-v1:giftcard-detail'),
('GET', 'plugins:pretix_posbackend:posclosing-list'),
('POST', 'plugins:pretix_posbackend:posreceipt-list'),
('POST', 'plugins:pretix_posbackend:posclosing-list'),
@@ -222,7 +220,6 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
('GET', 'api-v1:checkinrpc.search'),
('POST', 'api-v1:reusablemedium-lookup'),
)

View File

@@ -19,30 +19,3 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from rest_framework import serializers
class AsymmetricField(serializers.Field):
def __init__(self, read, write, **kwargs):
self.read = read
self.write = write
super().__init__(
required=self.write.required,
default=self.write.default,
initial=self.write.initial,
source=self.write.source if self.write.source != self.write.field_name else None,
label=self.write.label,
allow_null=self.write.allow_null,
error_messages=self.write.error_messages,
validators=self.write.validators,
**kwargs
)
def to_internal_value(self, data):
return self.write.to_internal_value(data)
def to_representation(self, value):
return self.read.to_representation(value)
def run_validation(self, data=serializers.empty):
return self.write.run_validation(data)

View File

@@ -26,7 +26,6 @@ from rest_framework.exceptions import ValidationError
from pretix.api.serializers.event import SubEventSerializer
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.channels import get_all_sales_channels
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import Checkin, CheckinList
@@ -85,7 +84,6 @@ class CheckinRPCRedeemInputSerializer(serializers.Serializer):
lists = serializers.PrimaryKeyRelatedField(required=True, many=True, queryset=CheckinList.objects.none())
secret = serializers.CharField(required=True, allow_null=False)
force = serializers.BooleanField(default=False, required=False)
source_type = serializers.ChoiceField(choices=[(k, v) for k, v in MEDIA_TYPES.items()], default='barcode')
type = serializers.ChoiceField(choices=Checkin.CHECKIN_TYPES, default=Checkin.TYPE_ENTRY)
ignore_unpaid = serializers.BooleanField(default=False, required=False)
questions_supported = serializers.BooleanField(default=True, required=False)

View File

@@ -50,9 +50,7 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.settings import SettingsSerializer
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
from pretix.base.models.event import SubEvent
from pretix.base.models.items import (
ItemMetaProperty, SubEventItem, SubEventItemVariation,
)
from pretix.base.models.items import SubEventItem, SubEventItemVariation
from pretix.base.services.seating import (
SeatProtected, generate_seats, validate_plan_change,
)
@@ -685,7 +683,6 @@ class EventSettingsSerializer(SettingsSerializer):
'waiting_list_phones_asked',
'waiting_list_phones_required',
'waiting_list_phones_explanation_text',
'waiting_list_limit_per_user',
'max_items_per_order',
'reservation_time',
'contact_mail',
@@ -696,7 +693,6 @@ class EventSettingsSerializer(SettingsSerializer):
'frontpage_subevent_ordering',
'event_list_type',
'event_list_available_only',
'event_calendar_future_only',
'frontpage_text',
'event_info_text',
'attendee_names_asked',
@@ -768,7 +764,6 @@ class EventSettingsSerializer(SettingsSerializer):
'invoice_footer_text',
'invoice_eu_currencies',
'invoice_logo_image',
'invoice_renderer_highlight_order_code',
'cancel_allow_user',
'cancel_allow_user_until',
'cancel_allow_user_unpaid_keep',
@@ -789,7 +784,6 @@ class EventSettingsSerializer(SettingsSerializer):
'change_allow_user_addons',
'change_allow_user_until',
'change_allow_user_price',
'change_allow_attendee',
'primary_color',
'theme_color_success',
'theme_color_danger',
@@ -801,21 +795,6 @@ class EventSettingsSerializer(SettingsSerializer):
'logo_show_title',
'og_image',
'name_scheme',
'reusable_media_active',
'reusable_media_type_barcode',
'reusable_media_type_barcode_identifier_length',
'reusable_media_type_nfc_uid',
'reusable_media_type_nfc_uid_autocreate_giftcard',
'reusable_media_type_nfc_uid_autocreate_giftcard_currency',
]
readonly_fields = [
# These are read-only since they are currently only settable on organizers, not events
'reusable_media_active',
'reusable_media_type_barcode',
'reusable_media_type_barcode_identifier_length',
'reusable_media_type_nfc_uid',
'reusable_media_type_nfc_uid_autocreate_giftcard',
'reusable_media_type_nfc_uid_autocreate_giftcard_currency',
]
def __init__(self, *args, **kwargs):
@@ -882,9 +861,6 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
'invoice_address_from_tax_id',
'invoice_address_from_vat_id',
'name_scheme',
'reusable_media_type_barcode',
'reusable_media_type_nfc_uid',
'system_question_order',
]
def __init__(self, *args, **kwargs):
@@ -906,23 +882,3 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
else []
)
)
class MultiLineStringField(serializers.Field):
def to_representation(self, value):
return [v.strip() for v in value.splitlines()]
def to_internal_value(self, data):
if isinstance(data, list) and len(data) > 0:
return "\n".join(data)
else:
raise ValidationError('Invalid data type.')
class ItemMetaPropertiesSerializer(I18nAwareModelSerializer):
allowed_values = MultiLineStringField(allow_null=True)
class Meta:
model = ItemMetaProperty
fields = ('id', 'name', 'default', 'required', 'allowed_values')

View File

@@ -20,7 +20,6 @@
# <https://www.gnu.org/licenses/>.
#
from django.conf import settings
from django.core.validators import URLValidator
from i18nfield.fields import I18nCharField, I18nTextField
from i18nfield.strings import LazyI18nString
from rest_framework.exceptions import ValidationError
@@ -70,17 +69,3 @@ class I18nAwareModelSerializer(ModelSerializer):
I18nAwareModelSerializer.serializer_field_mapping[I18nCharField] = I18nField
I18nAwareModelSerializer.serializer_field_mapping[I18nTextField] = I18nField
class I18nURLField(I18nField):
def to_internal_value(self, value):
value = super().to_internal_value(value)
if not value:
return value
if isinstance(value.data, dict):
for v in value.data.values():
if v:
URLValidator()(v)
else:
URLValidator()(value.data)
return value

View File

@@ -52,7 +52,7 @@ from pretix.base.models import (
class InlineItemVariationSerializer(I18nAwareModelSerializer):
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=10,
coerce_to_string=True)
meta_data = MetaDataField(required=False, source='*')
@@ -76,7 +76,7 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
class ItemVariationSerializer(I18nAwareModelSerializer):
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=10,
coerce_to_string=True)
meta_data = MetaDataField(required=False, source='*')
@@ -244,8 +244,7 @@ class ItemSerializer(I18nAwareModelSerializer):
'grant_membership_duration_like_event', 'grant_membership_duration_days',
'grant_membership_duration_months', 'validity_mode', 'validity_fixed_from', 'validity_fixed_until',
'validity_dynamic_duration_minutes', 'validity_dynamic_duration_hours', 'validity_dynamic_duration_days',
'validity_dynamic_duration_months', 'validity_dynamic_start_choice', 'validity_dynamic_start_choice_day_limit',
'media_policy', 'media_type')
'validity_dynamic_duration_months', 'validity_dynamic_start_choice', 'validity_dynamic_start_choice_day_limit')
read_only_fields = ('has_variations',)
def __init__(self, *args, **kwargs):
@@ -264,7 +263,6 @@ class ItemSerializer(I18nAwareModelSerializer):
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
Item.clean_available(data.get('available_from'), data.get('available_until'))
Item.clean_media_settings(self.context['event'], data.get('media_policy'), data.get('media_type'), data.get('issue_giftcard'))
if data.get('personalized') and not data.get('admission'):
raise ValidationError(_('Only admission products can currently be personalized.'))
@@ -306,9 +304,9 @@ class ItemSerializer(I18nAwareModelSerializer):
if not self.instance:
for addon_data in value:
ItemAddOn.clean_categories(self.context['event'], None, self.instance, addon_data['addon_category'])
ItemAddOn.clean_min_count(addon_data.get('min_count', 0))
ItemAddOn.clean_max_count(addon_data.get('max_count', 0))
ItemAddOn.clean_max_min_count(addon_data.get('max_count', 0), addon_data.get('min_count', 0))
ItemAddOn.clean_min_count(addon_data['min_count'])
ItemAddOn.clean_max_count(addon_data['max_count'])
ItemAddOn.clean_max_min_count(addon_data['max_count'], addon_data['min_count'])
return value
@cached_property
@@ -442,7 +440,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
'hidden', 'dependency_value', 'print_on_invoice', 'help_text', 'valid_number_min',
'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max',
'valid_string_length_max', 'valid_file_portrait')
'valid_file_portrait')
def validate_identifier(self, value):
Question._clean_identifier(self.context['event'], value, self.instance)

View File

@@ -1,130 +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 logging
from decimal import Decimal
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import OrderPositionSerializer
from pretix.api.serializers.organizer import (
CustomerSerializer, GiftCardSerializer,
)
from pretix.base.models import Order, OrderPosition, ReusableMedium
logger = logging.getLogger(__name__)
class NestedOrderMiniSerializer(I18nAwareModelSerializer):
event = serializers.SlugRelatedField(slug_field='slug', read_only=True)
class Meta:
model = Order
fields = ['code', 'event']
class NestedOrderPositionSerializer(OrderPositionSerializer):
order = NestedOrderMiniSerializer()
class NestedGiftCardSerializer(GiftCardSerializer):
def to_representation(self, instance):
d = super().to_representation(instance)
if hasattr(instance, 'cached_value'):
d['value'] = str(Decimal(instance.cached_value).quantize(Decimal("0.01")))
else:
d['value'] = str(Decimal(instance.value).quantize(Decimal("0.01")))
return d
class ReusableMediaSerializer(I18nAwareModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'linked_giftcard' in self.context['request'].query_params.getlist('expand'):
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True, context=self.context)
if 'linked_giftcard.owner_ticket' in self.context['request'].query_params.getlist('expand'):
self.fields['linked_giftcard'].fields['owner_ticket'] = NestedOrderPositionSerializer(read_only=True, context=self.context)
else:
self.fields['linked_giftcard'] = serializers.PrimaryKeyRelatedField(
required=False,
allow_null=True,
queryset=self.context['organizer'].issued_gift_cards.all()
)
if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'):
self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True)
else:
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
required=False,
allow_null=True,
queryset=OrderPosition.all.filter(order__event__organizer=self.context['organizer']),
)
if 'customer' in self.context['request'].query_params.getlist('expand'):
self.fields['customer'] = CustomerSerializer(read_only=True)
else:
self.fields['customer'] = serializers.SlugRelatedField(
required=False,
allow_null=True,
slug_field='identifier',
queryset=self.context['organizer'].customers.all()
)
def validate(self, data):
data = super().validate(data)
if 'type' in data and 'identifier' in data:
qs = self.context['organizer'].reusable_media.filter(
identifier=data['identifier'], type=data['type']
)
if self.instance:
qs = qs.exclude(pk=self.instance.pk)
if qs.exists():
raise ValidationError(
{'identifier': _('A medium with the same identifier and type already exists in your organizer account.')}
)
return data
class Meta:
model = ReusableMedium
fields = (
'id',
'created',
'updated',
'type',
'identifier',
'active',
'expires',
'customer',
'linked_orderposition',
'linked_giftcard',
'info',
'notes',
)
class MediaLookupInputSerializer(serializers.Serializer):
type = serializers.CharField(required=True)
identifier = serializers.CharField(required=True)

View File

@@ -33,7 +33,6 @@ from django.utils.encoding import force_str
from django.utils.timezone import now
from django.utils.translation import gettext_lazy
from django_countries.fields import Country
from django_scopes import scopes_disabled
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.relations import SlugRelatedField
@@ -49,8 +48,8 @@ from pretix.base.decimal import round_decimal
from pretix.base.i18n import language
from pretix.base.models import (
CachedFile, Checkin, Customer, Invoice, InvoiceAddress, InvoiceLine, Item,
ItemVariation, Order, OrderPosition, Question, QuestionAnswer,
ReusableMedium, Seat, SubEvent, TaxRule, Voucher,
ItemVariation, Order, OrderPosition, Question, QuestionAnswer, Seat,
SubEvent, TaxRule, Voucher,
)
from pretix.base.models.orders import (
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
@@ -357,9 +356,6 @@ class PdfDataSerializer(serializers.Field):
def to_representation(self, instance: OrderPosition):
res = {}
if 'event' not in self.context:
return {}
ev = instance.subevent or instance.order.event
with language(instance.order.locale, instance.order.event.settings.region):
# This needs to have some extra performance improvements to avoid creating hundreds of queries when
@@ -385,9 +381,11 @@ class PdfDataSerializer(serializers.Field):
res['meta:' + k] = v
if instance.variation_id:
print(instance, instance.variation, instance.variation_id, instance.item)
if not hasattr(instance.variation, '_cached_meta_data'):
instance.variation.item = instance.item # saves some database lookups
instance.variation._cached_meta_data = instance.variation.meta_data
print(instance.variation._cached_meta_data.items())
for k, v in instance.variation._cached_meta_data.items():
res['itemmeta:' + k] = v
else:
@@ -783,20 +781,18 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
attendee_name = serializers.CharField(required=False, allow_null=True)
seat = serializers.CharField(required=False, allow_null=True)
price = serializers.DecimalField(required=False, allow_null=True, decimal_places=2,
max_digits=13)
max_digits=10)
voucher = serializers.SlugRelatedField(slug_field='code', queryset=Voucher.objects.none(),
required=False, allow_null=True)
country = CompatibleCountryField(source='*')
requested_valid_from = serializers.DateTimeField(required=False, allow_null=True)
use_reusable_medium = serializers.PrimaryKeyRelatedField(queryset=ReusableMedium.objects.none(),
required=False, allow_null=True)
class Meta:
model = OrderPosition
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'company', 'street', 'zipcode', 'city', 'country', 'state', 'is_bundled',
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher', 'valid_from', 'valid_until',
'requested_valid_from', 'use_reusable_medium')
'requested_valid_from')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -805,9 +801,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
v.required = False
v.allow_blank = True
v.allow_null = True
with scopes_disabled():
if 'use_reusable_medium' in self.fields:
self.fields['use_reusable_medium'].queryset = ReusableMedium.objects.all()
def validate_secret(self, secret):
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
@@ -816,13 +809,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
)
return secret
def validate_use_reusable_medium(self, m):
if m.organizer_id != self.context['event'].organizer_id:
raise ValidationError(
'The specified medium does not belong to this organizer.'
)
return m
def validate_item(self, item):
if item.event != self.context['event']:
raise ValidationError(
@@ -1280,7 +1266,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
pos_data['attendee_name_parts'] = {
'_legacy': attendee_name
}
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k != 'answers' and k != '_quotas' and k != 'use_reusable_medium'})
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k != 'answers' and k != '_quotas'})
if simulate:
pos.order = order._wrapped
else:
@@ -1348,7 +1334,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
# Save instances
for pos_data in positions_data:
answers_data = pos_data.pop('answers', [])
use_reusable_medium = pos_data.pop('use_reusable_medium', None)
pos = pos_data['__instance']
pos._calculate_tax()
@@ -1387,17 +1372,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
answ = pos.answers.create(**answ_data)
answ.options.add(*options)
if use_reusable_medium:
use_reusable_medium.linked_orderposition = pos
use_reusable_medium.save(update_fields=['linked_orderposition'])
use_reusable_medium.log_action(
'pretix.reusable_medium.linked_orderposition.changed',
data={
'by_order': order.code,
'linked_orderposition': pos.pk,
}
)
if not simulate:
for cp in delete_cps:
if cp.addon_to_id:

View File

@@ -47,7 +47,7 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
attendee_name = serializers.CharField(required=False, allow_null=True)
seat = serializers.CharField(required=False, allow_null=True)
price = serializers.DecimalField(required=False, allow_null=True, decimal_places=2,
max_digits=13)
max_digits=10)
country = CompatibleCountryField(source='*')
class Meta:

View File

@@ -22,14 +22,12 @@
import logging
from decimal import Decimal
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.serializers import AsymmetricField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import CompatibleJSONField
from pretix.api.serializers.settings import SettingsSerializer
@@ -37,8 +35,8 @@ from pretix.base.auth import get_auth_backends
from pretix.base.i18n import get_language_without_region
from pretix.base.models import (
Customer, Device, GiftCard, GiftCardTransaction, Membership,
MembershipType, OrderPosition, Organizer, ReusableMedium, SeatingPlan,
Team, TeamAPIToken, TeamInvite, User,
MembershipType, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
User,
)
from pretix.base.models.seating import SeatingPlanLayoutValidator
from pretix.base.services.mail import SendMailException, mail
@@ -129,52 +127,8 @@ class MembershipSerializer(I18nAwareModelSerializer):
return super().update(instance, validated_data)
class FlexibleTicketRelatedField(serializers.PrimaryKeyRelatedField):
def to_internal_value(self, data):
queryset = self.get_queryset()
if isinstance(data, int):
try:
return queryset.get(pk=data)
except ObjectDoesNotExist:
self.fail('does_not_exist', pk_value=data)
elif isinstance(data, str):
try:
return queryset.get(
Q(secret=data)
| Q(pseudonymization_id=data)
| Q(pk__in=ReusableMedium.objects.filter(
organizer=self.context['organizer'],
type='barcode',
identifier=data
))
)
except ObjectDoesNotExist:
self.fail('does_not_exist', pk_value=data)
self.fail('incorrect_type', data_type=type(data).__name__)
class GiftCardSerializer(I18nAwareModelSerializer):
value = serializers.DecimalField(max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
owner_ticket = FlexibleTicketRelatedField(required=False, allow_null=True, queryset=OrderPosition.all.none())
issuer = serializers.SlugRelatedField(slug_field='slug', read_only=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['owner_ticket'].queryset = OrderPosition.objects.filter(order__event__organizer=self.context['organizer'])
if 'owner_ticket' in self.context['request'].query_params.getlist('expand'):
from pretix.api.serializers.media import (
NestedOrderPositionSerializer,
)
self.fields['owner_ticket'] = AsymmetricField(
NestedOrderPositionSerializer(read_only=True, context=self.context),
self.fields['owner_ticket'],
)
value = serializers.DecimalField(max_digits=10, decimal_places=2, min_value=Decimal('0.00'))
def validate(self, data):
data = super().validate(data)
@@ -197,8 +151,7 @@ class GiftCardSerializer(I18nAwareModelSerializer):
class Meta:
model = GiftCard
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions', 'owner_ticket',
'issuer')
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions')
class OrderEventSlugField(serializers.RelatedField):
@@ -209,12 +162,11 @@ class OrderEventSlugField(serializers.RelatedField):
class GiftCardTransactionSerializer(I18nAwareModelSerializer):
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
acceptor = serializers.SlugRelatedField(slug_field='slug', read_only=True)
event = OrderEventSlugField(source='order', read_only=True)
class Meta:
model = GiftCardTransaction
fields = ('id', 'datetime', 'value', 'event', 'order', 'text', 'info', 'acceptor')
fields = ('id', 'datetime', 'value', 'event', 'order', 'text')
class EventSlugField(serializers.SlugRelatedField):
@@ -231,7 +183,7 @@ class TeamSerializer(serializers.ModelSerializer):
'id', 'name', 'all_events', 'limit_events', 'can_create_events', 'can_change_teams',
'can_change_organizer_settings', 'can_manage_gift_cards', 'can_change_event_settings',
'can_change_items', 'can_view_orders', 'can_change_orders', 'can_view_vouchers',
'can_change_vouchers', 'can_checkin_orders', 'can_manage_customers', 'can_manage_reusable_media'
'can_change_vouchers', 'can_checkin_orders', 'can_manage_customers'
)
def validate(self, data):
@@ -381,12 +333,6 @@ class OrganizerSettingsSerializer(SettingsSerializer):
'cookie_consent_dialog_text_secondary',
'cookie_consent_dialog_button_yes',
'cookie_consent_dialog_button_no',
'reusable_media_active',
'reusable_media_type_barcode',
'reusable_media_type_barcode_identifier_length',
'reusable_media_type_nfc_uid',
'reusable_media_type_nfc_uid_autocreate_giftcard',
'reusable_media_type_nfc_uid_autocreate_giftcard_currency',
]
def __init__(self, *args, **kwargs):

View File

@@ -36,7 +36,6 @@ logger = logging.getLogger(__name__)
class SettingsSerializer(serializers.Serializer):
default_fields = []
readonly_fields = []
def __init__(self, *args, **kwargs):
self.changed_data = []
@@ -60,13 +59,8 @@ class SettingsSerializer(serializers.Serializer):
f.parent = self
self.fields[fname] = f
def validate(self, attrs):
return {k: v for k, v in attrs.items() if k not in self.readonly_fields}
def update(self, instance: HierarkeyProxy, validated_data):
for attr, value in validated_data.items():
if attr in self.readonly_fields:
continue
if isinstance(value, FieldFile):
# Delete old file
fname = instance.get(attr, as_type=File)

View File

@@ -63,8 +63,7 @@ class VoucherSerializer(I18nAwareModelSerializer):
model = Voucher
fields = ('id', 'code', 'max_usages', 'redeemed', 'min_usages', 'valid_until', 'block_quota',
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat', 'all_addons_included',
'all_bundles_included')
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat')
read_only_fields = ('id', 'redeemed')
list_serializer_class = VoucherListSerializer

View File

@@ -39,7 +39,7 @@ class WaitingListSerializer(I18nAwareModelSerializer):
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
WaitingListEntry.clean_duplicate(event, full_data.get('email'), full_data.get('item'), full_data.get('variation'),
WaitingListEntry.clean_duplicate(full_data.get('email'), full_data.get('item'), full_data.get('variation'),
full_data.get('subevent'), self.instance.pk if self.instance else None)
WaitingListEntry.clean_itemvar(event, full_data.get('item'), full_data.get('variation'))
WaitingListEntry.clean_subevent(event, full_data.get('subevent'))

View File

@@ -42,9 +42,9 @@ from rest_framework import routers
from pretix.api.views import cart
from .views import (
checkin, device, discount, event, exporters, idempotency, item, media,
oauth, order, organizer, shredders, upload, user, version, voucher,
waitinglist, webhooks,
checkin, device, discount, event, exporters, idempotency, item, oauth,
order, organizer, shredders, upload, user, version, voucher, waitinglist,
webhooks,
)
router = routers.DefaultRouter()
@@ -59,7 +59,6 @@ orga_router.register(r'giftcards', organizer.GiftCardViewSet)
orga_router.register(r'customers', organizer.CustomerViewSet)
orga_router.register(r'memberships', organizer.MembershipViewSet)
orga_router.register(r'membershiptypes', organizer.MembershipTypeViewSet)
orga_router.register(r'reusablemedia', media.ReusableMediaViewSet)
orga_router.register(r'teams', organizer.TeamViewSet)
orga_router.register(r'devices', organizer.DeviceViewSet)
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
@@ -89,7 +88,6 @@ event_router.register(r'checkinlists', checkin.CheckinListViewSet)
event_router.register(r'cartpositions', cart.CartPositionViewSet)
event_router.register(r'exporters', exporters.EventExportersViewSet, basename='exporters')
event_router.register(r'shredders', shredders.EventShreddersViewSet, basename='shredders')
event_router.register(r'item_meta_properties', event.ItemMetaPropertiesViewSet)
checkinlist_router = routers.DefaultRouter()
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos')

View File

@@ -59,7 +59,7 @@ from pretix.api.views.order import OrderPositionFilter
from pretix.base.i18n import language
from pretix.base.models import (
CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition,
Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken,
Question, RevokedTicketSecret, TeamAPIToken,
)
from pretix.base.services.checkin import (
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
@@ -396,7 +396,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce,
untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported,
source_type='barcode', legacy_url_support=False):
legacy_url_support=False):
if not checkinlists:
raise ValidationError('No check-in list passed.')
@@ -422,7 +422,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
common_checkin_args = dict(
raw_barcode=raw_barcode,
raw_source_type=source_type,
type=checkin_type,
list=checkinlists[0],
datetime=datetime,
@@ -434,7 +433,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
raw_barcode_for_checkin = None
from_revoked_secret = False
# 1. Gather a list of positions that could be the one we looking for, either from their ID, secret or
# 1. Gather a list of positions that could be the one we looking fore, either from their ID, secret or
# parent secret
queryset = _checkin_list_position_queryset(checkinlists, pdf_data=pdf_data, ignore_status=True, ignore_products=True).order_by(
F('addon_to').asc(nulls_first=True)
@@ -458,113 +457,98 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
# 2. Handle the "nothing found" case: Either it's really a bogus secret that we don't know (-> error), or it
# might be a revoked one that we actually know (-> error, but with better error message and logging and
# with respecting the force option), or it's a reusable medium (-> proceed with that)
# with respecting the force option).
if not op_candidates:
try:
media = ReusableMedium.objects.select_related('linked_orderposition').active().get(
organizer_id=checkinlists[0].event.organizer_id,
type=source_type,
identifier=raw_barcode,
linked_orderposition__isnull=False,
)
raw_barcode_for_checkin = raw_barcode
except ReusableMedium.DoesNotExist:
revoked_matches = list(
RevokedTicketSecret.objects.filter(event_id__in=list_by_event.keys(), secret=raw_barcode))
if len(revoked_matches) == 0:
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
'datetime': datetime,
'type': checkin_type,
'list': checkinlists[0].pk,
'barcode': raw_barcode,
'searched_lists': [cl.pk for cl in checkinlists]
}, user=user, auth=auth)
revoked_matches = list(RevokedTicketSecret.objects.filter(event_id__in=list_by_event.keys(), secret=raw_barcode))
if len(revoked_matches) == 0:
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
'datetime': datetime,
'type': checkin_type,
'list': checkinlists[0].pk,
'barcode': raw_barcode,
'searched_lists': [cl.pk for cl in checkinlists]
}, user=user, auth=auth)
for cl in checkinlists:
for k, s in cl.event.ticket_secret_generators.items():
try:
parsed = s.parse_secret(raw_barcode)
common_checkin_args.update({
'raw_item': parsed.item,
'raw_variation': parsed.variation,
'raw_subevent': parsed.subevent,
})
except:
pass
Checkin.objects.create(
position=None,
successful=False,
error_reason=Checkin.REASON_INVALID,
**common_checkin_args,
)
if force and legacy_url_support and isinstance(auth, Device):
# There was a bug in libpretixsync: If you scanned a ticket in offline mode that was
# valid at the time but no longer exists at time of upload, the device would retry to
# upload the same scan over and over again. Since we can't update all devices quickly,
# here's a dirty workaround to make it stop.
for cl in checkinlists:
for k, s in cl.event.ticket_secret_generators.items():
try:
brand = auth.software_brand
ver = parse(auth.software_version)
legacy_mode = (
(brand == 'pretixSCANPROXY' and ver < parse('0.0.3')) or
(brand == 'pretixSCAN Android' and ver < parse('1.11.2')) or
(brand == 'pretixSCAN' and ver < parse('1.11.2'))
)
if legacy_mode:
return Response({
'status': 'error',
'reason': Checkin.REASON_ALREADY_REDEEMED,
'reason_explanation': None,
'require_attention': False,
'__warning': 'Compatibility hack active due to detected old pretixSCAN version',
}, status=400)
except: # we don't care e.g. about invalid version numbers
parsed = s.parse_secret(raw_barcode)
common_checkin_args.update({
'raw_item': parsed.item,
'raw_variation': parsed.variation,
'raw_subevent': parsed.subevent,
})
except:
pass
return Response({
'detail': 'Not found.', # for backwards compatibility
'status': 'error',
'reason': Checkin.REASON_INVALID,
'reason_explanation': None,
'require_attention': False,
'list': MiniCheckinListSerializer(checkinlists[0]).data,
}, status=404)
elif revoked_matches and force:
op_candidates = [revoked_matches[0].position]
if list_by_event[revoked_matches[0].event_id].addon_match:
op_candidates += list(revoked_matches[0].position.addons.all())
raw_barcode_for_checkin = raw_barcode_for_checkin or raw_barcode
from_revoked_secret = True
else:
op = revoked_matches[0].position
op.order.log_action('pretix.event.checkin.revoked', data={
'datetime': datetime,
'type': checkin_type,
'list': list_by_event[revoked_matches[0].event_id].pk,
'barcode': raw_barcode
}, user=user, auth=auth)
common_checkin_args['list'] = list_by_event[revoked_matches[0].event_id]
Checkin.objects.create(
position=op,
successful=False,
error_reason=Checkin.REASON_REVOKED,
**common_checkin_args
)
return Response({
'status': 'error',
'reason': Checkin.REASON_REVOKED,
'reason_explanation': None,
'require_attention': False,
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, revoked_matches[
0].event)).data,
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
}, status=400)
Checkin.objects.create(
position=None,
successful=False,
error_reason=Checkin.REASON_INVALID,
**common_checkin_args,
)
if force and legacy_url_support and isinstance(auth, Device):
# There was a bug in libpretixsync: If you scanned a ticket in offline mode that was
# valid at the time but no longer exists at time of upload, the device would retry to
# upload the same scan over and over again. Since we can't update all devices quickly,
# here's a dirty workaround to make it stop.
try:
brand = auth.software_brand
ver = parse(auth.software_version)
legacy_mode = (
(brand == 'pretixSCANPROXY' and ver < parse('0.0.3')) or
(brand == 'pretixSCAN Android' and ver < parse('1.11.2')) or
(brand == 'pretixSCAN' and ver < parse('1.11.2'))
)
if legacy_mode:
return Response({
'status': 'error',
'reason': Checkin.REASON_ALREADY_REDEEMED,
'reason_explanation': None,
'require_attention': False,
'__warning': 'Compatibility hack active due to detected old pretixSCAN version',
}, status=400)
except: # we don't care e.g. about invalid version numbers
pass
return Response({
'detail': 'Not found.', # for backwards compatibility
'status': 'error',
'reason': Checkin.REASON_INVALID,
'reason_explanation': None,
'require_attention': False,
'list': MiniCheckinListSerializer(checkinlists[0]).data,
}, status=404)
elif revoked_matches and force:
op_candidates = [revoked_matches[0].position]
if list_by_event[revoked_matches[0].event_id].addon_match:
op_candidates += list(revoked_matches[0].position.addons.all())
raw_barcode_for_checkin = raw_barcode
from_revoked_secret = True
else:
op_candidates = [media.linked_orderposition]
if list_by_event[media.linked_orderposition.order.event_id].addon_match:
op_candidates += list(media.linked_orderposition.addons.all())
op = revoked_matches[0].position
op.order.log_action('pretix.event.checkin.revoked', data={
'datetime': datetime,
'type': checkin_type,
'list': list_by_event[revoked_matches[0].event_id].pk,
'barcode': raw_barcode
}, user=user, auth=auth)
common_checkin_args['list'] = list_by_event[revoked_matches[0].event_id]
Checkin.objects.create(
position=op,
successful=False,
error_reason=Checkin.REASON_REVOKED,
**common_checkin_args
)
return Response({
'status': 'error',
'reason': Checkin.REASON_REVOKED,
'reason_explanation': None,
'require_attention': False,
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, revoked_matches[0].event)).data,
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
}, status=400)
# 3. Handle the "multiple options found" case: Except for the unlikely case of a secret being also a valid primary
# key on the same list, we're probably dealing with the ``addon_match`` case here and need to figure out
@@ -650,7 +634,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
auth=auth,
type=checkin_type,
raw_barcode=raw_barcode_for_checkin,
raw_source_type=source_type,
from_revoked_secret=from_revoked_secret,
)
except RequiredQuestionsError as e:
@@ -829,7 +812,6 @@ class CheckinRPCRedeemView(views.APIView):
return _redeem_process(
checkinlists=s.validated_data['lists'],
raw_barcode=s.validated_data['secret'],
source_type=s.validated_data['source_type'],
answers_data=s.validated_data.get('answers'),
datetime=s.validated_data.get('datetime') or now(),
force=s.validated_data['force'],

View File

@@ -47,13 +47,11 @@ from pretix.api.auth.permission import EventCRUDPermission
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.event import (
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
EventSettingsSerializer, ItemMetaPropertiesSerializer, SubEventSerializer,
TaxRuleSerializer,
EventSettingsSerializer, SubEventSerializer, TaxRuleSerializer,
)
from pretix.api.views import ConditionalListView
from pretix.base.models import (
CartPosition, Device, Event, ItemMetaProperty, SeatCategoryMapping,
TaxRule, TeamAPIToken,
CartPosition, Device, Event, SeatCategoryMapping, TaxRule, TeamAPIToken,
)
from pretix.base.models.event import SubEvent
from pretix.base.services.quotas import QuotaAvailability
@@ -74,7 +72,7 @@ with scopes_disabled():
class Meta:
model = Event
fields = ['is_public', 'live', 'has_subevents', 'testmode']
fields = ['is_public', 'live', 'has_subevents']
def ends_after_qs(self, queryset, name, value):
expr = (
@@ -524,54 +522,6 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
super().perform_destroy(instance)
class ItemMetaPropertiesViewSet(viewsets.ModelViewSet):
serializer_class = ItemMetaPropertiesSerializer
queryset = ItemMetaProperty.objects.none()
write_permission = 'can_change_event_settings'
def get_queryset(self):
qs = self.request.event.item_meta_properties.all()
return qs
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
ctx['event'] = self.request.event
return ctx
@transaction.atomic()
def perform_destroy(self, instance):
instance.log_action(
'pretix.event.item_meta_property.deleted',
user=self.request.user,
auth=self.request.auth,
data={'id': instance.pk}
)
instance.delete()
@transaction.atomic()
def perform_create(self, serializer):
inst = serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.event.item_meta_property.added',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
return inst
@transaction.atomic()
def perform_update(self, serializer):
inst = serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.event.item_meta_property.changed',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
return inst
class EventSettingsView(views.APIView):
permission = None
write_permission = 'can_change_event_settings'
@@ -592,8 +542,7 @@ class EventSettingsView(views.APIView):
fname: {
'value': s.data[fname],
'label': getattr(field, '_label', fname),
'help_text': getattr(field, '_help_text', None),
'readonly': fname in s.readonly_fields,
'help_text': getattr(field, '_help_text', None)
} for fname, field in s.fields.items()
})
return Response(s.data)

View File

@@ -1,160 +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/>.
#
from decimal import Decimal
import django_filters
from django.db import transaction
from django.db.models import OuterRef, Prefetch, Subquery, Sum
from django.db.models.functions import Coalesce
from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import serializers, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import MethodNotAllowed
from rest_framework.filters import OrderingFilter
from rest_framework.response import Response
from pretix.api.serializers.media import (
MediaLookupInputSerializer, ReusableMediaSerializer,
)
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import (
Checkin, GiftCard, GiftCardTransaction, OrderPosition, ReusableMedium,
)
from pretix.helpers import OF_SELF
from pretix.helpers.dicts import merge_dicts
with scopes_disabled():
class ReusableMediumFilter(FilterSet):
identifier = django_filters.CharFilter(field_name='identifier')
type = django_filters.CharFilter(field_name='type')
customer = django_filters.CharFilter(field_name='customer__identifier')
updated_since = django_filters.IsoDateTimeFilter(field_name='updated', lookup_expr='gte')
created_since = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='gte')
class Meta:
model = ReusableMedium
fields = ['identifier', 'type', 'active', 'customer', 'linked_orderposition', 'linked_giftcard']
class ReusableMediaViewSet(viewsets.ModelViewSet):
serializer_class = ReusableMediaSerializer
queryset = ReusableMedium.objects.none()
permission = 'can_manage_reusable_media'
write_permission = 'can_manage_reusable_media'
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering = ('-updated', '-id')
ordering_fields = ('created', 'updated', 'identifier', 'type', 'id')
filterset_class = ReusableMediumFilter
def get_queryset(self):
s = GiftCardTransaction.objects.filter(
card=OuterRef('pk')
).order_by().values('card').annotate(s=Sum('value')).values('s')
return self.request.organizer.reusable_media.prefetch_related(
Prefetch(
'linked_orderposition',
queryset=OrderPosition.objects.select_related(
'order', 'order__event', 'order__event__organizer', 'seat',
).prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.all()),
'answers', 'answers__options', 'answers__question',
)
),
Prefetch(
'linked_giftcard',
queryset=GiftCard.objects.annotate(
cached_value=Coalesce(Subquery(s), Decimal('0.00'))
)
)
)
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
return ctx
@transaction.atomic()
def perform_create(self, serializer):
inst = serializer.save(organizer=self.request.organizer)
inst.log_action(
'pretix.reusable_medium.created',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': inst.pk})
)
@transaction.atomic()
def perform_update(self, serializer):
ReusableMedium.objects.select_for_update(of=OF_SELF).get(pk=self.get_object().pk)
inst = serializer.save(identifier=serializer.instance.identifier, type=serializer.instance.type)
inst.log_action(
'pretix.reusable_medium.changed',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
return inst
def perform_destroy(self, instance):
raise MethodNotAllowed("Media cannot be deleted.")
@action(methods=["POST"], detail=False)
def lookup(self, request, *args, **kwargs):
s = MediaLookupInputSerializer(
data=request.data,
)
s.is_valid(raise_exception=True)
try:
m = ReusableMedium.objects.get(
type=s.validated_data["type"],
identifier=s.validated_data["identifier"],
organizer=request.organizer,
)
s = self.get_serializer(m)
return Response({"result": s.data})
except ReusableMedium.DoesNotExist:
mt = MEDIA_TYPES.get(s.validated_data["type"])
if mt:
m = mt.handle_unknown(request.organizer, s.validated_data["identifier"], request.user, request.auth)
if m:
s = self.get_serializer(m)
return Response({"result": s.data})
return Response({"result": None})
@scopes_disabled() # we are sure enough that get_queryset() is correct, so we save some perforamnce
def list(self, request, **kwargs):
date = serializers.DateTimeField().to_representation(now())
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
resp = self.get_paginated_response(serializer.data)
resp['X-Page-Generated'] = date
return resp
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data, headers={'X-Page-Generated': date})

View File

@@ -112,7 +112,6 @@ class AuthorizationView(RecentAuthenticationRequiredMixin, BaseAuthorizationView
self.request.user.log_action('pretix.user.oauth.authorized', user=self.request.user, data={
'application_id': application.pk,
'application_name': application.name,
'organizers': [o.pk for o in form.cleaned_data.get("organizers")] if form.cleaned_data.get("organizers") else []
})
return self.redirect(self.success_url, application)

View File

@@ -67,8 +67,8 @@ from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Checkin, Device, EventMetaValue,
Invoice, InvoiceAddress, ItemMetaValue, ItemVariation,
ItemVariationMetaValue, Order, OrderFee, OrderPayment, OrderPosition,
OrderRefund, Quota, ReusableMedium, SubEvent, SubEventMetaValue, TaxRule,
TeamAPIToken, generate_secret,
OrderRefund, Quota, SubEvent, SubEventMetaValue, TaxRule, TeamAPIToken,
generate_secret,
)
from pretix.base.models.orders import (
BlockedTicketSecret, QuestionAnswer, RevokedTicketSecret,
@@ -105,9 +105,9 @@ with scopes_disabled():
subevent_after = django_filters.IsoDateTimeFilter(method='subevent_after_qs')
subevent_before = django_filters.IsoDateTimeFilter(method='subevent_before_qs')
search = django_filters.CharFilter(method='search_qs')
item = django_filters.CharFilter(field_name='all_positions', lookup_expr='item_id', distinct=True)
variation = django_filters.CharFilter(field_name='all_positions', lookup_expr='variation_id', distinct=True)
subevent = django_filters.CharFilter(field_name='all_positions', lookup_expr='subevent_id', distinct=True)
item = django_filters.CharFilter(field_name='all_positions', lookup_expr='item_id')
variation = django_filters.CharFilter(field_name='all_positions', lookup_expr='variation_id')
subevent = django_filters.CharFilter(field_name='all_positions', lookup_expr='subevent_id')
class Meta:
model = Order
@@ -148,13 +148,9 @@ with scopes_disabled():
else:
code = Q(code__icontains=Order.normalize_code(u))
invoice_nos = {u, u.upper()}
if u.isdigit():
for i in range(2, 12):
invoice_nos.add(u.zfill(i))
matching_invoices = Invoice.objects.filter(
Q(invoice_no__in=invoice_nos)
Q(invoice_no__iexact=u)
| Q(invoice_no__iexact=u.zfill(5))
| Q(full_invoice_no__iexact=u)
).values_list('order_id', flat=True)
@@ -166,15 +162,12 @@ with scopes_disabled():
)
).values('id')
matching_media = ReusableMedium.objects.filter(identifier=u).values_list('linked_orderposition__order_id', flat=True)
mainq = (
code
| Q(email__icontains=u)
| Q(invoice_address__name_cached__icontains=u)
| Q(invoice_address__company__icontains=u)
| Q(pk__in=matching_invoices)
| Q(pk__in=matching_media)
| Q(comment__icontains=u)
| Q(has_pos=True)
)
@@ -251,8 +244,7 @@ class OrderViewSet(viewsets.ModelViewSet):
Prefetch('subevent', queryset=self.request.event.subevents.prefetch_related(
Prefetch('meta_values', to_attr='meta_values_cached', queryset=SubEventMetaValue.objects.select_related('property'))
)),
Prefetch('addons', opq.select_related('item', 'variation', 'seat')),
'linked_media',
Prefetch('addons', opq.select_related('item', 'variation', 'seat'))
).select_related('seat', 'addon_to', 'addon_to__seat')
)
else:
@@ -321,7 +313,7 @@ class OrderViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['POST'])
def mark_paid(self, request, **kwargs):
order = self.get_object()
send_mail = request.data.get('send_email', True) if request.data else True
send_mail = request.data.get('send_email', True)
if order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED):
@@ -380,7 +372,7 @@ class OrderViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['POST'])
def mark_canceled(self, request, **kwargs):
send_mail = request.data.get('send_email', True) if request.data else True
send_mail = request.data.get('send_email', True)
comment = request.data.get('comment', None)
cancellation_fee = request.data.get('cancellation_fee', None)
if cancellation_fee:
@@ -439,7 +431,7 @@ class OrderViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['POST'])
def approve(self, request, **kwargs):
send_mail = request.data.get('send_email', True) if request.data else True
send_mail = request.data.get('send_email', True)
order = self.get_object()
try:
@@ -457,7 +449,7 @@ class OrderViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['POST'])
def deny(self, request, **kwargs):
send_mail = request.data.get('send_email', True) if request.data else True
send_mail = request.data.get('send_email', True)
comment = request.data.get('comment', '')
order = self.get_object()
@@ -647,11 +639,13 @@ class OrderViewSet(viewsets.ModelViewSet):
raise ValidationError(_('One of the selected products is not available in the selected country.'))
send_mail = serializer._send_mail
order = serializer.instance
if not order.pk:
# Simulation -- exit here
# Simulation
serializer = SimulatedOrderSerializer(order, context=serializer.context)
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
prefetch_related_objects([order], self._positions_prefetch(request))
serializer = OrderSerializer(order, context=serializer.context)
order.log_action(
'pretix.event.order.placed',
@@ -685,10 +679,6 @@ class OrderViewSet(viewsets.ModelViewSet):
if gen_invoice:
invoice = generate_invoice(order, trigger_pdf=True)
# Refresh serializer only after running signals
prefetch_related_objects([order], self._positions_prefetch(request))
serializer = OrderSerializer(order, context=serializer.context)
if send_mail:
free_flow = (
payment and order.total == Decimal('0.00') and order.status == Order.STATUS_PAID and
@@ -927,7 +917,6 @@ with scopes_disabled():
search = django_filters.CharFilter(method='search_qs')
def search_qs(self, queryset, name, value):
matching_media = ReusableMedium.objects.filter(identifier=value).values_list('linked_orderposition', flat=True)
return queryset.filter(
Q(secret__istartswith=value)
| Q(attendee_name_cached__icontains=value)
@@ -937,7 +926,6 @@ with scopes_disabled():
| Q(order__code__istartswith=value)
| Q(order__invoice_address__name_cached__icontains=value)
| Q(order__email__icontains=value)
| Q(pk__in=matching_media)
)
def has_checkin_qs(self, queryset, name, value):
@@ -1017,7 +1005,6 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
Prefetch('meta_values', to_attr='meta_values_cached',
queryset=SubEventMetaValue.objects.select_related('property'))
)),
'linked_media',
Prefetch('order', self.request.event.orders.select_related('invoice_address').prefetch_related(
Prefetch(
'positions',
@@ -1453,7 +1440,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
return order.payments.all()
def create(self, request, *args, **kwargs):
send_mail = request.data.get('send_email', True) if request.data else True
send_mail = request.data.get('send_email', True)
serializer = OrderPaymentCreateSerializer(data=request.data, context=self.get_serializer_context())
serializer.is_valid(raise_exception=True)
with transaction.atomic():
@@ -1498,7 +1485,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
def confirm(self, request, **kwargs):
payment = self.get_object()
force = request.data.get('force', False)
send_mail = request.data.get('send_email', True) if request.data else True
send_mail = request.data.get('send_email', True)
if payment.state not in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED):
return Response({'detail': 'Invalid state of payment'}, status=status.HTTP_400_BAD_REQUEST)
@@ -1520,7 +1507,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
@action(detail=True, methods=['POST'])
def refund(self, request, **kwargs):
payment = self.get_object()
amount = serializers.DecimalField(max_digits=13, decimal_places=2).to_internal_value(
amount = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
request.data.get('amount', str(payment.amount))
)
if 'mark_refunded' in request.data:

View File

@@ -155,9 +155,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
qs = self.request.organizer.accepted_gift_cards
else:
qs = self.request.organizer.issued_gift_cards.all()
return qs.prefetch_related(
'issuer'
)
return qs
def get_serializer_context(self):
ctx = super().get_serializer_context()
@@ -168,7 +166,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer):
value = serializer.validated_data.pop('value')
inst = serializer.save(issuer=self.request.organizer)
inst.transactions.create(value=value, acceptor=self.request.organizer)
inst.transactions.create(value=value)
inst.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
@@ -181,59 +179,42 @@ class GiftCardViewSet(viewsets.ModelViewSet):
if 'include_accepted' in self.request.GET:
raise PermissionDenied("Accepted gift cards cannot be updated, use transact instead.")
GiftCard.objects.select_for_update(of=OF_SELF).get(pk=self.get_object().pk)
value = serializer.validated_data.pop('value', None)
if any(k != 'value' for k in self.request.data):
inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency,
testmode=serializer.instance.testmode)
inst.log_action(
'pretix.giftcards.modified',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
else:
inst = serializer.instance
if 'value' in self.request.data and value is not None:
old_value = serializer.instance.value
diff = value - old_value
inst.transactions.create(value=diff, acceptor=self.request.organizer)
inst.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data={'value': diff}
)
old_value = serializer.instance.value
value = serializer.validated_data.pop('value')
inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency,
testmode=serializer.instance.testmode)
diff = value - old_value
inst.transactions.create(value=diff)
inst.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data={'value': diff}
)
return inst
@action(detail=True, methods=["POST"])
@transaction.atomic()
def transact(self, request, **kwargs):
gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=self.get_object().pk)
value = serializers.DecimalField(max_digits=13, decimal_places=2).to_internal_value(
value = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
request.data.get('value')
)
text = serializers.CharField(allow_blank=True, allow_null=True).to_internal_value(
request.data.get('text', '')
)
info = serializers.JSONField(required=False, allow_null=True).to_internal_value(
request.data.get('info', {})
)
if gc.value + value < Decimal('0.00'):
return Response({
'value': ['The gift card does not have sufficient credit for this operation.']
}, status=status.HTTP_409_CONFLICT)
gc.transactions.create(value=value, text=text, info=info, acceptor=self.request.organizer)
gc.transactions.create(value=value, text=text)
gc.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data={'value': value, 'text': text}
)
return Response(GiftCardSerializer(gc, context=self.get_serializer_context()).data, status=status.HTTP_200_OK)
return Response(GiftCardSerializer(gc).data, status=status.HTTP_200_OK)
def perform_destroy(self, instance):
raise MethodNotAllowed("Gift cards cannot be deleted.")
@@ -254,7 +235,7 @@ class GiftCardTransactionViewSet(viewsets.ReadOnlyModelViewSet):
return get_object_or_404(qs, pk=self.kwargs.get('giftcard'))
def get_queryset(self):
return self.giftcard.transactions.select_related('order', 'order__event').prefetch_related('acceptor')
return self.giftcard.transactions.select_related('order', 'order__event')
class TeamViewSet(viewsets.ModelViewSet):
@@ -476,8 +457,7 @@ class OrganizerSettingsView(views.APIView):
fname: {
'value': s.data[fname],
'label': getattr(field, '_label', fname),
'help_text': getattr(field, '_help_text', None),
'readonly': fname in s.readonly_fields,
'help_text': getattr(field, '_help_text', None)
} for fname, field in s.fields.items()
})
return Response(s.data)

View File

@@ -256,10 +256,6 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.event.order.refund.failed',
_('Refund of payment failed'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.payment.confirmed',
_('Payment confirmed'),
),
ParametrizedOrderWebhookEvent(
'pretix.event.order.approved',
_('Order approved'),

View File

@@ -117,15 +117,13 @@ def oidc_validate_and_complete_config(config):
scopes=", ".join(provider_config.get("scopes_supported", []))
))
if "claims_supported" in provider_config:
claims_supported = provider_config.get("claims_supported", [])
for k, v in config.items():
if k.endswith('_field') and v:
if v not in claims_supported: # https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
raise ValidationError(_('You are requesting field "{field}" but provider only supports these: {fields}.').format(
field=v,
fields=", ".join(provider_config.get("claims_supported", []))
))
for k, v in config.items():
if k.endswith('_field') and v:
if v not in provider_config.get("claims_supported", []): # https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
raise ValidationError(_('You are requesting field "{field}" but provider only supports these: {fields}.').format(
field=v,
fields=", ".join(provider_config.get("claims_supported", []))
))
config['provider_config'] = provider_config
return config

View File

@@ -35,14 +35,16 @@ from django.dispatch import receiver
from django.template.loader import get_template
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import get_language, gettext_lazy as _
from django.utils.translation import (
get_language, gettext_lazy as _, pgettext_lazy,
)
from pretix.base.i18n import (
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
)
from pretix.base.models import Event
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import (
register_html_mail_renderers, register_mail_placeholders,
)
@@ -644,10 +646,6 @@ def base_placeholders(sender, **kwargs):
'attendee_name', ['position'], lambda position: position.attendee_name,
_('John Doe'),
),
SimpleFunctionalMailTextPlaceholder(
'positionid', ['position'], lambda position: str(position.positionid),
'1'
),
SimpleFunctionalMailTextPlaceholder(
'name', ['position_or_address'],
get_best_name,
@@ -661,11 +659,6 @@ def base_placeholders(sender, **kwargs):
else:
concatenation_for_salutation = name_scheme["concatenation"]
ph.append(SimpleFunctionalMailTextPlaceholder(
"name_for_salutation", ["waiting_list_entry"],
lambda waiting_list_entry: concatenation_for_salutation(waiting_list_entry.name_parts),
_("Mr Doe"),
))
ph.append(SimpleFunctionalMailTextPlaceholder(
"name_for_salutation", ["position_or_address"],
lambda position_or_address: concatenation_for_salutation(get_best_name(position_or_address, parts=True)),
@@ -675,10 +668,6 @@ def base_placeholders(sender, **kwargs):
for f, l, w in name_scheme['fields']:
if f == 'full_name':
continue
ph.append(SimpleFunctionalMailTextPlaceholder(
'name_%s' % f, ['waiting_list_entry'], lambda waiting_list_entry, f=f: get_name_parts_localized(waiting_list_entry.name_parts, f),
name_scheme['sample'][f]
))
ph.append(SimpleFunctionalMailTextPlaceholder(
'attendee_name_%s' % f, ['position'], lambda position, f=f: get_name_parts_localized(position.attendee_name_parts, f),
name_scheme['sample'][f]
@@ -700,3 +689,10 @@ def base_placeholders(sender, **kwargs):
))
return ph
def get_name_parts_localized(name_parts, key):
value = name_parts.get(key, "")
if key == "salutation":
return pgettext_lazy("person_name_salutation", value)
return value

View File

@@ -58,7 +58,6 @@ class EventDataExporter(ListExporter):
_("Short form"),
_("Shop is live"),
_("Event currency"),
_("Timezone"),
_("Event start time"),
_("Event end time"),
_("Admission time"),
@@ -76,18 +75,16 @@ class EventDataExporter(ListExporter):
for e in self.events.all():
m = e.meta_data
tz = e.timezone
yield [
str(e.name),
e.slug,
_('Yes') if e.live else _('No'),
e.currency,
str(e.timezone),
date_format(e.date_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
date_format(e.date_to.astimezone(tz), 'SHORT_DATETIME_FORMAT') if e.date_to else '',
date_format(e.date_admission.astimezone(tz), 'SHORT_DATETIME_FORMAT') if e.date_admission else '',
date_format(e.presale_start.astimezone(tz), 'SHORT_DATETIME_FORMAT') if e.presale_start else '',
date_format(e.presale_end.astimezone(tz), 'SHORT_DATETIME_FORMAT') if e.presale_end else '',
date_format(e.date_from, 'SHORT_DATETIME_FORMAT'),
date_format(e.date_to, 'SHORT_DATETIME_FORMAT') if e.date_to else '',
date_format(e.date_admission, 'SHORT_DATETIME_FORMAT') if e.date_admission else '',
date_format(e.presale_start, 'SHORT_DATETIME_FORMAT') if e.presale_start else '',
date_format(e.presale_end, 'SHORT_DATETIME_FORMAT') if e.presale_end else '',
str(e.location),
e.geo_lat or '',
e.geo_lon or '',
@@ -97,7 +94,7 @@ class EventDataExporter(ListExporter):
]
def get_filename(self):
return '{}_events'.format(self.organizer.slug)
return '{}_events'.format(self.events.first().organizer.slug)
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_eventdata")

View File

@@ -103,9 +103,7 @@ class InvoiceExporterMixin:
qs = qs.annotate(
has_payment_with_provider=Exists(
OrderPayment.objects.filter(
Q(order=OuterRef('order_id')) & Q(provider=form_data.get('payment_provider')),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED),
Q(order=OuterRef('order_id')) & Q(provider=form_data.get('payment_provider'))
)
)
)
@@ -157,7 +155,7 @@ class InvoiceExporter(InvoiceExporterMixin, BaseExporter):
self.progress_callback(counter / total * 100)
if self.is_multievent:
filename = '{}_invoices.zip'.format(self.organizer.slug)
filename = '{}_invoices.zip'.format(self.events.first().organizer.slug)
else:
filename = '{}_invoices.zip'.format(self.event.slug)
@@ -417,7 +415,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
def get_filename(self):
if self.is_multievent:
return '{}_invoices'.format(self.organizer.slug)
return '{}_invoices'.format(self.events.first().organizer.slug)
else:
return '{}_invoices'.format(self.event.slug)

View File

@@ -218,9 +218,7 @@ class ItemDataExporter(ListExporter):
yield row
def get_filename(self):
if self.is_multievent:
return '{}_products'.format(self.organizer.slug)
return '{}_products'.format(self.event.slug)
return '{}_products'.format(self.events.first().organizer.slug)
def prepare_xlsx_sheet(self, ws):
self.__ws = ws

View File

@@ -63,7 +63,7 @@ class MailExporter(BaseExporter):
| set(a['attendee_email'] for a in pos if a['attendee_email']))
if self.is_multievent:
return '{}_pretixemails.txt'.format(self.organizer.slug), 'text/plain', data.encode("utf-8")
return '{}_pretixemails.txt'.format(self.events.first().organizer.slug), 'text/plain', data.encode("utf-8")
else:
return '{}_pretixemails.txt'.format(self.event.slug), 'text/plain', data.encode("utf-8")

View File

@@ -49,24 +49,18 @@ from django.utils.timezone import get_current_timezone, now
from django.utils.translation import (
gettext as _, gettext_lazy, pgettext, pgettext_lazy,
)
from openpyxl.cell import WriteOnlyCell
from openpyxl.comments import Comment
from openpyxl.styles import Font, PatternFill
from pretix.base.models import (
GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
OrderPosition, Question,
)
from pretix.base.models.orders import (
OrderFee, OrderPayment, OrderRefund, Transaction,
)
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
from pretix.base.settings import PERSON_NAME_SCHEMES
from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat
from ...helpers.iter import chunked_iterable
from ...helpers.safe_openpyxl import remove_invalid_excel_chars
from ..exporter import (
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
)
@@ -346,7 +340,7 @@ class OrderListExporter(MultiSheetListExporter):
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
get_name_parts_localized(order.invoice_address.name_parts, k)
order.invoice_address.name_parts.get(k, '')
)
row += [
order.invoice_address.street,
@@ -483,7 +477,7 @@ class OrderListExporter(MultiSheetListExporter):
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
get_name_parts_localized(order.invoice_address.name_parts, k)
order.invoice_address.name_parts.get(k, '')
)
row += [
order.invoice_address.street,
@@ -666,7 +660,7 @@ class OrderListExporter(MultiSheetListExporter):
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
get_name_parts_localized(op.attendee_name_parts, k)
op.attendee_name_parts.get(k, '')
)
row += [
op.attendee_email,
@@ -694,8 +688,8 @@ class OrderListExporter(MultiSheetListExporter):
row += [
_('Yes') if op.blocked else '',
date_format(op.valid_from.astimezone(tz), 'SHORT_DATETIME_FORMAT') if op.valid_from else '',
date_format(op.valid_until.astimezone(tz), 'SHORT_DATETIME_FORMAT') if op.valid_until else '',
date_format(op.valid_from, 'SHORT_DATETIME_FORMAT') if op.valid_from else '',
date_format(op.valid_until, 'SHORT_DATETIME_FORMAT') if op.valid_until else '',
]
row.append(order.comment)
row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "")
@@ -727,7 +721,7 @@ class OrderListExporter(MultiSheetListExporter):
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
get_name_parts_localized(order.invoice_address.name_parts, k)
order.invoice_address.name_parts.get(k, '')
)
row += [
order.invoice_address.street,
@@ -760,186 +754,11 @@ class OrderListExporter(MultiSheetListExporter):
def get_filename(self):
if self.is_multievent:
return '{}_orders'.format(self.organizer.slug)
return '{}_orders'.format(self.events.first().organizer.slug)
else:
return '{}_orders'.format(self.event.slug)
class TransactionListExporter(ListExporter):
identifier = 'transactions'
verbose_name = gettext_lazy('Order transaction data')
category = pgettext_lazy('export_category', 'Order data')
description = gettext_lazy('Download a spreadsheet of all substantial changes to orders, i.e. all changes to '
'products, prices or tax rates. The information is only accurate for changes made with '
'pretix versions released after October 2021.')
@cached_property
def providers(self):
return dict(get_all_payment_providers())
@property
def additional_form_fields(self):
d = [
('date_range',
DateFrameField(
label=_('Date range'),
include_future_frames=False,
required=False,
help_text=_('Only include transactions created within this date range.')
)),
]
d = OrderedDict(d)
return d
@cached_property
def event_object_cache(self):
return {e.pk: e for e in self.events}
def get_filename(self):
if self.is_multievent:
return '{}_transactions'.format(self.organizer.slug)
else:
return '{}_transactions'.format(self.event.slug)
def iterate_list(self, form_data):
qs = Transaction.objects.filter(
order__event__in=self.events,
)
if form_data.get('date_range'):
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['date_range'], self.timezone)
if dt_start:
qs = qs.filter(datetime__gte=dt_start)
if dt_end:
qs = qs.filter(datetime__lt=dt_end)
qs = qs.select_related(
'order', 'order__event', 'item', 'variation', 'subevent',
).order_by(
'datetime', 'id',
)
headers = [
_('Event'),
_('Event slug'),
_('Currency'),
_('Order code'),
_('Order date'),
_('Order time'),
_('Transaction date'),
_('Transaction time'),
_('Old data'),
_('Position ID'),
_('Quantity'),
_('Product ID'),
_('Product'),
_('Variation ID'),
_('Variation'),
_('Fee type'),
_('Internal fee type'),
pgettext('subevent', 'Date ID'),
pgettext('subevent', 'Date'),
_('Price'),
_('Tax rate'),
_('Tax rule ID'),
_('Tax rule'),
_('Tax value'),
]
if form_data.get('_format') == 'xlsx':
for i in range(len(headers)):
headers[i] = WriteOnlyCell(self.__ws, value=headers[i])
if i in (0, 12, 14, 18, 22):
headers[i].fill = PatternFill(start_color="FFB419", end_color="FFB419", fill_type="solid")
headers[i].comment = Comment(
text=_(
"This value is supplied for informational purposes, it is not part of the original transaction "
"data and might have changed since the transaction."
),
author='system'
)
headers[i].font = Font(bold=True)
yield headers
yield self.ProgressSetTotal(total=qs.count())
for t in qs.iterator():
row = [
str(t.order.event.name),
t.order.event.slug,
t.order.event.currency,
t.order.code,
t.order.datetime.astimezone(self.timezone).strftime('%Y-%m-%d'),
t.order.datetime.astimezone(self.timezone).strftime('%H:%M:%S'),
t.datetime.astimezone(self.timezone).strftime('%Y-%m-%d'),
t.datetime.astimezone(self.timezone).strftime('%H:%M:%S'),
_('Converted from legacy version') if t.migrated else '',
t.positionid,
t.count,
t.item_id,
str(t.item),
t.variation_id or '',
str(t.variation) if t.variation_id else '',
t.fee_type,
t.internal_type,
t.subevent_id or '',
str(t.subevent) if t.subevent else '',
t.price,
t.tax_rate,
t.tax_rule_id or '',
str(t.tax_rule.internal_name or t.tax_rule.name) if t.tax_rule_id else '',
t.tax_value,
]
if form_data.get('_format') == 'xlsx':
for i in range(len(row)):
if t.order.testmode:
row[i] = WriteOnlyCell(self.__ws, value=remove_invalid_excel_chars(row[i]))
row[i].fill = PatternFill(start_color="FFB419", end_color="FFB419", fill_type="solid")
yield row
def prepare_xlsx_sheet(self, ws):
self.__ws = ws
ws.freeze_panes = 'A2'
ws.column_dimensions['A'].width = 25
ws.column_dimensions['B'].width = 10
ws.column_dimensions['C'].width = 10
ws.column_dimensions['D'].width = 10
ws.column_dimensions['E'].width = 15
ws.column_dimensions['F'].width = 15
ws.column_dimensions['G'].width = 15
ws.column_dimensions['H'].width = 15
ws.column_dimensions['I'].width = 15
ws.column_dimensions['J'].width = 10
ws.column_dimensions['K'].width = 10
ws.column_dimensions['L'].width = 10
ws.column_dimensions['M'].width = 25
ws.column_dimensions['N'].width = 10
ws.column_dimensions['O'].width = 25
ws.column_dimensions['P'].width = 20
ws.column_dimensions['Q'].width = 20
ws.column_dimensions['R'].width = 10
ws.column_dimensions['S'].width = 25
ws.column_dimensions['T'].width = 15
ws.column_dimensions['U'].width = 10
ws.column_dimensions['V'].width = 10
ws.column_dimensions['W'].width = 20
ws.column_dimensions['X'].width = 15
class PaymentListExporter(ListExporter):
identifier = 'paymentlist'
verbose_name = gettext_lazy('Payments and refunds')
@@ -1061,7 +880,7 @@ class PaymentListExporter(ListExporter):
def get_filename(self):
if self.is_multievent:
return '{}_payments'.format(self.organizer.slug)
return '{}_payments'.format(self.events.first().organizer.slug)
else:
return '{}_payments'.format(self.event.slug)
@@ -1218,7 +1037,7 @@ class GiftcardRedemptionListExporter(ListExporter):
def get_filename(self):
if self.is_multievent:
return '{}_giftcardredemptions'.format(self.organizer.slug)
return '{}_giftcardredemptions'.format(self.events.first().organizer.slug)
else:
return '{}_giftcardredemptions'.format(self.event.slug)
@@ -1346,16 +1165,6 @@ def register_multievent_orderlist_exporter(sender, **kwargs):
return OrderListExporter
@receiver(register_data_exporters, dispatch_uid="exporter_ordertransactionlist")
def register_ordertransactionlist_exporter(sender, **kwargs):
return TransactionListExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_ordertransactionlist")
def register_multievent_ordertransactionlist_exporter(sender, **kwargs):
return TransactionListExporter
@receiver(register_data_exporters, dispatch_uid="exporter_paymentlist")
def register_paymentlist_exporter(sender, **kwargs):
return PaymentListExporter

View File

@@ -36,13 +36,11 @@ import logging
import i18nfield.forms
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.translation import gettext_lazy as _
from formtools.wizard.views import SessionWizardView
from hierarkey.forms import HierarkeyForm
from i18nfield.strings import LazyI18nString
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
@@ -224,17 +222,3 @@ class SecretKeySettingsField(forms.CharField):
if value == SECRET_REDACTED:
return
return super().run_validators(value)
class I18nURLFormField(i18nfield.forms.I18nFormField):
def clean(self, value) -> LazyI18nString:
value = super().clean(value)
if not value:
return value
if isinstance(value.data, dict):
for v in value.data.values():
if v:
URLValidator()(v)
else:
URLValidator()(value.data)
return value

View File

@@ -45,7 +45,6 @@ import pytz
from django import forms
from django.conf import settings
from django.contrib import messages
from django.contrib.gis.geoip2 import GeoIP2
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.validators import (
@@ -92,7 +91,6 @@ from pretix.helpers.countries import (
CachedCountries, get_phone_prefixes_sorted_and_localized,
)
from pretix.helpers.escapejson import escapejson_attr
from pretix.helpers.http import get_client_ip
from pretix.helpers.i18n import get_format_without_seconds
from pretix.presale.signals import question_form_fields
@@ -353,15 +351,6 @@ class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
return ""
def guess_country_from_request(request, event):
if settings.HAS_GEOIP:
g = GeoIP2()
res = g.country(get_client_ip(request))
if res['country_code'] and len(res['country_code']) == 2:
return Country(res['country_code'])
return guess_country(event)
def guess_country(event):
# Try to guess the initial country from either the country of the merchant
# or the locale. This will hopefully save at least some users some scrolling :)
@@ -393,12 +382,6 @@ def guess_phone_prefix(event):
return get_phone_prefix(country)
def guess_phone_prefix_from_request(request, event):
with language(get_babel_locale()):
country = str(guess_country_from_request(request, event))
return get_phone_prefix(country)
def get_phone_prefix(country):
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
if country in values:
@@ -573,7 +556,6 @@ class BaseQuestionsForm(forms.Form):
the attendee name for admission tickets, if the corresponding setting is enabled,
as well as additional questions defined by the organizer.
"""
address_validation = False
def __init__(self, *args, **kwargs):
"""
@@ -582,7 +564,6 @@ class BaseQuestionsForm(forms.Form):
:param cartpos: The cart position the form should be for
:param event: The event this belongs to
"""
request = kwargs.pop('request', None)
cartpos = self.cartpos = kwargs.pop('cartpos', None)
orderpos = self.orderpos = kwargs.pop('orderpos', None)
pos = cartpos or orderpos
@@ -680,7 +661,7 @@ class BaseQuestionsForm(forms.Form):
'autocomplete': 'address-level2',
}),
)
country = (cartpos.country if cartpos else orderpos.country) or guess_country_from_request(request, event)
country = (cartpos.country if cartpos else orderpos.country) or guess_country(event)
add_fields['country'] = CountryField(
countries=CachedCountries
).formfield(
@@ -738,7 +719,7 @@ class BaseQuestionsForm(forms.Form):
label = escape(q.question) # django-bootstrap3 calls mark_safe
required = q.required and not self.all_optional
if q.type == Question.TYPE_BOOLEAN:
if required:
if q.required:
# For some reason, django-bootstrap3 does not set the required attribute
# itself.
widget = forms.CheckboxInput(attrs={'required': 'required'})
@@ -766,14 +747,12 @@ class BaseQuestionsForm(forms.Form):
elif q.type == Question.TYPE_STRING:
field = forms.CharField(
label=label, required=required,
max_length=q.valid_string_length_max,
help_text=help_text,
initial=initial.answer if initial else None,
)
elif q.type == Question.TYPE_TEXT:
field = forms.CharField(
label=label, required=required,
max_length=q.valid_string_length_max,
help_text=help_text,
widget=forms.Textarea,
initial=initial.answer if initial else None,
@@ -787,7 +766,7 @@ class BaseQuestionsForm(forms.Form):
help_text=help_text,
widget=forms.Select,
empty_label=' ',
initial=initial.answer if initial else (guess_country_from_request(request, event) if required else None),
initial=initial.answer if initial else (guess_country(event) if required else None),
)
elif q.type == Question.TYPE_CHOICE:
field = forms.ModelChoiceField(
@@ -875,7 +854,7 @@ class BaseQuestionsForm(forms.Form):
initial = None
if not initial:
phone_prefix = guess_phone_prefix_from_request(request, event)
phone_prefix = guess_phone_prefix(event)
if phone_prefix:
initial = "+{}.".format(phone_prefix)
@@ -921,14 +900,8 @@ class BaseQuestionsForm(forms.Form):
v.widget.attrs['autocomplete'] = 'section-{} '.format(self.prefix) + v.widget.attrs.get('autocomplete', '')
def clean(self):
from pretix.base.addressvalidation import \
validate_address # local import to prevent impact on startup time
d = super().clean()
if self.address_validation:
self.cleaned_data = d = validate_address(d, True)
if d.get('city') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if not d.get('state'):
self.add_error('state', _('This field is required.'))
@@ -1017,7 +990,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
kwargs.setdefault('initial', {})
if not kwargs.get('instance') or not kwargs['instance'].country:
kwargs['initial']['country'] = guess_country_from_request(self.request, self.event)
kwargs['initial']['country'] = guess_country(self.event)
super().__init__(*args, **kwargs)
if not event.settings.invoice_address_vatid:

View File

@@ -20,8 +20,6 @@
# <https://www.gnu.org/licenses/>.
#
import logging
import re
import unicodedata
from collections import defaultdict
from decimal import Decimal
from io import BytesIO
@@ -30,7 +28,6 @@ from typing import Tuple
import bleach
import vat_moss.exchange_rates
from bidi.algorithm import get_display
from django.contrib.staticfiles import finders
from django.db.models import Sum
from django.dispatch import receiver
@@ -56,8 +53,7 @@ from pretix.base.models import Event, Invoice, Order, OrderPayment
from pretix.base.services.currencies import SOURCE_NAMES
from pretix.base.signals import register_invoice_renderers
from pretix.base.templatetags.money import money_filter
from pretix.helpers.reportlab import ThumbnailingImageReader, reshaper
from pretix.presale.style import get_fonts
from pretix.helpers.reportlab import ThumbnailingImageReader
logger = logging.getLogger(__name__)
@@ -83,12 +79,7 @@ class NumberedCanvas(Canvas):
def draw_page_number(self, page_count):
self.saveState()
self.setFont(self.font_regular, 8)
text = pgettext("invoice", "Page %d of %d") % (self._pageNumber, page_count,)
try:
text = get_display(reshaper.reshape(text))
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
self.drawRightString(self._pagesize[0] - 20 * mm, 10 * mm, text)
self.drawRightString(self._pagesize[0] - 20 * mm, 10 * mm, pgettext("invoice", "Page %d of %d") % (self._pageNumber, page_count,))
self.restoreState()
@@ -148,8 +139,8 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
"""
Initialize the renderer. By default, this registers fonts and sets ``self.stylesheet``.
"""
self._register_fonts()
self.stylesheet = self._get_stylesheet()
self._register_fonts()
def _get_stylesheet(self):
"""
@@ -157,18 +148,12 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
"""
stylesheet = StyleSheet1()
stylesheet.add(ParagraphStyle(name='Normal', fontName=self.font_regular, fontSize=10, leading=12))
stylesheet.add(ParagraphStyle(name='Bold', fontName=self.font_bold, fontSize=10, leading=12))
stylesheet.add(ParagraphStyle(name='BoldRight', fontName=self.font_bold, fontSize=10, leading=12, alignment=TA_RIGHT))
stylesheet.add(ParagraphStyle(name='BoldRightNoSplit', fontName=self.font_bold, fontSize=10, leading=12, alignment=TA_RIGHT,
splitLongWords=False))
stylesheet.add(ParagraphStyle(name='NormalRight', fontName=self.font_regular, fontSize=10, leading=12, alignment=TA_RIGHT))
stylesheet.add(ParagraphStyle(name='BoldInverseCenter', fontName=self.font_bold, fontSize=10, leading=12,
textColor=colors.white, alignment=TA_CENTER))
stylesheet.add(ParagraphStyle(name='InvoiceFrom', parent=stylesheet['Normal']))
stylesheet.add(ParagraphStyle(name='Heading1', fontName=self.font_bold, fontSize=15, leading=15 * 1.2))
stylesheet.add(ParagraphStyle(name='FineprintHeading', fontName=self.font_bold, fontSize=8, leading=12))
stylesheet.add(ParagraphStyle(name='Fineprint', fontName=self.font_regular, fontSize=8, leading=10))
stylesheet.add(ParagraphStyle(name='FineprintRight', fontName=self.font_regular, fontSize=8, leading=10, alignment=TA_RIGHT))
return stylesheet
def _register_fonts(self):
@@ -182,32 +167,6 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
italic='OpenSansIt', boldItalic='OpenSansBI')
for family, styles in get_fonts().items():
if family == self.event.settings.invoice_renderer_font:
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
self.font_regular = family
if 'italic' in styles:
pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype'])))
if 'bold' in styles:
pdfmetrics.registerFont(TTFont(family + ' B', finders.find(styles['bold']['truetype'])))
self.font_bold = family + ' B'
if 'bolditalic' in styles:
pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype'])))
def _normalize(self, text):
# reportlab does not support unicode combination characters
# It's important we do this before we use ArabicReshaper
text = unicodedata.normalize("NFKC", text)
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
try:
text = "<br />".join(get_display(reshaper.reshape(l)) for l in re.split("<br ?/>", text))
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
return text
def _upper(self, val):
# We uppercase labels, but not in every language
if get_language().startswith('el'):
@@ -287,10 +246,10 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
return 'invoice.pdf', 'application/pdf', buffer.read()
def _clean_text(self, text, tags=None):
return self._normalize(bleach.clean(
return bleach.clean(
text,
tags=tags or []
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')
class PaidMarker(Flowable):
@@ -331,7 +290,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
canvas.setFont(self.font_regular, 8)
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, self._normalize(line.strip()))
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
canvas.restoreState()
@@ -364,13 +323,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_invoice_from_label(self, canvas):
textobject = canvas.beginText(25 * mm, (297 - 15) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice from'))))
textobject.textLine(self._upper(pgettext('invoice', 'Invoice from')))
canvas.drawText(textobject)
def _draw_invoice_to_label(self, canvas):
textobject = canvas.beginText(25 * mm, (297 - 50) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice to'))))
textobject.textLine(self._upper(pgettext('invoice', 'Invoice to')))
canvas.drawText(textobject)
logo_width = 25 * mm
@@ -398,51 +357,51 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_metadata(self, canvas):
textobject = canvas.beginText(125 * mm, (297 - 38) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Order code'))))
textobject.textLine(self._upper(pgettext('invoice', 'Order code')))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self._normalize(self.invoice.order.full_code))
textobject.textLine(self.invoice.order.full_code)
canvas.drawText(textobject)
textobject = canvas.beginText(125 * mm, (297 - 50) * mm)
textobject.setFont(self.font_bold, 8)
if self.invoice.is_cancellation:
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Cancellation number'))))
textobject.textLine(self._upper(pgettext('invoice', 'Cancellation number')))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self._normalize(self.invoice.number))
textobject.textLine(self.invoice.number)
textobject.moveCursor(0, 5)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Original invoice'))))
textobject.textLine(self._upper(pgettext('invoice', 'Original invoice')))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self._normalize(self.invoice.refers.number))
textobject.textLine(self.invoice.refers.number)
else:
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice number'))))
textobject.textLine(self._upper(pgettext('invoice', 'Invoice number')))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self._normalize(self.invoice.number))
textobject.textLine(self.invoice.number)
textobject.moveCursor(0, 5)
if self.invoice.is_cancellation:
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Cancellation date'))))
textobject.textLine(self._upper(pgettext('invoice', 'Cancellation date')))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self._normalize(date_format(self.invoice.date, "DATE_FORMAT")))
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Original invoice date'))))
textobject.textLine(self._upper(pgettext('invoice', 'Original invoice date')))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self._normalize(date_format(self.invoice.refers.date, "DATE_FORMAT")))
textobject.textLine(date_format(self.invoice.refers.date, "DATE_FORMAT"))
textobject.moveCursor(0, 5)
else:
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice date'))))
textobject.textLine(self._upper(pgettext('invoice', 'Invoice date')))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self._normalize(date_format(self.invoice.date, "DATE_FORMAT")))
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
textobject.moveCursor(0, 5)
canvas.drawText(textobject)
@@ -455,19 +414,19 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_event_label(self, canvas):
textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Event'))))
textobject.textLine(self._upper(pgettext('invoice', 'Event')))
canvas.drawText(textobject)
def _draw_event(self, canvas):
def shorten(txt):
txt = str(txt)
txt = bleach.clean(txt, tags=[]).strip()
p = Paragraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height)
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
txt = ' '.join(txt.replace('', '').split()[:-1]) + ''
p = Paragraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height)
return txt
@@ -493,7 +452,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
else:
p_str = shorten(self.invoice.event.name)
p = Paragraph(self._normalize(p_str.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p = Paragraph(p_str.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p.wrapOn(canvas, self.event_width, self.event_height)
p_size = p.wrap(self.event_width, self.event_height)
p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1])
@@ -502,14 +461,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_footer(self, canvas):
canvas.setFont(self.font_regular, 8)
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, self._normalize(line.strip()))
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
def _draw_testmode(self, canvas):
if self.invoice.order.testmode:
canvas.saveState()
canvas.setFont(self.font_bold, 30)
canvas.setFont('OpenSansBd', 30)
canvas.setFillColorRGB(32, 0, 0)
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, self._normalize(gettext('TEST MODE')))
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, gettext('TEST MODE'))
canvas.restoreState()
def _on_first_page(self, canvas: Canvas, doc):
@@ -557,22 +516,22 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.internal_reference:
story.append(Paragraph(
self._normalize(pgettext('invoice', 'Customer reference: {reference}').format(
pgettext('invoice', 'Customer reference: {reference}').format(
reference=self._clean_text(self.invoice.internal_reference),
)),
),
self.stylesheet['Normal']
))
if self.invoice.invoice_to_vat_id:
story.append(Paragraph(
self._normalize(pgettext('invoice', 'Customer VAT ID')) + ': ' +
pgettext('invoice', 'Customer VAT ID') + ': ' +
self._clean_text(self.invoice.invoice_to_vat_id),
self.stylesheet['Normal']
))
if self.invoice.invoice_to_beneficiary:
story.append(Paragraph(
self._normalize(pgettext('invoice', 'Beneficiary')) + ':<br />' +
pgettext('invoice', 'Beneficiary') + ':<br />' +
self._clean_text(self.invoice.invoice_to_beneficiary),
self.stylesheet['Normal']
))
@@ -592,10 +551,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story = [
NextPageTemplate('FirstPage'),
Paragraph(
self._normalize(
(
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
else pgettext('invoice', 'Invoice')
) if not self.invoice.is_cancellation else self._normalize(pgettext('invoice', 'Cancellation')),
) if not self.invoice.is_cancellation else pgettext('invoice', 'Cancellation'),
self.stylesheet['Heading1']
),
Spacer(1, 5 * mm),
@@ -617,17 +576,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
]
if has_taxes:
tdata = [(
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
Paragraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
Paragraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']),
Paragraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']),
Paragraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']),
pgettext('invoice', 'Description'),
pgettext('invoice', 'Qty'),
pgettext('invoice', 'Tax rate'),
pgettext('invoice', 'Net'),
pgettext('invoice', 'Gross'),
)]
else:
tdata = [(
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['BoldRight']),
Paragraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
Paragraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']),
pgettext('invoice', 'Description'),
pgettext('invoice', 'Qty'),
pgettext('invoice', 'Amount'),
)]
def _group_key(line):
@@ -651,8 +610,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
),
str(len(lines)),
localize(tax_rate) + " %",
Paragraph(money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), self.stylesheet['NormalRight']),
Paragraph(money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), self.stylesheet['NormalRight']),
money_filter(net_value * len(lines), self.invoice.event.currency),
money_filter(gross_value * len(lines), self.invoice.event.currency),
))
else:
if len(lines) > 1:
@@ -666,7 +625,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
self.stylesheet['Normal']
),
str(len(lines)),
Paragraph(money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), self.stylesheet['NormalRight']),
money_filter(gross_value * len(lines), self.invoice.event.currency),
))
taxvalue_map[tax_rate, tax_name] += (gross_value - net_value) * len(lines)
grossvalue_map[tax_rate, tax_name] += gross_value * len(lines)
@@ -674,14 +633,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if has_taxes:
tdata.append([
Paragraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '',
money_filter(total, self.invoice.event.currency)
pgettext('invoice', 'Invoice total'), '', '', '', money_filter(total, self.invoice.event.currency)
])
colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)]
else:
tdata.append([
Paragraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '',
money_filter(total, self.invoice.event.currency)
pgettext('invoice', 'Invoice total'), '', money_filter(total, self.invoice.event.currency)
])
colwidths = [a * doc.width for a in (.65, .20, .15)]
@@ -689,16 +646,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.event.settings.invoice_show_payments and self.invoice.order.status == Order.STATUS_PENDING:
pending_sum = self.invoice.order.pending_sum
if pending_sum != total:
tdata.append(
[Paragraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum - total, self.invoice.event.currency)]
)
tdata.append(
[Paragraph(self._normalize(pgettext('invoice', 'Outstanding payments')), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum, self.invoice.event.currency)]
)
tdata.append([pgettext('invoice', 'Received payments')] + (['', '', ''] if has_taxes else ['']) + [
money_filter(pending_sum - total, self.invoice.event.currency)
])
tdata.append([pgettext('invoice', 'Outstanding payments')] + (['', '', ''] if has_taxes else ['']) + [
money_filter(pending_sum, self.invoice.event.currency)
])
tstyledata += [
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
]
@@ -711,24 +664,19 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
).aggregate(
s=Sum('amount')
)['s'] or Decimal('0.00')
tdata.append(
[Paragraph(self._normalize(pgettext('invoice', 'Paid by gift card')), self.stylesheet['Normal'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(giftcard_sum, self.invoice.event.currency)]
)
tdata.append(
[Paragraph(self._normalize(pgettext('invoice', 'Remaining amount')), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(total - giftcard_sum, self.invoice.event.currency)]
)
tdata.append([pgettext('invoice', 'Paid by gift card')] + (['', '', ''] if has_taxes else ['']) + [
money_filter(giftcard_sum, self.invoice.event.currency)
])
tdata.append([pgettext('invoice', 'Remaining amount')] + (['', '', ''] if has_taxes else ['']) + [
money_filter(total - giftcard_sum, self.invoice.event.currency)
])
tstyledata += [
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
]
elif self.invoice.payment_provider_stamp:
pm = PaidMarker(
text=self._normalize(self.invoice.payment_provider_stamp),
text=self.invoice.payment_provider_stamp,
color=colors.HexColor(self.event.settings.theme_color_success),
font=self.font_bold,
size=16
)
tdata[-1][-2] = pm
@@ -741,7 +689,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.payment_provider_text:
story.append(Paragraph(
self._normalize(self.invoice.payment_provider_text),
self.invoice.payment_provider_text,
self.stylesheet['Normal']
))
@@ -765,10 +713,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
]
thead = [
Paragraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']),
Paragraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']),
Paragraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']),
Paragraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']),
pgettext('invoice', 'Tax rate'),
pgettext('invoice', 'Net value'),
pgettext('invoice', 'Gross value'),
pgettext('invoice', 'Tax'),
''
]
tdata = [thead]
@@ -779,7 +727,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
continue
tax = taxvalue_map[idx]
tdata.append([
Paragraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
localize(rate) + " % " + name,
money_filter(gross - tax, self.invoice.event.currency),
money_filter(gross, self.invoice.event.currency),
money_filter(tax, self.invoice.event.currency),
@@ -798,7 +746,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
table.setStyle(TableStyle(tstyledata))
story.append(Spacer(5 * mm, 5 * mm))
story.append(KeepTogether([
Paragraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']),
Paragraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
table
]))
@@ -815,7 +763,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net = gross - tax
tdata.append([
Paragraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
localize(rate) + " % " + name,
fmt(net), fmt(gross), fmt(tax), ''
])
@@ -825,12 +773,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(KeepTogether([
Spacer(1, height=2 * mm),
Paragraph(
self._normalize(pgettext(
pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, this corresponds to:'
).format(rate=localize(self.invoice.foreign_currency_rate),
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"))),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT")),
self.stylesheet['Fineprint']
),
Spacer(1, height=3 * mm),
@@ -839,14 +787,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
story.append(Spacer(1, 5 * mm))
story.append(Paragraph(self._normalize(
story.append(Paragraph(
pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, the invoice total corresponds to {total}.'
).format(rate=localize(self.invoice.foreign_currency_rate),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
total=fmt(foreign_total))),
total=fmt(foreign_total)),
self.stylesheet['Fineprint']
))
@@ -892,7 +840,7 @@ class Modern1Renderer(ClassicInvoiceRenderer):
self._clean_text(l)
for l in self.invoice.address_invoice_from.strip().split('\n')
]
p = Paragraph(self._normalize(' · '.join(c)), style=self.stylesheet['Sender'])
p = Paragraph(' · '.join(c), style=self.stylesheet['Sender'])
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
super()._draw_invoice_from(canvas)
@@ -908,12 +856,8 @@ class Modern1Renderer(ClassicInvoiceRenderer):
def _get_first_page_frames(self, doc):
footer_length = 3.5 * len(self.invoice.footer_text.split('\n')) * mm
if self.event.settings.invoice_renderer_highlight_order_code:
margin_top = 100 * mm
else:
margin_top = 95 * mm
return [
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height - margin_top,
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height - 95 * mm,
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=footer_length,
id='normal')
]
@@ -924,35 +868,25 @@ class Modern1Renderer(ClassicInvoiceRenderer):
# the font size until it fits.
begin_top = 100 * mm
def _draw(label, value, value_size, x, width, bold=False, sublabel=None):
def _draw(label, value, value_size, x, width):
if canvas.stringWidth(value, self.font_regular, value_size) > width and value_size > 6:
return False
textobject = canvas.beginText(x, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8)
textobject.textLine(self._normalize(label))
textobject.textLine(label)
textobject.moveCursor(0, 5)
textobject.setFont(self.font_bold if bold else self.font_regular, value_size)
textobject.textLine(self._normalize(value))
if sublabel:
textobject.moveCursor(0, 1)
textobject.setFont(self.font_regular, 8)
textobject.textLine(self._normalize(sublabel))
textobject.setFont(self.font_regular, value_size)
textobject.textLine(value)
return textobject
value_size = 10
while value_size >= 5:
if self.event.settings.invoice_renderer_highlight_order_code:
kwargs = dict(bold=True, sublabel=pgettext('invoice', '(Please quote at all times.)'))
else:
kwargs = {}
objects = [
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs)
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm)
]
p = Paragraph(
self._normalize(date_format(self.invoice.date, "DATE_FORMAT")),
date_format(self.invoice.date, "DATE_FORMAT"),
style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2)
)
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
@@ -983,9 +917,9 @@ class Modern1Renderer(ClassicInvoiceRenderer):
textobject = canvas.beginText(date_x, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8)
if self.invoice.is_cancellation:
textobject.textLine(self._normalize(pgettext('invoice', 'Cancellation date')))
textobject.textLine(pgettext('invoice', 'Cancellation date'))
else:
textobject.textLine(self._normalize(pgettext('invoice', 'Invoice date')))
textobject.textLine(pgettext('invoice', 'Invoice date'))
canvas.drawText(textobject)

View File

@@ -44,8 +44,8 @@ class Command(BaseCommand):
OrderPosition.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(p=Sum('price')).values('p'),
output_field=models.DecimalField(decimal_places=2, max_digits=13)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=13)
output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
position_cnt=Case(
When(Q(status__in=('e', 'c')) | Q(require_approval=True), then=Value(0)),
@@ -64,16 +64,16 @@ class Command(BaseCommand):
OrderFee.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(p=Sum('value')).values('p'),
output_field=models.DecimalField(decimal_places=2, max_digits=13)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=13)
output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
tx_total=Coalesce(
Subquery(
Transaction.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(p=Sum(F('price') * F('count'))).values('p'),
output_field=models.DecimalField(decimal_places=2, max_digits=13)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=13)
output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
tx_cnt=Coalesce(
Subquery(
@@ -81,15 +81,15 @@ class Command(BaseCommand):
order=OuterRef('pk'),
item__isnull=False,
).order_by().values('order').annotate(p=Sum(F('count'))).values('p'),
output_field=models.DecimalField(decimal_places=2, max_digits=13)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=13)
output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
).annotate(
correct_total=Case(
When(Q(status=Order.STATUS_CANCELED) | Q(status=Order.STATUS_EXPIRED) | Q(require_approval=True),
then=Value(0)),
default=F('position_total') + F('fee_total'),
output_field=models.DecimalField(decimal_places=2, max_digits=13)
output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
).exclude(
total=F('position_total') + F('fee_total'),

View File

@@ -49,9 +49,8 @@ class Command(BaseCommand):
except ImportError:
cmd = 'shell'
del options['skip_checks']
del options['print_sql']
if options.get('print_sql'):
if options['print_sql']:
connection.force_debug_cursor = True
logger = logging.getLogger("django.db.backends")
logger.setLevel(logging.DEBUG)

View File

@@ -1,116 +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/>.
#
from django.db import transaction
from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _
class BaseMediaType:
medium_created_by_server = False
supports_orderposition = False
supports_giftcard = False
@property
def identifier(self):
raise NotImplementedError()
@property
def verbose_name(self):
raise NotImplementedError()
def generate_identifier(self, organizer):
if self.medium_created_by_server:
raise NotImplementedError()
else:
raise ValueError("Media type does not allow to generate identifier")
def is_active(self, organizer):
return organizer.settings.get(f'reusable_media_type_{self.identifier}', as_type=bool, default=False)
def handle_unknown(self, organizer, identifier, user, auth):
pass
def __str__(self):
return str(self.verbose_name)
class BarcodePlainMediaType(BaseMediaType):
identifier = 'barcode'
verbose_name = _('Barcode / QR-Code')
medium_created_by_server = True
supports_giftcard = False
supports_orderposition = True
def generate_identifier(self, organizer):
return get_random_string(
length=organizer.settings.reusable_media_type_barcode_identifier_length,
# Exclude o,0,1,i to avoid confusion with bad fonts/printers
# We use upper case to make collisions with ticket secrets less likely
allowed_chars='ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
)
class NfcUidMediaType(BaseMediaType):
identifier = 'nfc_uid'
verbose_name = _('NFC UID-based')
medium_created_by_server = False
supports_giftcard = True
supports_orderposition = False
def handle_unknown(self, organizer, identifier, user, auth):
from pretix.base.models import GiftCard, ReusableMedium
if organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard', as_type=bool):
if identifier.startswith("08"):
# Don't create gift cards for NFC UIDs that start with 08, which represents NFC cards that issue random
# UIDs on every read, so they won't be useful.
return
with transaction.atomic():
gc = GiftCard.objects.create(
issuer=organizer,
expires=organizer.default_gift_card_expiry,
currency=organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard_currency'),
)
m = ReusableMedium.objects.create(
type=self.identifier,
identifier=identifier,
organizer=organizer,
active=True,
linked_giftcard=gc
)
m.log_action(
'pretix.reusable_medium.created.auto',
user=user, auth=auth,
)
gc.log_action(
'pretix.giftcards.created',
user=user, auth=auth,
)
return m
MEDIA_TYPES = {
m.identifier: m for m in [
BarcodePlainMediaType(),
NfcUidMediaType(),
]
}

View File

@@ -1,165 +0,0 @@
# Generated by Django 3.2.18 on 2023-03-16 20:23
from decimal import Decimal
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0234_total_ordering'),
]
operations = [
migrations.AlterField(
model_name='cancellationrequest',
name='cancellation_fee',
field=models.DecimalField(decimal_places=2, max_digits=13),
),
migrations.AlterField(
model_name='cartposition',
name='custom_price_input',
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
),
migrations.AlterField(
model_name='cartposition',
name='line_price_gross',
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
),
migrations.AlterField(
model_name='cartposition',
name='listed_price',
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
),
migrations.AlterField(
model_name='cartposition',
name='price',
field=models.DecimalField(decimal_places=2, max_digits=13),
),
migrations.AlterField(
model_name='cartposition',
name='price_after_voucher',
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
),
migrations.AlterField(
model_name='discount',
name='condition_min_value',
field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=13),
),
migrations.AlterField(
model_name='giftcardtransaction',
name='value',
field=models.DecimalField(decimal_places=2, max_digits=13),
),
migrations.AlterField(
model_name='invoice',
name='foreign_currency_rate',
field=models.DecimalField(decimal_places=4, max_digits=13, null=True),
),
migrations.AlterField(
model_name='invoiceline',
name='gross_value',
field=models.DecimalField(decimal_places=2, max_digits=13),
),
migrations.AlterField(
model_name='invoiceline',
name='tax_value',
field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=13),
),
migrations.AlterField(
model_name='item',
name='default_price',
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
),
migrations.AlterField(
model_name='item',
name='original_price',
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
),
migrations.AlterField(
model_name='itembundle',
name='designated_price',
field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=13),
),
migrations.AlterField(
model_name='itemvariation',
name='default_price',
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
),
migrations.AlterField(
model_name='itemvariation',
name='original_price',
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
),
migrations.AlterField(
model_name='order',
name='total',
field=models.DecimalField(decimal_places=2, max_digits=13),
),
migrations.AlterField(
model_name='orderfee',
name='tax_value',
field=models.DecimalField(decimal_places=2, max_digits=13),
),
migrations.AlterField(
model_name='orderfee',
name='value',
field=models.DecimalField(decimal_places=2, max_digits=13),
),
migrations.AlterField(
model_name='orderpayment',
name='amount',
field=models.DecimalField(decimal_places=2, max_digits=13),
),
migrations.AlterField(
model_name='orderposition',
name='price',
field=models.DecimalField(decimal_places=2, max_digits=13),
),
migrations.AlterField(
model_name='orderposition',
name='tax_value',
field=models.DecimalField(decimal_places=2, max_digits=13),
),
migrations.AlterField(
model_name='orderposition',
name='voucher_budget_use',
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
),
migrations.AlterField(
model_name='orderrefund',
name='amount',
field=models.DecimalField(decimal_places=2, max_digits=13),
),
migrations.AlterField(
model_name='subeventitem',
name='price',
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
),
migrations.AlterField(
model_name='subeventitemvariation',
name='price',
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
),
migrations.AlterField(
model_name='transaction',
name='price',
field=models.DecimalField(decimal_places=2, max_digits=13),
),
migrations.AlterField(
model_name='transaction',
name='tax_value',
field=models.DecimalField(decimal_places=2, max_digits=13),
),
migrations.AlterField(
model_name='voucher',
name='budget',
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
),
migrations.AlterField(
model_name='voucher',
name='value',
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
),
]

View File

@@ -1,77 +0,0 @@
# Generated by Django 3.2.18 on 2023-02-20 12:46
import django.core.serializers.json
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.base
def set_can_manage_reusable_media(apps, schema_editor):
Team = apps.get_model('pretixbase', 'Team')
Team.objects.filter(can_change_organizer_settings=True).update(can_manage_reusable_media=True)
Team.objects.filter(can_change_orders=True, all_events=True).update(can_manage_reusable_media=True)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0235_auto_20230316_2023'),
]
operations = [
migrations.AddField(
model_name='item',
name='media_policy',
field=models.CharField(max_length=16, null=True),
),
migrations.AddField(
model_name='item',
name='media_type',
field=models.CharField(max_length=100, null=True),
),
migrations.AddField(
model_name='team',
name='can_manage_reusable_media',
field=models.BooleanField(default=False),
),
migrations.CreateModel(
name='ReusableMedium',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('type', models.CharField(max_length=100)),
('identifier', models.CharField(max_length=200)),
('active', models.BooleanField(default=True)),
('expires', models.DateTimeField(blank=True, null=True)),
('info', models.JSONField(default=dict)),
('notes', models.TextField(null=True, blank=True)),
('customer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL,
related_name='reusable_media', to='pretixbase.customer')),
('linked_giftcard',
models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='linked_media',
to='pretixbase.giftcard')),
('linked_orderposition',
models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='linked_media',
to='pretixbase.orderposition')),
('organizer',
models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='reusable_media',
to='pretixbase.organizer')),
],
options={
'ordering': ('identifier', 'type', 'organizer'),
'unique_together': {('identifier', 'type', 'organizer')},
'index_together': {('identifier', 'type', 'organizer'), ('updated', 'id')},
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.AddField(
model_name='checkin',
name='raw_source_type',
field=models.CharField(max_length=100, null=True),
),
migrations.RunPython(
set_can_manage_reusable_media,
migrations.RunPython.noop,
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.18 on 2023-04-05 10:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0236_reusable_media'),
]
operations = [
migrations.AddField(
model_name='question',
name='valid_string_length_max',
field=models.PositiveIntegerField(null=True),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 3.2.18 on 2023-05-04 12:19
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0237_question_valid_string_length'),
]
operations = [
migrations.AddField(
model_name='giftcard',
name='owner_ticket',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='owned_gift_cards', to='pretixbase.orderposition'),
),
]

View File

@@ -1,24 +0,0 @@
# Generated by Django 3.2.18 on 2023-05-11 11:02
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0238_giftcard_owner_ticket'),
]
operations = [
migrations.AddField(
model_name='giftcardtransaction',
name='acceptor',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='gift_card_transactions', to='pretixbase.organizer'),
),
migrations.AddField(
model_name='giftcardtransaction',
name='info',
field=models.JSONField(null=True),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 3.2.18 on 2023-05-16 11:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0239_giftcard_info'),
]
operations = [
migrations.AddField(
model_name='voucher',
name='all_addons_included',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='voucher',
name='all_bundles_included',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 3.2.19 on 2023-05-25 15:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0240_auto_20230516_1119'),
]
operations = [
migrations.AddField(
model_name='itemmetaproperty',
name='allowed_values',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='itemmetaproperty',
name='required',
field=models.BooleanField(default=False),
),
]

View File

@@ -40,7 +40,6 @@ from .items import (
SubEventItem, SubEventItemVariation, itempicture_upload_to,
)
from .log import LogEntry
from .media import ReusableMedium
from .memberships import Membership, MembershipType
from .notifications import NotificationSetting
from .orders import (

View File

@@ -44,7 +44,6 @@ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager, scopes_disabled
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import LoggedModel
from pretix.base.models.fields import MultiStringField
from pretix.helpers import PostgresWindowFrame
@@ -56,12 +55,7 @@ class CheckinList(LoggedModel):
all_products = models.BooleanField(default=True, verbose_name=_("All products (including newly created ones)"))
limit_products = models.ManyToManyField('Item', verbose_name=_("Limit to products"), blank=True)
subevent = models.ForeignKey('SubEvent', null=True, blank=True,
verbose_name=pgettext_lazy('subevent', 'Date'),
on_delete=models.CASCADE,
help_text=_('If you choose "all dates", tickets will be considered part of this list '
'and valid for check-in regardless of which date they are purchased for. '
'You can limit their validity through the advanced check-in rules, '
'though.'))
verbose_name=pgettext_lazy('subevent', 'Date'), on_delete=models.CASCADE)
include_pending = models.BooleanField(verbose_name=pgettext_lazy('checkin', 'Include pending orders'),
default=False,
help_text=_('With this option, people will be able to check in even if the '
@@ -378,11 +372,6 @@ class Checkin(models.Model):
# For "raw" scans where we do not know which position they belong to (e.g. scan of signed
# barcode that is not in database).
raw_barcode = models.TextField(null=True, blank=True)
raw_source_type = models.CharField(
max_length=100,
null=True, blank=True,
choices=[(k, v) for k, v in MEDIA_TYPES.items()],
)
raw_item = models.ForeignKey(
'pretixbase.Item',
related_name='checkins',

View File

@@ -39,7 +39,6 @@ from pretix.base.models.fields import MultiStringField
from pretix.base.models.organizer import Organizer
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.helpers.countries import FastCountryField
from pretix.helpers.names import build_name
class CustomerSSOProvider(LoggedModel):
@@ -143,7 +142,6 @@ class Customer(LoggedModel):
self.save()
self.all_logentries().update(data={}, shredded=True)
self.orders.all().update(customer=None)
self.reusable_media.all().update(customer=None)
self.memberships.all().update(attendee_name_parts=None)
self.attendee_profiles.all().delete()
self.invoice_addresses.all().delete()
@@ -172,11 +170,15 @@ class Customer(LoggedModel):
@property
def name(self):
return build_name(self.name_parts, fallback_scheme=lambda: self.organizer.settings.name_scheme) or ""
@property
def name_all_components(self):
return build_name(self.name_parts, "concatenation_all_components", fallback_scheme=lambda: self.organizer.settings.name_scheme) or ""
if not self.name_parts:
return ""
if '_legacy' in self.name_parts:
return self.name_parts['_legacy']
if '_scheme' in self.name_parts:
scheme = PERSON_NAME_SCHEMES[self.name_parts['_scheme']]
else:
raise TypeError("Invalid name given.")
return scheme['concatenation'](self.name_parts).strip()
def __str__(self):
s = f'#{self.identifier}'
@@ -219,7 +221,7 @@ class Customer(LoggedModel):
return salted_hmac(key_salt, payload).hexdigest()
def get_email_context(self):
from pretix.base.settings import get_name_parts_localized
from pretix.base.email import get_name_parts_localized
ctx = {
'name': self.name,
'organizer': self.organizer.name,
@@ -299,11 +301,15 @@ class AttendeeProfile(models.Model):
@property
def attendee_name(self):
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
@property
def attendee_name_all_components(self):
return build_name(self.attendee_name_parts, "concatenation_all_components", fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
if not self.attendee_name_parts:
return None
if '_legacy' in self.attendee_name_parts:
return self.attendee_name_parts['_legacy']
if '_scheme' in self.attendee_name_parts:
scheme = PERSON_NAME_SCHEMES[self.attendee_name_parts['_scheme']]
else:
scheme = PERSON_NAME_SCHEMES[self.customer.organizer.settings.name_scheme]
return scheme['concatenation'](self.attendee_name_parts).strip()
@property
def state_name(self):

View File

@@ -180,8 +180,7 @@ class Device(LoggedModel):
'can_view_orders',
'can_change_orders',
'can_view_vouchers',
'can_manage_gift_cards',
'can_manage_reusable_media',
'can_manage_gift_cards'
}
def get_event_permission_set(self, organizer, event) -> set:

View File

@@ -116,7 +116,7 @@ class Discount(LoggedModel):
condition_min_value = models.DecimalField(
verbose_name=_('Minimum gross value of matching products'),
decimal_places=2,
max_digits=13,
max_digits=10,
default=Decimal('0.00'),
)

View File

@@ -290,19 +290,19 @@ class EventMixin:
return safe_string(json.dumps(eventdict))
@classmethod
def annotated(cls, qs, channel='web', voucher=None):
def annotated(cls, qs, channel='web'):
from pretix.base.models import Item, ItemVariation, Quota
sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel, voucher=voucher).filter(
sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel).filter(
Q(variations__isnull=True)
& Q(quotas__pk=OuterRef('pk'))
).order_by().values_list('quotas__pk').annotate(
items=GroupConcat('pk', delimiter=',')
).values('items')
q_variation = (
sq_active_variation = ItemVariation.objects.filter(
Q(active=True)
& Q(sales_channels__contains=channel)
& Q(hide_without_voucher=False)
& 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)
@@ -310,23 +310,10 @@ class EventMixin:
& 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__hide_without_voucher=False)
& Q(item__require_bundling=False)
& Q(quotas__pk=OuterRef('pk'))
)
if voucher:
if voucher.variation_id:
q_variation &= Q(pk=voucher.variation_id)
elif voucher.item_id:
q_variation &= Q(item_id=voucher.item_id)
elif voucher.quota_id:
q_variation &= Q(quotas__in=[voucher.quota_id])
if not voucher or not voucher.show_hidden_items:
q_variation &= Q(hide_without_voucher=False)
q_variation &= Q(item__hide_without_voucher=False)
sq_active_variation = ItemVariation.objects.filter(q_variation).order_by().values_list('quotas__pk').annotate(
).order_by().values_list('quotas__pk').annotate(
items=GroupConcat('pk', delimiter=',')
).values('items')
quota_base_qs = Quota.objects.using(settings.DATABASE_REPLICA).filter(
@@ -638,7 +625,6 @@ class Event(EventMixin, LoggedModel):
"""
self.settings.invoice_renderer = 'modern1'
self.settings.invoice_include_expire_date = True
self.settings.invoice_renderer_highlight_order_code = True
self.settings.ticketoutput_pdf__enabled = True
self.settings.ticketoutput_passbook__enabled = True
self.settings.event_list_type = 'calendar'
@@ -1146,8 +1132,8 @@ class Event(EventMixin, LoggedModel):
irs = self.get_invoice_renderers()
return irs[self.settings.invoice_renderer]
def subevents_annotated(self, channel, voucher=None):
return SubEvent.annotated(self.subevents, channel, voucher)
def subevents_annotated(self, channel):
return SubEvent.annotated(self.subevents, channel)
def subevents_sorted(self, queryset):
ordering = self.settings.get('frontpage_subevent_ordering', default='date_ascending', as_type=str)
@@ -1467,10 +1453,10 @@ class SubEvent(EventMixin, LoggedModel):
return qs_annotated
@classmethod
def annotated(cls, qs, channel='web', voucher=None):
def annotated(cls, qs, channel='web'):
from .items import SubEventItem, SubEventItemVariation
qs = super().annotated(qs, channel, voucher=voucher)
qs = super().annotated(qs, channel)
qs = qs.annotate(
disabled_items=Coalesce(
Subquery(

View File

@@ -25,9 +25,7 @@ from django.conf import settings
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Sum
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.html import format_html
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
@@ -68,13 +66,6 @@ class GiftCard(LoggedModel):
on_delete=models.PROTECT,
null=True, blank=True
)
owner_ticket = models.ForeignKey(
'OrderPosition',
related_name='owned_gift_cards',
on_delete=models.PROTECT,
null=True, blank=True,
verbose_name=_('Owned by ticket holder')
)
issuance = models.DateTimeField(
auto_now_add=True,
)
@@ -138,7 +129,7 @@ class GiftCardTransaction(models.Model):
)
value = models.DecimalField(
decimal_places=2,
max_digits=13
max_digits=10
)
order = models.ForeignKey(
'Order',
@@ -162,61 +153,6 @@ class GiftCardTransaction(models.Model):
on_delete=models.PROTECT
)
text = models.TextField(blank=True, null=True)
info = models.JSONField(
null=True, blank=True,
)
acceptor = models.ForeignKey(
'Organizer',
related_name='gift_card_transactions',
on_delete=models.PROTECT,
null=True, blank=True
)
class Meta:
ordering = ("datetime",)
def save(self, *args, **kwargs):
if not self.pk and not self.acceptor:
raise ValueError("`acceptor` should be set on all new gift card transactions.")
super().save(*args, **kwargs)
def display(self, customer_facing=True):
from ..signals import gift_card_transaction_display
for receiver, response in gift_card_transaction_display.send(self, transaction=self, customer_facing=customer_facing):
if response:
return response
if self.order_id:
if not self.text:
if not customer_facing:
return format_html(
'<a href="{}">{}</a>',
reverse(
"control:event.order",
kwargs={
"event": self.order.event.slug,
"organizer": self.order.event.organizer.slug,
"code": self.order.code,
}
),
self.order.full_code
)
return self.order.full_code
else:
return self.text
else:
if self.text:
return format_html(
'<em>{}:</em> {}',
_('Manual transaction'),
self.text,
)
else:
return _('Manual transaction')
def display_backend(self):
return self.display(customer_facing=False)
def display_presale(self):
return self.display(customer_facing=True)

View File

@@ -152,7 +152,7 @@ class Invoice(models.Model):
footer_text = models.TextField(blank=True)
foreign_currency_display = models.CharField(max_length=50, null=True, blank=True)
foreign_currency_rate = models.DecimalField(decimal_places=4, max_digits=13, null=True, blank=True)
foreign_currency_rate = models.DecimalField(decimal_places=4, max_digits=10, null=True, blank=True)
foreign_currency_rate_date = models.DateField(null=True, blank=True)
foreign_currency_source = models.CharField(max_length=100, null=True, blank=True)
@@ -347,8 +347,8 @@ class InvoiceLine(models.Model):
invoice = models.ForeignKey('Invoice', related_name='lines', on_delete=models.CASCADE)
position = models.PositiveIntegerField(default=0)
description = models.TextField()
gross_value = models.DecimalField(max_digits=13, decimal_places=2)
tax_value = models.DecimalField(max_digits=13, decimal_places=2, default=Decimal('0.00'))
gross_value = models.DecimalField(max_digits=10, decimal_places=2)
tax_value = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
tax_rate = models.DecimalField(max_digits=7, decimal_places=2, default=Decimal('0.00'))
tax_name = models.CharField(max_length=190)
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)

View File

@@ -45,9 +45,7 @@ import dateutil.parser
import pytz
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import (
MaxLengthValidator, MinValueValidator, RegexValidator,
)
from django.core.validators import MinValueValidator, RegexValidator
from django.db import models
from django.db.models import Q
from django.utils import formats
@@ -66,7 +64,6 @@ from pretix.base.models.fields import MultiStringField
from pretix.base.models.tax import TaxedPrice
from ...helpers.images import ImageSizeValidator
from ..media import MEDIA_TYPES
from .event import Event, SubEvent
@@ -167,7 +164,7 @@ class SubEventItem(models.Model):
"""
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE)
item = models.ForeignKey('Item', on_delete=models.CASCADE)
price = models.DecimalField(max_digits=13, decimal_places=2, null=True, blank=True)
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
disabled = models.BooleanField(default=False, verbose_name=_('Disable product for this date'))
available_from = models.DateTimeField(
verbose_name=_("Available from"),
@@ -223,7 +220,7 @@ class SubEventItemVariation(models.Model):
"""
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE)
variation = models.ForeignKey('ItemVariation', on_delete=models.CASCADE)
price = models.DecimalField(max_digits=13, decimal_places=2, null=True, blank=True)
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
disabled = models.BooleanField(default=False, verbose_name=_('Disable product for this date'))
available_from = models.DateTimeField(
verbose_name=_("Available from"),
@@ -371,16 +368,6 @@ class Item(LoggedModel):
(VALIDITY_MODE_DYNAMIC, _('Dynamic validity')),
)
MEDIA_POLICY_REUSE = 'reuse'
MEDIA_POLICY_NEW = 'new'
MEDIA_POLICY_REUSE_OR_NEW = 'reuse_or_new'
MEDIA_POLICIES = (
(None, _("Don't use re-usable media, use regular one-off tickets")),
(MEDIA_POLICY_REUSE, _('Require an existing medium to be re-used')),
(MEDIA_POLICY_NEW, _('Require a previously unknown medium to be newly added')),
(MEDIA_POLICY_REUSE_OR_NEW, _('Require either an existing or a new medium to be used')),
)
objects = ItemQuerySetManager()
event = models.ForeignKey(
@@ -420,7 +407,7 @@ class Item(LoggedModel):
help_text=_("If this product has multiple variations, you can set different prices for each of the "
"variations. If a variation does not have a special price or if you do not have variations, "
"this price will be used."),
max_digits=13, decimal_places=2, null=True
max_digits=7, decimal_places=2, null=True
)
free_price = models.BooleanField(
default=False,
@@ -551,7 +538,7 @@ class Item(LoggedModel):
original_price = models.DecimalField(
verbose_name=_('Original price'),
blank=True, null=True,
max_digits=13, decimal_places=2,
max_digits=7, decimal_places=2,
help_text=_('If set, this will be displayed next to the current price to show that the current price is a '
'discounted one. This is just a cosmetic setting and will not actually impact pricing.')
)
@@ -643,29 +630,6 @@ class Item(LoggedModel):
help_text=_('The selected start date may only be this many days in the future.')
)
media_policy = models.CharField(
choices=MEDIA_POLICIES,
null=True, blank=True, max_length=16,
verbose_name=_('Reusable media policy'),
help_text=_(
'If this product should be stored on a re-usable physical medium, you can attach a physical media policy. '
'This is not required for regular tickets, which just use a one-time barcode, but only for products like '
'renewable season tickets or re-chargeable gift card wristbands. '
'This is an advanced feature that also requires specific configuration of ticketing and printing settings.'
)
)
media_type = models.CharField(
max_length=100,
null=True, blank=True,
choices=[(None, _("Don't use re-usable media, use regular one-off tickets"))] + [(k, v) for k, v in MEDIA_TYPES.items()],
verbose_name=_('Reusable media type'),
help_text=_(
'Select the type of physical medium that should be used for this product. Note that not all media types '
'support all types of products, and not all media types are supported across all sales channels or '
'check-in processes.'
)
)
# !!! Attention: If you add new fields here, also add them to the copying code in
# pretix/control/forms/item.py if applicable.
@@ -837,24 +801,6 @@ class Item(LoggedModel):
def has_variations(self):
return self.variations.exists()
@staticmethod
def clean_media_settings(event, media_policy, media_type, issue_giftcard):
if media_policy:
if not media_type:
raise ValidationError(_('If you select a reusable media policy, you also need to select a reusable '
'media type.'))
mt = MEDIA_TYPES[media_type]
if not mt.is_active(event.organizer):
raise ValidationError(_('The selected media type is not enabled in your organizer settings.'))
if not mt.supports_orderposition and not issue_giftcard:
raise ValidationError(_('The selected media type does not support usage for tickets currently.'))
if not mt.supports_giftcard and issue_giftcard:
raise ValidationError(_('The selected media type does not support usage for gift cards currently.'))
if issue_giftcard:
raise ValidationError(_('You currently cannot create gift cards with a reusable media policy. Instead, '
'gift cards for some reusable media types can be created or re-charged directly '
'at the POS.'))
@staticmethod
def clean_per_order(min_per_order, max_per_order):
if min_per_order is not None and max_per_order is not None:
@@ -1006,14 +952,14 @@ class ItemVariation(models.Model):
verbose_name=_("Position")
)
default_price = models.DecimalField(
decimal_places=2, max_digits=13,
decimal_places=2, max_digits=7,
null=True, blank=True,
verbose_name=_("Default price"),
)
original_price = models.DecimalField(
verbose_name=_('Original price'),
blank=True, null=True,
max_digits=13, decimal_places=2,
max_digits=7, decimal_places=2,
help_text=_('If set, this will be displayed next to the current price to show that the current price is a '
'discounted one. This is just a cosmetic setting and will not actually impact pricing.')
)
@@ -1358,7 +1304,7 @@ class ItemBundle(models.Model):
)
designated_price = models.DecimalField(
default=Decimal('0.00'), blank=True,
decimal_places=2, max_digits=13,
decimal_places=2, max_digits=10,
verbose_name=_('Designated price part'),
help_text=_('If set, it will be shown that this bundled item is responsible for the given value of the total '
'gross price. This might be important in cases of mixed taxation, but can be kept blank otherwise. This '
@@ -1542,11 +1488,6 @@ class Question(LoggedModel):
valid_datetime_max = models.DateTimeField(null=True, blank=True,
verbose_name=_('Maximum value'),
help_text=_('Currently not supported in our apps and during check-in'))
valid_string_length_max = models.PositiveIntegerField(null=True, blank=True,
verbose_name=_('Maximum length'),
help_text=_(
'Currently not supported in our apps and during check-in'
))
valid_file_portrait = models.BooleanField(
default=False,
verbose_name=_('Validate file to be a portrait'),
@@ -1693,12 +1634,6 @@ class Question(LoggedModel):
return answer
else:
raise ValidationError(_('Unknown country code.'))
elif self.type in (Question.TYPE_STRING, Question.TYPE_TEXT):
if self.valid_string_length_max is not None and len(answer) > self.valid_string_length_max:
raise ValidationError(MaxLengthValidator.message % {
'limit_value': self.valid_string_length_max,
'show_value': len(answer)
})
return answer
@@ -2001,15 +1936,6 @@ class ItemMetaProperty(LoggedModel):
verbose_name=_("Name"),
)
default = models.TextField(blank=True)
required = models.BooleanField(
default=False, verbose_name=_("Required for products"),
help_text=_("If checked, this property must be set in each product. Does not apply if a default value is set.")
)
allowed_values = models.TextField(
null=True, blank=True,
verbose_name=_("Valid values"),
help_text=_("If you keep this empty, any value is allowed. Otherwise, enter one possible value per line.")
)
class Meta:
ordering = ("name",)

View File

@@ -1,125 +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/>.
#
from django.db import models
from django.db.models import Q
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import LoggedModel
from pretix.base.models.customers import Customer
from pretix.base.models.giftcards import GiftCard
from pretix.base.models.orders import OrderPosition
from pretix.base.models.organizer import Organizer
class ReusableMediumQuerySet(models.QuerySet):
def active(self):
return self.filter(
Q(expires__isnull=True) | Q(expires__gte=now()),
active=True,
)
class ReusableMediumQuerySetManager(ScopedManager(organizer='organizer').__class__):
def __init__(self):
super().__init__()
self._queryset_class = ReusableMediumQuerySet
def active(self):
return self.get_queryset().active()
class ReusableMedium(LoggedModel):
id = models.BigAutoField(primary_key=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
organizer = models.ForeignKey(
Organizer,
related_name='reusable_media',
on_delete=models.PROTECT
)
type = models.CharField(
verbose_name=pgettext_lazy('reusable_medium', 'Media type'),
choices=((k, v) for k, v in MEDIA_TYPES.items()),
max_length=100,
)
identifier = models.CharField(
max_length=200,
verbose_name=pgettext_lazy('reusable_medium', 'Identifier'),
)
active = models.BooleanField(
verbose_name=_('Active'),
default=True
)
expires = models.DateTimeField(
verbose_name=_('Expiration date'),
null=True, blank=True
)
customer = models.ForeignKey(
Customer,
null=True, blank=True,
related_name='reusable_media',
on_delete=models.SET_NULL,
verbose_name=_('Customer account'),
)
linked_orderposition = models.ForeignKey(
OrderPosition,
null=True, blank=True,
related_name='linked_media',
on_delete=models.SET_NULL,
verbose_name=_('Linked ticket'),
)
linked_giftcard = models.ForeignKey(
GiftCard,
null=True, blank=True,
related_name='linked_media',
on_delete=models.SET_NULL,
verbose_name=_('Linked gift card'),
)
info = models.JSONField(
default=dict
)
notes = models.TextField(verbose_name=_('Notes'), null=True, blank=True)
objects = ReusableMediumQuerySetManager()
@cached_property
def media_type(self):
return MEDIA_TYPES[self.type]
@property
def is_expired(self):
return self.expires and self.expires > now()
class Meta:
unique_together = (("identifier", "type", "organizer"),)
index_together = (("identifier", "type", "organizer"), ("updated", "id"))
ordering = "identifier", "type", "organizer"

View File

@@ -31,7 +31,7 @@ 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.helpers.names import build_name
from pretix.base.settings import PERSON_NAME_SCHEMES
class MembershipType(LoggedModel):
@@ -160,7 +160,15 @@ class Membership(models.Model):
@property
def attendee_name(self):
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
if not self.attendee_name_parts:
return None
if '_legacy' in self.attendee_name_parts:
return self.attendee_name_parts['_legacy']
if '_scheme' in self.attendee_name_parts:
scheme = PERSON_NAME_SCHEMES[self.attendee_name_parts['_scheme']]
else:
scheme = PERSON_NAME_SCHEMES[self.customer.organizer.settings.name_scheme]
return scheme['concatenation'](self.attendee_name_parts).strip()
def is_valid(self, ev=None):
if ev:

View File

@@ -82,7 +82,6 @@ from pretix.base.signals import order_gracefully_delete
from ...helpers import OF_SELF
from ...helpers.countries import CachedCountries, FastCountryField
from ...helpers.format import format_map
from ...helpers.names import build_name
from ._transactions import (
_fail, _transactions_mark_order_clean, _transactions_mark_order_dirty,
)
@@ -221,7 +220,7 @@ class Order(LockModel, LoggedModel):
verbose_name=_("Expiration date")
)
total = models.DecimalField(
decimal_places=2, max_digits=13,
decimal_places=2, max_digits=10,
verbose_name=_("Total amount")
)
comment = models.TextField(
@@ -404,8 +403,8 @@ class Order(LockModel, LoggedModel):
state__in=(OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT),
order=OuterRef('pk')
)
payment_sum_sq = Subquery(payment_sum, output_field=models.DecimalField(decimal_places=2, max_digits=13))
refund_sum_sq = Subquery(refund_sum, output_field=models.DecimalField(decimal_places=2, max_digits=13))
payment_sum_sq = Subquery(payment_sum, output_field=models.DecimalField(decimal_places=2, max_digits=10))
refund_sum_sq = Subquery(refund_sum, output_field=models.DecimalField(decimal_places=2, max_digits=10))
if sums:
qs = qs.annotate(
payment_sum=payment_sum_sq,
@@ -627,10 +626,7 @@ class Order(LockModel, LoggedModel):
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
).select_related('item').prefetch_related('issued_gift_cards')
)
if self.event.settings.change_allow_user_if_checked_in:
cancelable = all([op.item.allow_cancel for op in positions])
else:
cancelable = all([op.item.allow_cancel and not op.has_checkin for op in positions])
cancelable = all([op.item.allow_cancel and not op.has_checkin for op in positions])
if not cancelable or not positions:
return False
for op in positions:
@@ -831,7 +827,7 @@ class Order(LockModel, LoggedModel):
@property
def is_expired_by_time(self):
return (
self.status == Order.STATUS_PENDING and not self.require_approval and self.expires < now()
self.status == Order.STATUS_PENDING and self.expires < now()
and not self.event.settings.get('payment_term_expire_automatically')
)
@@ -989,7 +985,7 @@ class Order(LockModel, LoggedModel):
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
user: User=None, headers: dict=None, sender: str=None, invoices: list=None,
auth=None, attach_tickets=False, position: 'OrderPosition'=None, auto_email=True,
attach_ical=False, attach_other_files: list=None, attach_cached_files: list=None):
attach_ical=False, attach_other_files: list=None):
"""
Sends an email to the user that placed this order. Basically, this method does two things:
@@ -1034,7 +1030,7 @@ class Order(LockModel, LoggedModel):
self.event, self.locale, self, headers=headers, sender=sender,
invoices=invoices, attach_tickets=attach_tickets,
position=position, auto_email=auto_email, attach_ical=attach_ical,
attach_other_files=attach_other_files, attach_cached_files=attach_cached_files,
attach_other_files=attach_other_files,
)
except SendMailException:
raise
@@ -1317,7 +1313,7 @@ class AbstractPosition(models.Model):
on_delete=models.PROTECT
)
price = models.DecimalField(
decimal_places=2, max_digits=13,
decimal_places=2, max_digits=10,
verbose_name=_("Price")
)
attendee_name_cached = models.CharField(
@@ -1452,11 +1448,15 @@ class AbstractPosition(models.Model):
@property
def attendee_name(self):
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.event.settings.name_scheme)
@property
def attendee_name_all_components(self):
return build_name(self.attendee_name_parts, "concatenation_all_components", fallback_scheme=lambda: self.event.settings.name_scheme)
if not self.attendee_name_parts:
return None
if '_legacy' in self.attendee_name_parts:
return self.attendee_name_parts['_legacy']
if '_scheme' in self.attendee_name_parts:
scheme = PERSON_NAME_SCHEMES[self.attendee_name_parts['_scheme']]
else:
scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
return scheme['concatenation'](self.attendee_name_parts).strip()
@property
def state_name(self):
@@ -1545,7 +1545,7 @@ class OrderPayment(models.Model):
max_length=190, choices=PAYMENT_STATES
)
amount = models.DecimalField(
decimal_places=2, max_digits=13,
decimal_places=2, max_digits=10,
verbose_name=_("Amount")
)
order = models.ForeignKey(
@@ -1929,7 +1929,7 @@ class OrderRefund(models.Model):
max_length=190, choices=REFUND_SOURCES
)
amount = models.DecimalField(
decimal_places=2, max_digits=13,
decimal_places=2, max_digits=10,
verbose_name=_("Amount")
)
order = models.ForeignKey(
@@ -2078,7 +2078,7 @@ class OrderFee(models.Model):
)
value = models.DecimalField(
decimal_places=2, max_digits=13,
decimal_places=2, max_digits=10,
verbose_name=_("Value")
)
order = models.ForeignKey(
@@ -2102,7 +2102,7 @@ class OrderFee(models.Model):
null=True, blank=True
)
tax_value = models.DecimalField(
max_digits=13, decimal_places=2,
max_digits=10, decimal_places=2,
verbose_name=_('Tax value')
)
canceled = models.BooleanField(default=False)
@@ -2236,7 +2236,7 @@ class OrderPosition(AbstractPosition):
)
voucher_budget_use = models.DecimalField(
max_digits=13, decimal_places=2, null=True, blank=True,
max_digits=10, decimal_places=2, null=True, blank=True,
)
tax_rate = models.DecimalField(
@@ -2249,7 +2249,7 @@ class OrderPosition(AbstractPosition):
null=True, blank=True
)
tax_value = models.DecimalField(
max_digits=13, decimal_places=2,
max_digits=10, decimal_places=2,
verbose_name=_('Tax value')
)
@@ -2555,27 +2555,6 @@ class OrderPosition(AbstractPosition):
attach_tickets=True
)
@property
@scopes_disabled()
def attendee_change_allowed(self) -> bool:
"""
Returns whether or not this order can be changed by the attendee.
"""
from .items import ItemAddOn
if not self.event.settings.change_allow_attendee or not self.order.user_change_allowed:
return False
positions = list(
self.order.positions.filter(Q(pk=self.pk) | Q(addon_to_id=self.pk)).annotate(
has_variations=Exists(ItemVariation.objects.filter(item_id=OuterRef('item_id'))),
).select_related('item').prefetch_related('issued_gift_cards')
)
return (
(self.order.event.settings.change_allow_user_variation and any([op.has_variations for op in positions])) or
(self.order.event.settings.change_allow_user_addons and ItemAddOn.objects.filter(base_item_id__in=[op.item_id for op in positions]).exists())
)
class Transaction(models.Model):
"""
@@ -2670,7 +2649,7 @@ class Transaction(models.Model):
verbose_name=pgettext_lazy("subevent", "Date"),
)
price = models.DecimalField(
decimal_places=2, max_digits=13,
decimal_places=2, max_digits=10,
verbose_name=_("Price")
)
tax_rate = models.DecimalField(
@@ -2683,7 +2662,7 @@ class Transaction(models.Model):
null=True, blank=True
)
tax_value = models.DecimalField(
max_digits=13, decimal_places=2,
max_digits=10, decimal_places=2,
verbose_name=_('Tax value')
)
fee_type = models.CharField(
@@ -2761,19 +2740,19 @@ class CartPosition(AbstractPosition):
verbose_name=_('Tax rate')
)
listed_price = models.DecimalField(
decimal_places=2, max_digits=13, null=True,
decimal_places=2, max_digits=10, null=True,
)
price_after_voucher = models.DecimalField(
decimal_places=2, max_digits=13, null=True,
decimal_places=2, max_digits=10, null=True,
)
custom_price_input = models.DecimalField(
decimal_places=2, max_digits=13, null=True,
decimal_places=2, max_digits=10, null=True,
)
custom_price_input_is_net = models.BooleanField(
default=False,
)
line_price_gross = models.DecimalField(
decimal_places=2, max_digits=13, null=True,
decimal_places=2, max_digits=10, null=True,
)
requested_valid_from = models.DateTimeField(
null=True,
@@ -2831,12 +2810,8 @@ class CartPosition(AbstractPosition):
if self.is_bundled:
bundle = self.addon_to.item.bundles.filter(bundled_item=self.item, bundled_variation=self.variation).first()
if bundle:
if self.addon_to.voucher_id and self.addon_to.voucher.all_bundles_included:
listed_price = Decimal('0.00')
price_after_voucher = Decimal('0.00')
else:
listed_price = bundle.designated_price
price_after_voucher = bundle.designated_price
listed_price = bundle.designated_price
price_after_voucher = bundle.designated_price
if listed_price != self.listed_price or price_after_voucher != self.price_after_voucher:
self.listed_price = listed_price
@@ -2847,7 +2822,7 @@ class CartPosition(AbstractPosition):
# Migrate from pre-discounts position
if self.item.free_price and self.custom_price_input is None:
custom_price = self.price
if custom_price > 99_999_999_999:
if custom_price > 100000000:
raise ValueError('price_too_high')
self.custom_price_input = custom_price
self.custom_price_input_is_net = not False
@@ -2981,11 +2956,15 @@ class InvoiceAddress(models.Model):
@property
def name(self):
return build_name(self.name_parts, fallback_scheme=lambda: self.order.event.settings.name_scheme) or ""
@property
def name_all_components(self):
return build_name(self.name_parts, "concatenation_all_components", fallback_scheme=lambda: self.order.event.settings.name_scheme) or ""
if not self.name_parts:
return ""
if '_legacy' in self.name_parts:
return self.name_parts['_legacy']
if '_scheme' in self.name_parts:
scheme = PERSON_NAME_SCHEMES[self.name_parts['_scheme']]
else:
raise TypeError("Invalid name given.")
return scheme['concatenation'](self.name_parts).strip()
def for_js(self):
d = {}
@@ -3056,7 +3035,7 @@ class CachedCombinedTicket(models.Model):
class CancellationRequest(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='cancellation_requests')
created = models.DateTimeField(auto_now_add=True)
cancellation_fee = models.DecimalField(max_digits=13, decimal_places=2)
cancellation_fee = models.DecimalField(max_digits=10, decimal_places=2)
refund_as_giftcard = models.BooleanField(default=False)

View File

@@ -236,8 +236,6 @@ class Team(LoggedModel):
:type can_change_teams: bool
:param can_manage_customers: If ``True``, the members can view and change organizer-level customer accounts.
:type can_manage_customers: bool
:param can_manage_reusable_media: If ``True``, the members can view and change organizer-level reusable media.
:type can_manage_reusable_media: bool
:param can_change_organizer_settings: If ``True``, the members can change the settings of this organizer account.
:type can_change_organizer_settings: bool
:param can_change_event_settings: If ``True``, the members can change the settings of the associated events.
@@ -279,10 +277,6 @@ class Team(LoggedModel):
default=False,
verbose_name=_("Can manage customer accounts")
)
can_manage_reusable_media = models.BooleanField(
default=False,
verbose_name=_("Can manage reusable media")
)
can_manage_gift_cards = models.BooleanField(
default=False,
verbose_name=_("Can manage gift cards")

View File

@@ -213,7 +213,7 @@ class Voucher(LoggedModel):
verbose_name=_("Maximum discount budget"),
help_text=_("This is the maximum monetary amount that will be discounted using this voucher across all usages. "
"If this is sum reached, the voucher can no longer be used."),
decimal_places=2, max_digits=13,
decimal_places=2, max_digits=10,
null=True, blank=True
)
valid_until = models.DateTimeField(
@@ -243,7 +243,7 @@ class Voucher(LoggedModel):
)
value = models.DecimalField(
verbose_name=_("Voucher value"),
decimal_places=2, max_digits=13, null=True, blank=True,
decimal_places=2, max_digits=10, null=True, blank=True,
)
item = models.ForeignKey(
Item, related_name='vouchers',
@@ -296,14 +296,6 @@ class Voucher(LoggedModel):
verbose_name=_("Shows hidden products that match this voucher"),
default=True
)
all_addons_included = models.BooleanField(
verbose_name=_("Offer all add-on products for free when redeeming this voucher"),
default=False
)
all_bundles_included = models.BooleanField(
verbose_name=_("Include all bundled products without a designated price when redeeming this voucher"),
default=False
)
objects = ScopedManager(organizer='event__organizer')
@@ -462,7 +454,7 @@ class Voucher(LoggedModel):
@staticmethod
def clean_voucher_code(data, event, pk):
if 'code' in data and Voucher.objects.filter(Q(code__iexact=data['code'].upper()) & Q(event=event) & ~Q(pk=pk)).exists():
if 'code' in data and Voucher.objects.filter(Q(code__iexact=data['code']) & Q(event=event) & ~Q(pk=pk)).exists():
raise ValidationError(_('A voucher with this code already exists.'))
@staticmethod
@@ -607,7 +599,7 @@ class Voucher(LoggedModel):
Order.STATUS_PENDING
]
).order_by().values('voucher_id').annotate(s=Sum('voucher_budget_use')).values('s')
return qs.annotate(budget_used_orders=Coalesce(Subquery(opq, output_field=models.DecimalField(max_digits=13, decimal_places=2)), Decimal('0.00')))
return qs.annotate(budget_used_orders=Coalesce(Subquery(opq, output_field=models.DecimalField(max_digits=10, decimal_places=2)), Decimal('0.00')))
def budget_used(self):
ops = OrderPosition.objects.filter(

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