forked from CGM_Public/pretix_original
Compare commits
32 Commits
dekodi
...
794-questi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df25ae3c7c | ||
|
|
d686b386e3 | ||
|
|
bdda68c82d | ||
|
|
2aa36ee2a7 | ||
|
|
43affc22ab | ||
|
|
60575377d7 | ||
|
|
5ec8c7ed96 | ||
|
|
ef55a018f8 | ||
|
|
6bf9327f87 | ||
|
|
f761f93550 | ||
|
|
c996289563 | ||
|
|
89bbed42e6 | ||
|
|
8e22c0f3a4 | ||
|
|
3483c522da | ||
|
|
5f15ebc46f | ||
|
|
3415fd947a | ||
|
|
a70a42c273 | ||
|
|
697cdfd5c9 | ||
|
|
d8a7de8b23 | ||
|
|
9f7f0e74ff | ||
|
|
7ef289da45 | ||
|
|
e82bc732a3 | ||
|
|
4636ccac3b | ||
|
|
e3518bfb4b | ||
|
|
b2471169af | ||
|
|
487418678c | ||
|
|
d4795868d6 | ||
|
|
45af18a23d | ||
|
|
a6de586b80 | ||
|
|
e6859fa82b | ||
|
|
2d5e14e517 | ||
|
|
7219575b84 |
@@ -58,16 +58,29 @@ Database
|
||||
--------
|
||||
|
||||
Next, we need a database and a database user. We can create these with any kind of database managing tool or directly on
|
||||
our database's shell, e.g. for MySQL::
|
||||
our database's shell. For PostgreSQL, we would do::
|
||||
|
||||
$ mysql -u root -p
|
||||
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||
mysql> GRANT ALL PRIVILEGES ON pretix.* TO pretix@'localhost' IDENTIFIED BY '*********';
|
||||
mysql> FLUSH PRIVILEGES;
|
||||
# sudo -u postgres createuser -P pretix
|
||||
# sudo -u postgres createdb -O pretix pretix
|
||||
|
||||
Replace the asterisks with a password of your own. For MySQL, we will use a unix domain socket to connect to the
|
||||
database. For PostgreSQL, be sure to configure the interface binding and your firewall so that the docker container
|
||||
can reach PostgreSQL.
|
||||
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'
|
||||
|
||||
You also need to add a new line to ``/etc/postgresql/<version>/main/pg_hba.conf`` to allow network connections to this user and database::
|
||||
|
||||
host pretix pretix 172.17.0.1/16 md5
|
||||
|
||||
Restart PostgreSQL after you changed these files::
|
||||
|
||||
# systemctl restart postgresql
|
||||
|
||||
If you have a firewall running, you should also make sure that port 5432 is reachable from the ``172.17.0.1/16`` subnet.
|
||||
|
||||
For MySQL, you can either also use network-based connections or mount the ``/var/run/mysqld/mysqld.sock`` socket into the docker container.
|
||||
When using MySQL, make sure you set the character set of the database to ``utf8mb4``, e.g. like this::
|
||||
|
||||
mysql > CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
Redis
|
||||
-----
|
||||
@@ -114,13 +127,16 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
|
||||
datadir=/data
|
||||
|
||||
[database]
|
||||
; Replace mysql with postgresql_psycopg2 for PostgreSQL
|
||||
backend=mysql
|
||||
; Replace postgresql with mysql for MySQL
|
||||
backend=postgresql
|
||||
name=pretix
|
||||
user=pretix
|
||||
; Replace with the password you chose above
|
||||
password=*********
|
||||
; Replace with host IP address for PostgreSQL
|
||||
host=/var/run/mysqld/mysqld.sock
|
||||
; In most docker setups, 172.17.0.1 is the address of the docker host. Adjuts
|
||||
; this to wherever your database is running, e.g. the name of a linked container
|
||||
; or of a mounted MySQL socket.
|
||||
host=172.17.0.1
|
||||
|
||||
[mail]
|
||||
; See config file documentation for more options
|
||||
@@ -164,14 +180,15 @@ named ``/etc/systemd/system/pretix.service`` with the following content::
|
||||
-v /var/pretix-data:/data \
|
||||
-v /etc/pretix:/etc/pretix \
|
||||
-v /var/run/redis:/var/run/redis \
|
||||
-v /var/run/mysqld:/var/run/mysqld \
|
||||
pretix/standalone:stable all
|
||||
ExecStop=/usr/bin/docker stop %n
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
You can leave the MySQL socket volume out if you're using PostgreSQL. You can now run the following commands
|
||||
When using MySQL and socket mounting, you'll need the additional flag ``-v /var/run/mysqld:/var/run/mysqld`` in the command.
|
||||
|
||||
You can now run the following commands
|
||||
to enable and start the service::
|
||||
|
||||
# systemctl daemon-reload
|
||||
|
||||
@@ -50,21 +50,27 @@ Database
|
||||
--------
|
||||
|
||||
Having the database server installed, we still need a database and a database user. We can create these with any kind
|
||||
of database managing tool or directly on our database's shell, e.g. for MySQL::
|
||||
of database managing tool or directly on our database's shell. For PostgreSQL, we would do::
|
||||
|
||||
$ mysql -u root -p
|
||||
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||
mysql> GRANT ALL PRIVILEGES ON pretix.* TO pretix@'localhost' IDENTIFIED BY '*********';
|
||||
mysql> FLUSH PRIVILEGES;
|
||||
# sudo -u postgres createuser pretix
|
||||
# sudo -u postgres createdb -O pretix pretix
|
||||
|
||||
When using MySQL, make sure you set the character set of the database to ``utf8mb4``, e.g. like this::
|
||||
|
||||
mysql > CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
Package dependencies
|
||||
--------------------
|
||||
|
||||
To build and run pretix, you will need the following debian packages::
|
||||
|
||||
# apt-get install git build-essential python-dev python-virtualenv python3 python3-pip \
|
||||
# apt-get install git build-essential python-dev python3-venv python3 python3-pip \
|
||||
python3-dev libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
|
||||
gettext libpq-dev libmysqlclient-dev libjpeg-dev libopenjp2-7-dev
|
||||
gettext libpq-dev libmariadbclient-dev libjpeg-dev libopenjp2-7-dev
|
||||
|
||||
.. note:: Python 3.7 is not yet supported, so if you run a very recent OS, make sure to get
|
||||
Python 3.6 from somewhere. You can check the current state of things in our
|
||||
`Python 3.7 issue`_.
|
||||
|
||||
Config file
|
||||
-----------
|
||||
@@ -85,13 +91,18 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
|
||||
datadir=/var/pretix/data
|
||||
|
||||
[database]
|
||||
; Replace mysql with postgresql_psycopg2 for PostgreSQL
|
||||
backend=mysql
|
||||
; For MySQL, replace with "mysql"
|
||||
backend=postgresql
|
||||
name=pretix
|
||||
user=pretix
|
||||
password=*********
|
||||
; Replace with host IP address for PostgreSQL
|
||||
host=/var/run/mysqld/mysqld.sock
|
||||
; For MySQL, enter the user password. For PostgreSQL on the same host,
|
||||
; we don't need one because we can use peer authentification if our
|
||||
; PostgreSQL user matches our unix user.
|
||||
password=
|
||||
; For MySQL, use local socket, e.g. /var/run/mysqld/mysqld.sock
|
||||
; For a remote host, supply an IP address
|
||||
; For local postgres authentication, you can leave it empty
|
||||
host=
|
||||
|
||||
[mail]
|
||||
; See config file documentation for more options
|
||||
@@ -115,14 +126,14 @@ Now we will install pretix itself. The following steps are to be executed as the
|
||||
actually install pretix, we will create a virtual environment to isolate the python packages from your global
|
||||
python installation::
|
||||
|
||||
$ virtualenv -p python3 /var/pretix/venv
|
||||
$ python3 -m venv /var/pretix/venv
|
||||
$ source /var/pretix/venv/bin/activate
|
||||
(venv)$ pip3 install -U pip setuptools wheel
|
||||
|
||||
We now install pretix, its direct dependencies and gunicorn. Replace ``mysql`` with ``postgres`` in the following
|
||||
command if you're running PostgreSQL::
|
||||
We now install pretix, its direct dependencies and gunicorn. Replace ``postgres`` with ``mysql`` in the following
|
||||
command if you're running MySQL::
|
||||
|
||||
(venv)$ pip3 install "pretix[mysql]" gunicorn
|
||||
(venv)$ pip3 install "pretix[postgres]" gunicorn
|
||||
|
||||
Note that you need Python 3.5 or newer. You can find out your Python version using ``python -V``.
|
||||
|
||||
@@ -268,10 +279,10 @@ Updates
|
||||
.. warning:: While we try hard not to break things, **please perform a backup before every upgrade**.
|
||||
|
||||
To upgrade to a new pretix release, pull the latest code changes and run the following commands (again, replace
|
||||
``mysql`` with ``postgres`` if necessary)::
|
||||
``postgres`` with ``mysql`` if necessary)::
|
||||
|
||||
$ source /var/pretix/venv/bin/activate
|
||||
(venv)$ pip3 install -U pretix[mysql] gunicorn
|
||||
(venv)$ pip3 install -U pretix[postgres] gunicorn
|
||||
(venv)$ python -m pretix migrate
|
||||
(venv)$ python -m pretix rebuild
|
||||
(venv)$ python -m pretix updatestyles
|
||||
@@ -303,3 +314,4 @@ example::
|
||||
.. _redis: https://blog.programster.org/debian-8-install-redis-server/
|
||||
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
|
||||
.. _strong encryption settings: https://mozilla.github.io/server-side-tls/ssl-config-generator/
|
||||
.. _Python 3.7 issue: https://github.com/pretix/pretix/issues/1025
|
||||
|
||||
@@ -20,7 +20,7 @@ internal_name string An optional nam
|
||||
description multi-lingual string A public description (might include markdown, can
|
||||
be ``null``)
|
||||
position integer An integer, used for sorting the categories
|
||||
is_addon boolean If ``True``, items within this category are not on sale
|
||||
is_addon boolean If ``true``, items within this category are not on sale
|
||||
on their own but the category provides a source for
|
||||
defining add-ons for other products.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
@@ -156,14 +156,14 @@ Endpoints
|
||||
"checkin_count": 17,
|
||||
"position_count": 42,
|
||||
"event": {
|
||||
"name": "Demo Converence",
|
||||
"name": "Demo Conference"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"name": "T-Shirt",
|
||||
"id": 1,
|
||||
"checkin_count": 1,
|
||||
"admission": False,
|
||||
"admission": false,
|
||||
"position_count": 1,
|
||||
"variations": [
|
||||
{
|
||||
@@ -184,7 +184,7 @@ Endpoints
|
||||
"name": "Ticket",
|
||||
"id": 2,
|
||||
"checkin_count": 15,
|
||||
"admission": True,
|
||||
"admission": true,
|
||||
"position_count": 22,
|
||||
"variations": []
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ is_public boolean If ``true``, th
|
||||
presale_start datetime The date at which the ticket shop opens (or ``null``)
|
||||
presale_end datetime The date at which the ticket shop closes (or ``null``)
|
||||
location multi-lingual string The event location (or ``null``)
|
||||
has_subevents boolean ``True`` if the event series feature is active for this
|
||||
has_subevents boolean ``true`` if the event series feature is active for this
|
||||
event. Cannot change after event is created.
|
||||
meta_data dict Values set for organizer-specific meta data parameters.
|
||||
plugins list A list of package names of the enabled plugins for this
|
||||
|
||||
@@ -13,7 +13,7 @@ Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
number string Invoice number (with prefix)
|
||||
order string Order code of the order this invoice belongs to
|
||||
is_cancellation boolean ``True``, if this invoice is the cancellation of a
|
||||
is_cancellation boolean ``true``, if this invoice is the cancellation of a
|
||||
different invoice.
|
||||
invoice_from string Sender address
|
||||
invoice_to string Receiver address
|
||||
|
||||
@@ -18,7 +18,7 @@ default_price money (string) The price set d
|
||||
price money (string) The price used for this variation. This is either the
|
||||
same as ``default_price`` if that value is set or equal
|
||||
to the item's ``default_price`` (read-only).
|
||||
active boolean If ``False``, this variation will not be sold or shown.
|
||||
active boolean If ``false``, this variation will not be sold or shown.
|
||||
description multi-lingual string A public description of the variation. May contain
|
||||
Markdown syntax or can be ``null``.
|
||||
position integer An integer, used for sorting
|
||||
|
||||
@@ -21,18 +21,18 @@ default_price money (string) The item price
|
||||
overwritten by variations or other options.
|
||||
category integer The ID of the category this item belongs to
|
||||
(or ``null``).
|
||||
active boolean If ``False``, the item is hidden from all public lists
|
||||
active boolean If ``false``, the item is hidden from all public lists
|
||||
and will not be sold.
|
||||
description multi-lingual string A public description of the item. May contain Markdown
|
||||
syntax or can be ``null``.
|
||||
free_price boolean If ``True``, customers can change the price at which
|
||||
free_price boolean If ``true``, customers can change the price at which
|
||||
they buy the product (however, the price can't be set
|
||||
lower than the price defined by ``default_price`` or
|
||||
otherwise).
|
||||
tax_rate decimal (string) The VAT rate to be applied for this item.
|
||||
tax_rule integer The internal ID of the applied tax rule (or ``null``).
|
||||
admission boolean ``True`` for items that grant admission to the event
|
||||
(such as primary tickets) and ``False`` for others
|
||||
admission boolean ``true`` for items that grant admission to the event
|
||||
(such as primary tickets) and ``false`` for others
|
||||
(such as add-ons or merchandise).
|
||||
position integer An integer, used for sorting
|
||||
picture string A product picture to be displayed in the shop
|
||||
@@ -43,12 +43,12 @@ available_from datetime The first date
|
||||
(or ``null``).
|
||||
available_until datetime The last date time at which this item can be bought
|
||||
(or ``null``).
|
||||
require_voucher boolean If ``True``, this item can only be bought using a
|
||||
require_voucher boolean If ``true``, this item can only be bought using a
|
||||
voucher that is specifically assigned to this item.
|
||||
hide_without_voucher boolean If ``True``, this item is only shown during the voucher
|
||||
hide_without_voucher boolean If ``true``, this item is only shown during the voucher
|
||||
redemption process, but not in the normal shop
|
||||
frontend.
|
||||
allow_cancel boolean If ``False``, customers cannot cancel orders containing
|
||||
allow_cancel boolean If ``false``, customers cannot cancel orders containing
|
||||
this item.
|
||||
min_per_order integer This product can only be bought if it is included at
|
||||
least this many times in the order (or ``null`` for no
|
||||
@@ -56,17 +56,17 @@ min_per_order integer This product ca
|
||||
max_per_order integer This product can only be bought if it is included at
|
||||
most this many times in the order (or ``null`` for no
|
||||
limitation).
|
||||
checkin_attention boolean If ``True``, the check-in app should show a warning
|
||||
checkin_attention boolean If ``true``, the check-in app should show a warning
|
||||
that this ticket requires special attention if such
|
||||
a product is being scanned.
|
||||
original_price money (string) An original price, shown for comparison, not used
|
||||
for price calculations (or ``null``).
|
||||
require_approval boolean If ``True``, orders with this product will need to be
|
||||
require_approval boolean If ``true``, orders with this product will need to be
|
||||
approved by the event organizer before they can be
|
||||
paid.
|
||||
require_bundling boolean If ``True``, this item is only available as part of bundles.
|
||||
generate_tickets boolean If ``False``, tickets are never generated for this
|
||||
product, regardless of other settings. If ``True``,
|
||||
require_bundling boolean If ``true``, this item is only available as part of bundles.
|
||||
generate_tickets boolean If ``false``, tickets are never generated for this
|
||||
product, regardless of other settings. If ``true``,
|
||||
tickets are generated even if this is a
|
||||
non-admission or add-on product, regardless of event
|
||||
settings. If this is ``null``, regular ticketing
|
||||
@@ -81,7 +81,7 @@ variations list of objects A list with one
|
||||
├ price money (string) The price used for this variation. This is either the
|
||||
same as ``default_price`` if that value is set or equal
|
||||
to the item's ``default_price``.
|
||||
├ active boolean If ``False``, this variation will not be sold or shown.
|
||||
├ active boolean If ``false``, this variation will not be sold or shown.
|
||||
├ description multi-lingual string A public description of the variation. May contain
|
||||
Markdown syntax or can be ``null``.
|
||||
└ position integer An integer, used for sorting
|
||||
|
||||
@@ -26,7 +26,7 @@ status string Order status, o
|
||||
* ``p`` – paid
|
||||
* ``e`` – expired
|
||||
* ``c`` – canceled
|
||||
testmode boolean If ``True``, this order was created when the event was in
|
||||
testmode boolean If ``true``, this order was created when the event was in
|
||||
test mode. Only orders in test mode can be deleted.
|
||||
secret string The secret contained in the link sent to the customer
|
||||
email string The customer email address
|
||||
@@ -39,13 +39,13 @@ payment_date date **DEPRECATED AN
|
||||
payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order
|
||||
total money (string) Total value of this order
|
||||
comment string Internal comment on this order
|
||||
checkin_attention boolean If ``True``, the check-in app should show a warning
|
||||
checkin_attention boolean If ``true``, the check-in app should show a warning
|
||||
that this ticket requires special attention if a ticket
|
||||
of this order is scanned.
|
||||
invoice_address object Invoice address information (can be ``null``)
|
||||
├ last_modified datetime Last modification date of the address
|
||||
├ company string Customer company name
|
||||
├ is_business boolean Business or individual customers (always ``False``
|
||||
├ is_business boolean Business or individual customers (always ``false``
|
||||
for orders created before pretix 1.7, do not rely on
|
||||
it).
|
||||
├ name string Customer name
|
||||
@@ -56,7 +56,7 @@ invoice_address object Invoice address
|
||||
├ country string Customer country
|
||||
├ internal_reference string Customer's internal reference to be printed on the invoice
|
||||
├ vat_id string Customer VAT ID
|
||||
└ vat_id_validated string ``True``, if the VAT ID has been validated against the
|
||||
└ vat_id_validated string ``true``, if the VAT ID has been validated against the
|
||||
EU VAT service and validation was successful. This only
|
||||
happens in rare cases.
|
||||
positions list of objects List of non-canceled order positions (see below)
|
||||
@@ -78,9 +78,9 @@ downloads list of objects List of ticket
|
||||
download options.
|
||||
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
||||
└ url string Download URL
|
||||
require_approval boolean If ``True`` and the order is pending, this order
|
||||
require_approval boolean If ``true`` and the order is pending, this order
|
||||
needs approval by an organizer before it can
|
||||
continue. If ``True`` and the order is canceled,
|
||||
continue. If ``true`` and the order is canceled,
|
||||
this order has been denied by the event organizer.
|
||||
payments list of objects List of payment processes (see below)
|
||||
refunds list of objects List of refund processes (see below)
|
||||
@@ -295,7 +295,7 @@ List of all orders
|
||||
"require_approval": false,
|
||||
"invoice_address": {
|
||||
"last_modified": "2017-12-01T10:00:00Z",
|
||||
"is_business": True,
|
||||
"is_business": true,
|
||||
"company": "Sample company",
|
||||
"name": "John Doe",
|
||||
"name_parts": {"full_name": "John Doe"},
|
||||
@@ -305,7 +305,7 @@ List of all orders
|
||||
"country": "Testikistan",
|
||||
"internal_reference": "",
|
||||
"vat_id": "EU123456789",
|
||||
"vat_id_validated": False
|
||||
"vat_id_validated": false
|
||||
},
|
||||
"positions": [
|
||||
{
|
||||
@@ -437,7 +437,7 @@ Fetching individual orders
|
||||
"invoice_address": {
|
||||
"last_modified": "2017-12-01T10:00:00Z",
|
||||
"company": "Sample company",
|
||||
"is_business": True,
|
||||
"is_business": true,
|
||||
"name": "John Doe",
|
||||
"name_parts": {"full_name": "John Doe"},
|
||||
"street": "Test street 12",
|
||||
@@ -446,7 +446,7 @@ Fetching individual orders
|
||||
"country": "Testikistan",
|
||||
"internal_reference": "",
|
||||
"vat_id": "EU123456789",
|
||||
"vat_id_validated": False
|
||||
"vat_id_validated": false
|
||||
},
|
||||
"positions": [
|
||||
{
|
||||
@@ -593,7 +593,7 @@ Updating order fields
|
||||
"email": "other@example.org",
|
||||
"locale": "de",
|
||||
"comment": "Foo",
|
||||
"checkin_attention": True
|
||||
"checkin_attention": true
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -817,7 +817,7 @@ Creating orders
|
||||
],
|
||||
"payment_provider": "banktransfer",
|
||||
"invoice_address": {
|
||||
"is_business": False,
|
||||
"is_business": false,
|
||||
"company": "Sample company",
|
||||
"name_parts": {"full_name": "John Doe"},
|
||||
"street": "Sesam Street 12",
|
||||
|
||||
@@ -30,12 +30,12 @@ type string The expected ty
|
||||
* ``D`` – date
|
||||
* ``H`` – time
|
||||
* ``W`` – date and time
|
||||
required boolean If ``True``, the question needs to be filled out.
|
||||
required boolean If ``true``, the question needs to be filled out.
|
||||
position integer An integer, used for sorting
|
||||
items list of integers List of item IDs this question is assigned to.
|
||||
identifier string An arbitrary string that can be used for matching with
|
||||
other sources.
|
||||
ask_during_checkin boolean If ``True``, this question will not be asked while
|
||||
ask_during_checkin boolean If ``true``, this question will not be asked while
|
||||
buying the ticket, but will show up when redeeming
|
||||
the ticket instead.
|
||||
options list of objects In case of question type ``C`` or ``M``, this lists the
|
||||
@@ -53,7 +53,7 @@ dependency_question integer Internal ID of
|
||||
``ask_during_checkin``.
|
||||
dependency_value string The value ``dependency_question`` needs to be set to.
|
||||
If ``dependency_question`` is set to a boolean
|
||||
question, this should be ``"True"`` or ``"False"``.
|
||||
question, this should be ``"true"`` or ``"false"``.
|
||||
Otherwise, it should be the ``identifier`` of a
|
||||
question option.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
@@ -18,8 +18,8 @@ max_usages integer The maximum num
|
||||
redeemed integer The number of times this voucher already has been
|
||||
redeemed.
|
||||
valid_until datetime The voucher expiration date (or ``null``).
|
||||
block_quota boolean If ``True``, quota is blocked for this voucher.
|
||||
allow_ignore_quota boolean If ``True``, this voucher can be redeemed even if a
|
||||
block_quota boolean If ``true``, quota is blocked for this voucher.
|
||||
allow_ignore_quota boolean If ``true``, this voucher can be redeemed even if a
|
||||
product is sold out and even if quota is not blocked
|
||||
for this voucher.
|
||||
price_mode string Determines how this voucher affects product prices.
|
||||
|
||||
@@ -17,11 +17,11 @@ The webhook resource contains the following public fields:
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the webhook
|
||||
enabled boolean If ``False``, this webhook will not receive any notifications
|
||||
enabled boolean If ``false``, this webhook will not receive any notifications
|
||||
target_url string The URL to call
|
||||
all_events boolean If ``True``, this webhook will receive notifications
|
||||
all_events boolean If ``true``, this webhook will receive notifications
|
||||
on all events of this organizer
|
||||
limit_events list of strings If ``all_events`` is ``False``, this is a list of
|
||||
limit_events list of strings If ``all_events`` is ``false``, this is a list of
|
||||
event slugs this webhook is active for
|
||||
action_types list of strings A list of action type filters that limit the
|
||||
notifications sent to this webhook. See below for
|
||||
|
||||
@@ -26,7 +26,7 @@ Frontend
|
||||
--------
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional
|
||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble
|
||||
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
|
||||
@@ -316,7 +316,7 @@ uses to communicate with the pretix server.
|
||||
"total": 42,
|
||||
"version": 3,
|
||||
"event": {
|
||||
"name": "Demo Converence",
|
||||
"name": "Demo Conference",
|
||||
"slug": "democon",
|
||||
"date_from": "2016-12-27T17:00:00Z",
|
||||
"date_to": "2016-12-30T18:00:00Z",
|
||||
|
||||
@@ -95,6 +95,7 @@ renderers
|
||||
reportlab
|
||||
SaaS
|
||||
screenshot
|
||||
scss
|
||||
searchable
|
||||
selectable
|
||||
serializers
|
||||
@@ -110,6 +111,7 @@ subdomains
|
||||
subevent
|
||||
subevents
|
||||
submodule
|
||||
subnet
|
||||
subpath
|
||||
Symfony
|
||||
systemd
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from .answers import * # noqa
|
||||
from .dekodi import * # noqa
|
||||
from .invoices import * # noqa
|
||||
from .json import * # noqa
|
||||
from .mail import * # noqa
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
import json
|
||||
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.dispatch import receiver
|
||||
|
||||
from pretix.base.models import Invoice, OrderPayment
|
||||
|
||||
from ..exporter import BaseExporter
|
||||
from ..signals import register_data_exporters
|
||||
|
||||
|
||||
class DekodiNREIExporter(BaseExporter):
|
||||
identifier = 'dekodi_nrei'
|
||||
verbose_name = 'dekodi NREI (JSON)'
|
||||
# Specification: http://manuals.dekodi.de/nexuspub/schnittstellenbuch/
|
||||
|
||||
def _encode_invoice(self, invoice: Invoice):
|
||||
hdr = {
|
||||
'APmtA': None,
|
||||
'APmtAEUR': None,
|
||||
'C': str(invoice.invoice_to_country),
|
||||
'CA': None,
|
||||
'CAEUR': None,
|
||||
'CC': self.event.currency,
|
||||
'CGrp': None,
|
||||
'CID': None,
|
||||
'City': invoice.invoice_to_city,
|
||||
'CN': invoice.invoice_to_company,
|
||||
'CNo': None,
|
||||
'CoDA': None,
|
||||
'CoDAEUR': None,
|
||||
'DL': None,
|
||||
'DIC': None, # Event-land?
|
||||
'DICt': None,
|
||||
'DIDt': invoice.order.datetime.isoformat().replace('Z', '+00:00'),
|
||||
'DIZ': None,
|
||||
'DN': None,
|
||||
'DT': '30' if invoice.is_cancellation else '10',
|
||||
'EbNm': None,
|
||||
'EbPmtID': Nine,
|
||||
'EM': invoice.order.email,
|
||||
'FamN': invoice.invoice_to_name, # todo: split? should be last name
|
||||
'FCExR': None,
|
||||
'FN': invoice.invoice_to_name, # todo: split? should be first name
|
||||
'FS': None,
|
||||
'GwA': None,
|
||||
'GwAEUR': None,
|
||||
'IDt': invoice.date.isoformat() + 'T08:00:00+01:00',
|
||||
'INo': invoice.full_invoice_no,
|
||||
'IsNet': invoice.reverse_charge,
|
||||
'IsPf': False,
|
||||
'IT': None,
|
||||
'KlnId': None,
|
||||
'ODt': invoice.order.datetime.isoformat().replace('Z', '+00:00'),
|
||||
'OID': invoice.order.code,
|
||||
'Pb': None,
|
||||
'PL': None,
|
||||
'PmDt': None, # todo: payment date?
|
||||
'PPEm': None, # todo: fill,
|
||||
'PvrINo': invoice.refers.full_invoice_no if invoice.refers else None,
|
||||
'PrvOID': None,
|
||||
'Rmrks': None,
|
||||
'ShA': None,
|
||||
'ShAEUR': None,
|
||||
'ShDt': None,
|
||||
'ShGrp': None,
|
||||
'SID': self.event.slug,
|
||||
'SN': str(self.event),
|
||||
'SSID': None,
|
||||
'SSINo': None,
|
||||
'SSN': None,
|
||||
'SSOID': None,
|
||||
'Str': invoice.invoice_to_street,
|
||||
'TGrossA': gross_total, # TODO,
|
||||
'TGrossAEUR': None,
|
||||
'TNetA': net_total, # TODO,
|
||||
'TNetAEUR': None,
|
||||
'TNo': None,
|
||||
'TT': None,
|
||||
'TVatA': vat_total, # todo
|
||||
'VatDp': False,
|
||||
'VatID': invoice.invoice_to_vat_id or None,
|
||||
'Zip': invoice.invoice_to_zipcode
|
||||
|
||||
}
|
||||
positions = []
|
||||
for l in invoice.lines.all():
|
||||
positions.append({
|
||||
'ABcd': None,
|
||||
'ADes': l.description,
|
||||
'ANetA': round(float(l.net_value), 2),
|
||||
'ANetAEUR': None,
|
||||
'ANo': None, # TODO: needs to be there!
|
||||
'ANo1': None,
|
||||
'ANo2': None,
|
||||
'ANoEx': None,
|
||||
'ANoM': None,
|
||||
'AQ': -1 if invoice.is_cancellation else 1,
|
||||
'ASku': None,
|
||||
'AST': 0,
|
||||
'ATm': None,
|
||||
'ATT': None,
|
||||
'AU': None,
|
||||
'AVatP': round(float(l.tax_rate), 2),
|
||||
'AWgt': None,
|
||||
'DiC': None,
|
||||
'DiCeID': None,
|
||||
'DICeN': None,
|
||||
'DiZ': None,
|
||||
'DIDt': (l.subevent or invoice.order.event).date_from.isoformat().replace('Z', '+00:00'),
|
||||
'OC': None,
|
||||
'PosGrossA': round(float((-1 if invoice.is_cancellation else 1) * l.gross_value), 2),
|
||||
'PosGrossAEUR': None,
|
||||
'PosNetA': round(float((-1 if invoice.is_cancellation else 1) * l.net_value), 2),
|
||||
'PosNetAEUR': None,
|
||||
})
|
||||
payments = []
|
||||
for p in invoice.order.payments.filter(
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_PENDING,
|
||||
OrderPayment.PAYMENT_STATE_STARTED, OrderPayment.PAYMENT_STATE_REFUNDED)
|
||||
):
|
||||
if p.provider == 'paypal':
|
||||
payments.append({
|
||||
'PTID': '1',
|
||||
'PTN': 'PayPal',
|
||||
'PTNo1': None, # TODO: transaktionsid
|
||||
'PTNo2': None,
|
||||
'PTNo3': None,
|
||||
'PTNo4': None,
|
||||
'PTNo5': None,
|
||||
'PTNo6': None,
|
||||
'PTNo7': None,
|
||||
'PTNo8': None,
|
||||
'PTNo9': None,
|
||||
'PTNo10': None,
|
||||
'PTNo11': None,
|
||||
'PTNo12': None,
|
||||
'PTNo13': None,
|
||||
'PTNo14': None,
|
||||
'PTNo15': None,
|
||||
})
|
||||
elif p.provider == 'banktransfer':
|
||||
payments.append({
|
||||
'PTID': '4',
|
||||
'PTN': 'Vorkasse',
|
||||
'PTNo1': None, # TODO: transaktionsid
|
||||
'PTNo2': None,
|
||||
'PTNo3': None,
|
||||
'PTNo4': None,
|
||||
'PTNo5': None,
|
||||
'PTNo6': None,
|
||||
'PTNo7': None,
|
||||
'PTNo8': None,
|
||||
'PTNo9': None,
|
||||
'PTNo10': None,
|
||||
'PTNo11': None,
|
||||
'PTNo12': None,
|
||||
'PTNo13': None,
|
||||
'PTNo14': None,
|
||||
'PTNo15': None,
|
||||
})
|
||||
elif p.provider == 'sepadebit':
|
||||
payments.append({
|
||||
'PTID': '5',
|
||||
'PTN': 'Lastschrift',
|
||||
'PTNo1': None, # TODO: transaktionsid
|
||||
'PTNo2': None,
|
||||
'PTNo3': None,
|
||||
'PTNo4': None,
|
||||
'PTNo5': None,
|
||||
'PTNo6': None,
|
||||
'PTNo7': None,
|
||||
'PTNo8': None,
|
||||
'PTNo9': None,
|
||||
'PTNo10': None,
|
||||
'PTNo11': None,
|
||||
'PTNo12': None,
|
||||
'PTNo13': None,
|
||||
'PTNo14': None,
|
||||
'PTNo15': None,
|
||||
})
|
||||
elif p.provider.startswith('stripe'):
|
||||
payments.append({
|
||||
'PTID': '81',
|
||||
'PTN': 'Stripe',
|
||||
'PTNo1': None, # TODO: transaktionsid
|
||||
'PTNo2': None,
|
||||
'PTNo3': None,
|
||||
'PTNo4': None,
|
||||
'PTNo5': None,
|
||||
'PTNo6': None,
|
||||
'PTNo7': None,
|
||||
'PTNo8': None,
|
||||
'PTNo9': None,
|
||||
'PTNo10': None,
|
||||
'PTNo11': None,
|
||||
'PTNo12': None,
|
||||
'PTNo13': None,
|
||||
'PTNo14': None,
|
||||
'PTNo15': None,
|
||||
})
|
||||
return {
|
||||
'IsValid': True,
|
||||
'Hdr': hdr,
|
||||
'InvcPstns': positions,
|
||||
'PmIs': payments,
|
||||
'ValidationMessage': ''
|
||||
}
|
||||
|
||||
def render(self, form_data):
|
||||
jo = {
|
||||
'Format': 'NREI',
|
||||
'Version': '18.10.2',
|
||||
'SourceSystem': 'pretix',
|
||||
'Data': [
|
||||
self._encode_invoice(i) for i in self.event.invoices.select_related('order').prefetch_related('lines', 'lines__subevent')
|
||||
]
|
||||
}
|
||||
return '{}_nrei.json'.format(self.event.slug), 'application/json', json.dumps(jo, cls=DjangoJSONEncoder)
|
||||
|
||||
|
||||
@receiver(register_data_exporters, dispatch_uid="exporter_dekodi_nrei")
|
||||
def register_dekodi_export(sender, **kwargs):
|
||||
return DekodiNREIExporter
|
||||
@@ -173,6 +173,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
initial = None
|
||||
tz = pytz.timezone(event.settings.timezone)
|
||||
help_text = rich_text(q.help_text)
|
||||
default = q.default_value
|
||||
label = escape(q.question) # django-bootstrap3 calls mark_safe
|
||||
required = q.required and not self.all_optional
|
||||
if q.type == Question.TYPE_BOOLEAN:
|
||||
@@ -185,6 +186,8 @@ class BaseQuestionsForm(forms.Form):
|
||||
|
||||
if initial:
|
||||
initialbool = (initial.answer == "True")
|
||||
elif default:
|
||||
initialbool = (default == "True")
|
||||
else:
|
||||
initialbool = False
|
||||
|
||||
@@ -197,21 +200,21 @@ class BaseQuestionsForm(forms.Form):
|
||||
field = forms.DecimalField(
|
||||
label=label, required=required,
|
||||
help_text=q.help_text,
|
||||
initial=initial.answer if initial else None,
|
||||
initial=initial.answer if initial else (default if default else None),
|
||||
min_value=Decimal('0.00'),
|
||||
)
|
||||
elif q.type == Question.TYPE_STRING:
|
||||
field = forms.CharField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=initial.answer if initial else None,
|
||||
initial=initial.answer if initial else (default if default else None),
|
||||
)
|
||||
elif q.type == Question.TYPE_TEXT:
|
||||
field = forms.CharField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
widget=forms.Textarea,
|
||||
initial=initial.answer if initial else None,
|
||||
initial=initial.answer if initial else (default if default else None),
|
||||
)
|
||||
elif q.type == Question.TYPE_CHOICE:
|
||||
field = forms.ModelChoiceField(
|
||||
@@ -243,21 +246,24 @@ class BaseQuestionsForm(forms.Form):
|
||||
field = forms.DateField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=dateutil.parser.parse(initial.answer).date() if initial and initial.answer else None,
|
||||
initial=dateutil.parser.parse(initial.answer).date() if initial and initial.answer else (
|
||||
dateutil.parser.parse(default).date() if default else None),
|
||||
widget=DatePickerWidget(),
|
||||
)
|
||||
elif q.type == Question.TYPE_TIME:
|
||||
field = forms.TimeField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=dateutil.parser.parse(initial.answer).time() if initial and initial.answer else None,
|
||||
initial=dateutil.parser.parse(initial.answer).time() if initial and initial.answer else (
|
||||
dateutil.parser.parse(default).time() if default else None),
|
||||
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
|
||||
)
|
||||
elif q.type == Question.TYPE_DATETIME:
|
||||
field = SplitDateTimeField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=dateutil.parser.parse(initial.answer).astimezone(tz) if initial and initial.answer else None,
|
||||
initial=dateutil.parser.parse(initial.answer).astimezone(tz) if initial and initial.answer else (
|
||||
dateutil.parser.parse(default).astimezone(tz) if default else None),
|
||||
widget=SplitDateTimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
|
||||
)
|
||||
field.question = q
|
||||
|
||||
24
src/pretix/base/migrations/0115_auto_20190323_2238.py
Normal file
24
src/pretix/base/migrations/0115_auto_20190323_2238.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 2.1.7 on 2019-03-23 22:38
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
import django.db.models.deletion
|
||||
import jsonfallback.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0114_auto_20190316_1014'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='itembundle',
|
||||
name='designated_price',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, default=Decimal('0.00'), 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 value will NOT be added to the base item's price.", max_digits=10, verbose_name='Designated price part'),
|
||||
),
|
||||
]
|
||||
20
src/pretix/base/migrations/0116_auto_20180531_1739.py
Normal file
20
src/pretix/base/migrations/0116_auto_20180531_1739.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.13 on 2018-05-31 15:39
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0115_auto_20190323_2238'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='question',
|
||||
name='default_value',
|
||||
field=models.TextField(blank=True, help_text='The question will be filled with this response by default', null=True, verbose_name='Default answer'),
|
||||
),
|
||||
]
|
||||
@@ -818,7 +818,7 @@ class ItemBundle(models.Model):
|
||||
verbose_name=_('Number')
|
||||
)
|
||||
designated_price = models.DecimalField(
|
||||
null=True, blank=True,
|
||||
default=Decimal('0.00'), blank=True,
|
||||
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 '
|
||||
@@ -963,6 +963,11 @@ class Question(LoggedModel):
|
||||
'Question', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
|
||||
)
|
||||
dependency_value = models.TextField(null=True, blank=True)
|
||||
default_value = models.TextField(
|
||||
verbose_name=_('Default answer'),
|
||||
help_text=_('The question will be filled with this response by default'),
|
||||
null=True, blank=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Question")
|
||||
@@ -980,6 +985,10 @@ class Question(LoggedModel):
|
||||
def clean_identifier(self, code):
|
||||
Question._clean_identifier(self.event, code, self)
|
||||
|
||||
def clean(self):
|
||||
if self.default_value is not None:
|
||||
self.clean_answer(self.default_value, check_required=False)
|
||||
|
||||
@staticmethod
|
||||
def _clean_identifier(event, code, instance=None):
|
||||
qs = Question.objects.filter(event=event, identifier__iexact=code)
|
||||
@@ -1007,8 +1016,8 @@ class Question(LoggedModel):
|
||||
def __lt__(self, other) -> bool:
|
||||
return self.sortkey < other.sortkey
|
||||
|
||||
def clean_answer(self, answer):
|
||||
if self.required:
|
||||
def clean_answer(self, answer, check_required=True):
|
||||
if self.required and check_required:
|
||||
if not answer or (self.type == Question.TYPE_BOOLEAN and answer not in ("true", "True", True)):
|
||||
raise ValidationError(_('An answer to this question is required to proceed.'))
|
||||
if not answer:
|
||||
|
||||
@@ -1470,7 +1470,7 @@ def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_tok
|
||||
raise OrderError(error_messages['busy'])
|
||||
|
||||
|
||||
def change_payment_provider(order: Order, payment_provider, amount=None):
|
||||
def change_payment_provider(order: Order, payment_provider, amount=None, new_payment=None):
|
||||
e = OrderPayment.objects.filter(fee=OuterRef('pk'), state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||||
OrderPayment.PAYMENT_STATE_REFUNDED))
|
||||
open_fees = list(
|
||||
@@ -1502,7 +1502,10 @@ def change_payment_provider(order: Order, payment_provider, amount=None):
|
||||
fee = None
|
||||
|
||||
open_payment = None
|
||||
lp = order.payments.exclude(provider=payment_provider.identifier).last()
|
||||
if new_payment:
|
||||
lp = order.payments.exclude(pk=new_payment.pk).last()
|
||||
else:
|
||||
lp = order.payments.last()
|
||||
if lp and lp.state not in (OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED):
|
||||
open_payment = lp
|
||||
|
||||
|
||||
@@ -31,6 +31,6 @@ def refresh_quota_caches():
|
||||
Q(cached_availability_time__isnull=True) |
|
||||
Q(cached_availability_time__lt=F('last_activity')) |
|
||||
Q(cached_availability_time__lt=now() - timedelta(hours=2), last_activity__gt=now() - timedelta(days=7))
|
||||
)
|
||||
).select_related('subevent')
|
||||
for q in quotas:
|
||||
q.availability()
|
||||
|
||||
@@ -97,6 +97,10 @@ DEFAULTS = {
|
||||
'default': '30',
|
||||
'type': int
|
||||
},
|
||||
'redirect_to_checkout_directly': {
|
||||
'default': 'False',
|
||||
'type': bool
|
||||
},
|
||||
'payment_explanation': {
|
||||
'default': '',
|
||||
'type': LazyI18nString
|
||||
|
||||
@@ -1074,6 +1074,10 @@ class DisplaySettingsForm(SettingsForm):
|
||||
label=_('Ask search engines not to index the ticket shop'),
|
||||
required=False
|
||||
)
|
||||
redirect_to_checkout_directly = forms.BooleanField(
|
||||
label=_('Directly redirect to check-out after a product has been added to the cart.'),
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
event = kwargs['obj']
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import datetime
|
||||
|
||||
import dateutil
|
||||
import pytz
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Max
|
||||
from django.forms.formsets import DELETION_FIELD_NAME
|
||||
from django.forms.utils import from_current_timezone, to_current_timezone
|
||||
from django.urls import reverse
|
||||
from django.utils import formats
|
||||
from django.utils.translation import (
|
||||
pgettext_lazy, ugettext as __, ugettext_lazy as _,
|
||||
)
|
||||
@@ -33,6 +39,63 @@ class CategoryForm(I18nModelForm):
|
||||
]
|
||||
|
||||
|
||||
class AdjustableTypeField(forms.Textarea):
|
||||
def __init__(self, *args, type=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def format_value(self, value):
|
||||
if getattr(self, 'type') == 'W' and value:
|
||||
return str(formats.localize_input(to_current_timezone(dateutil.parser.parse(value))))
|
||||
elif getattr(self, 'type') == 'D' and value:
|
||||
return str(formats.localize_input(dateutil.parser.parse(value).date()))
|
||||
elif getattr(self, 'type') == 'T' and value:
|
||||
return str(formats.localize_input(dateutil.parser.parse(value).time()))
|
||||
return super().format_value(value)
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
if 'type' in data and data['type'] == 'W' and 'default_value_date' in data and 'default_value_time' in data:
|
||||
d_date = d_time = None
|
||||
|
||||
for format in formats.get_format('DATE_INPUT_FORMATS'):
|
||||
try:
|
||||
d_date = datetime.datetime.strptime(data['default_value_date'], format).date()
|
||||
break
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
for format in formats.get_format('TIME_INPUT_FORMATS'):
|
||||
try:
|
||||
d_time = datetime.datetime.strptime(data['default_value_time'], format).time()
|
||||
break
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
if d_date and d_time:
|
||||
return from_current_timezone(datetime.datetime.combine(d_date, d_time)).astimezone(pytz.UTC).isoformat()
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
val = super().value_from_datadict(data, files, name)
|
||||
if 'type' in data and data['type'] == 'D' and val:
|
||||
for format in formats.get_format('DATE_INPUT_FORMATS'):
|
||||
try:
|
||||
d_date = datetime.datetime.strptime(val, format).date()
|
||||
val = d_date.isoformat()
|
||||
break
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
elif 'type' in data and data['type'] == 'T' and val:
|
||||
for format in formats.get_format('TIME_INPUT_FORMATS'):
|
||||
try:
|
||||
d_date = datetime.datetime.strptime(val, format).time()
|
||||
val = d_date.isoformat()
|
||||
break
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
return val
|
||||
|
||||
|
||||
class QuestionForm(I18nModelForm):
|
||||
question = I18nFormField(
|
||||
label=_("Question"),
|
||||
@@ -51,6 +114,8 @@ class QuestionForm(I18nModelForm):
|
||||
pk=self.instance.pk
|
||||
)
|
||||
self.fields['identifier'].required = False
|
||||
if self.instance:
|
||||
self.fields['default_value'].widget.type = self.instance.type
|
||||
self.fields['help_text'].widget.attrs['rows'] = 3
|
||||
|
||||
def clean_dependency_question(self):
|
||||
@@ -80,6 +145,7 @@ class QuestionForm(I18nModelForm):
|
||||
'help_text',
|
||||
'type',
|
||||
'required',
|
||||
'default_value',
|
||||
'ask_during_checkin',
|
||||
'identifier',
|
||||
'items',
|
||||
@@ -90,7 +156,8 @@ class QuestionForm(I18nModelForm):
|
||||
'items': forms.CheckboxSelectMultiple(
|
||||
attrs={'class': 'scrolling-multiple-choice'}
|
||||
),
|
||||
'dependency_value': forms.Select,
|
||||
'default_value': AdjustableTypeField(),
|
||||
'dependency_value': forms.Select
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
{% if form.frontpage_subevent_ordering %}
|
||||
{% bootstrap_field form.frontpage_subevent_ordering layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field form.redirect_to_checkout_directly layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Shop design" %}</legend>
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "Advanced settings" %}</legend>
|
||||
{% bootstrap_field form.help_text layout="control" %}
|
||||
{% bootstrap_field form.default_value layout="control" %}
|
||||
{% bootstrap_field form.identifier layout="control" %}
|
||||
{% bootstrap_field form.ask_during_checkin layout="control" %}
|
||||
|
||||
|
||||
@@ -82,7 +82,10 @@ class OrderSearch(PaginationMixin, ListView):
|
||||
|
||||
page = self.kwargs.get(self.page_kwarg) or self.request.GET.get(self.page_kwarg) or 1
|
||||
limit = self.get_paginate_by(None)
|
||||
offset = (page - 1) * limit
|
||||
try:
|
||||
offset = (int(page) - 1) * limit
|
||||
except ValueError:
|
||||
offset = 0
|
||||
resultids = list(qs.order_by().values_list('id', flat=True)[:201])
|
||||
if len(resultids) <= 200 and len(resultids) <= offset + limit:
|
||||
qs = Order.objects.using(settings.DATABASE_REPLICA).filter(
|
||||
|
||||
@@ -92,14 +92,23 @@ def _handle_transaction(trans: BankTransaction, code: str, event: Event=None, or
|
||||
trans.state = BankTransaction.STATE_ERROR
|
||||
trans.message = ugettext_noop('The order has already been canceled.')
|
||||
else:
|
||||
p, created = trans.order.payments.get_or_create(
|
||||
amount=trans.amount,
|
||||
provider='banktransfer',
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING),
|
||||
defaults={
|
||||
'state': OrderPayment.PAYMENT_STATE_CREATED,
|
||||
}
|
||||
)
|
||||
try:
|
||||
p, created = trans.order.payments.get_or_create(
|
||||
amount=trans.amount,
|
||||
provider='banktransfer',
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING),
|
||||
defaults={
|
||||
'state': OrderPayment.PAYMENT_STATE_CREATED,
|
||||
}
|
||||
)
|
||||
except OrderPayment.MultipleObjectsReturned:
|
||||
created = False
|
||||
p = trans.order.payments.filter(
|
||||
amount=trans.amount,
|
||||
provider='banktransfer',
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING),
|
||||
).last()
|
||||
|
||||
p.info_data = {
|
||||
'reference': trans.reference,
|
||||
'date': trans.date,
|
||||
@@ -109,7 +118,7 @@ def _handle_transaction(trans: BankTransaction, code: str, event: Event=None, or
|
||||
|
||||
if created:
|
||||
# We're perform a payment method switchign on-demand here
|
||||
old_fee, new_fee, fee = change_payment_provider(trans.order, p.payment_provider, p.amount) # noqa
|
||||
old_fee, new_fee, fee = change_payment_provider(trans.order, p.payment_provider, p.amount, new_payment=p) # noqa
|
||||
if fee:
|
||||
p.fee = fee
|
||||
p.save(update_fields=['fee'])
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
{% if job.state == "error" %}
|
||||
<div class="alert alert-danger">
|
||||
{% trans "An internal error occurred during processing your data." %}
|
||||
{% trans "Some transactions might be missing, please try ot re-import the file." %}
|
||||
</div>
|
||||
{% elif transactions_ignored == 0 and transactions_invalid == 0 and transactions_valid == 0 %}
|
||||
<div class="alert alert-info">
|
||||
|
||||
@@ -8,7 +8,7 @@ from pretix.helpers.i18n import (
|
||||
get_javascript_format_without_seconds, get_moment_locale,
|
||||
)
|
||||
|
||||
from .signals import footer_link, html_footer, html_head
|
||||
from .signals import footer_link, html_footer, html_head, html_page_header
|
||||
|
||||
|
||||
def contextprocessor(request):
|
||||
@@ -23,6 +23,7 @@ def contextprocessor(request):
|
||||
'DEBUG': settings.DEBUG,
|
||||
}
|
||||
_html_head = []
|
||||
_html_page_header = []
|
||||
_html_foot = []
|
||||
_footer = []
|
||||
|
||||
@@ -45,6 +46,8 @@ def contextprocessor(request):
|
||||
if hasattr(request, 'event'):
|
||||
for receiver, response in html_head.send(request.event, request=request):
|
||||
_html_head.append(response)
|
||||
for receiver, response in html_page_header.send(request.event, request=request):
|
||||
_html_page_header.append(response)
|
||||
for receiver, response in html_footer.send(request.event, request=request):
|
||||
_html_foot.append(response)
|
||||
for receiver, response in footer_link.send(request.event, request=request):
|
||||
@@ -71,6 +74,7 @@ def contextprocessor(request):
|
||||
|
||||
ctx['html_head'] = "".join(_html_head)
|
||||
ctx['html_foot'] = "".join(_html_foot)
|
||||
ctx['html_page_header'] = "".join(_html_page_header)
|
||||
ctx['footer'] = _footer
|
||||
ctx['site_url'] = settings.SITE_URL
|
||||
|
||||
|
||||
@@ -11,6 +11,17 @@ of every page in the frontend. You will get the request as the keyword argument
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
html_page_header = EventPluginSignal(
|
||||
providing_args=["request"]
|
||||
)
|
||||
"""
|
||||
This signal allows you to put code right in the beginning of the HTML ``<body>`` tag
|
||||
of every page in the frontend. You will get the request as the keyword argument
|
||||
``request`` and are expected to return plain HTML.
|
||||
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
html_footer = EventPluginSignal(
|
||||
providing_args=["request"]
|
||||
)
|
||||
@@ -22,6 +33,33 @@ of every page in the frontend. You will get the request as the keyword argument
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
sass_preamble = EventPluginSignal(
|
||||
providing_args=["filename"]
|
||||
)
|
||||
"""
|
||||
This signal allows you to put SASS code at the beginning of the event-specific
|
||||
stylesheet. Keep in mind that this will only be called/rebuilt when the user changes
|
||||
display settings or pretix gets updated. You will get the filename that is being
|
||||
generated (usually "main.scss" or "widget.scss"). This SASS code will be loaded *after*
|
||||
setting of user-defined variables like colors and fonts but *before* pretix' SASS
|
||||
code.
|
||||
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
sass_postamble = EventPluginSignal(
|
||||
providing_args=["filename"]
|
||||
)
|
||||
"""
|
||||
This signal allows you to put SASS code at the end of the event-specific
|
||||
stylesheet. Keep in mind that this will only be called/rebuilt when the user changes
|
||||
display settings or pretix gets updated. You will get the filename that is being
|
||||
generated (usually "main.scss" or "widget.scss"). This SASS code will be loaded *after*
|
||||
all of pretix' SASS code.
|
||||
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
footer_link = EventPluginSignal(
|
||||
providing_args=["request"]
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@ from pretix.base.models import Event, Event_SettingsStore, Organizer
|
||||
from pretix.base.services.tasks import ProfiledTask
|
||||
from pretix.celery_app import app
|
||||
from pretix.multidomain.urlreverse import get_domain
|
||||
from pretix.presale.signals import sass_postamble, sass_preamble
|
||||
|
||||
logger = logging.getLogger('pretix.presale.style')
|
||||
affected_keys = ['primary_font', 'primary_color']
|
||||
@@ -54,8 +55,16 @@ def compile_scss(object, file="main.scss", fonts=True):
|
||||
font
|
||||
))
|
||||
|
||||
if isinstance(object, Event):
|
||||
for recv, resp in sass_preamble.send(object, filename=file):
|
||||
sassrules.append(resp)
|
||||
|
||||
sassrules.append('@import "{}";'.format(file))
|
||||
|
||||
if isinstance(object, Event):
|
||||
for recv, resp in sass_postamble.send(object, filename=file):
|
||||
sassrules.append(resp)
|
||||
|
||||
cf = dict(django_libsass.CUSTOM_FUNCTIONS)
|
||||
cf['static'] = static
|
||||
css = sass.compile(
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
<meta name="theme-color" content="{{ settings.primary_color|default:"#3b1c4a" }}">
|
||||
</head>
|
||||
<body class="nojs" data-locale="{{ request.LANGUAGE_CODE }}" data-now="{% now "U.u" %}" data-datetimeformat="{{ js_datetime_format }}" data-timeformat="{{ js_time_format }}" data-dateformat="{{ js_date_format }}" data-datetimelocale="{{ js_locale }}">
|
||||
{{ html_page_header|safe }}
|
||||
{% block above %}
|
||||
{% endblock %}
|
||||
<div class="container">
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
</div>
|
||||
<div class="col-sm-4 hidden-xs text-right">
|
||||
<a href="?{% url_replace request "year" after.year "month" after.month %}" class="btn btn-default">
|
||||
<span class="fa fa-arrow-right"></span>
|
||||
{{ after|date:"F Y" }}
|
||||
<span class="fa fa-arrow-right"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
<form method="post" data-asynctask
|
||||
data-asynctask-headline="{% trans "We're now trying to reserve this for you!" %}"
|
||||
data-asynctask-text="{% blocktrans with time=event.settings.reservation_time %}Once the items are in your cart, you will have {{ time }} minutes to complete your purchase.{% endblocktrans %}"
|
||||
action="{% eventurl request.event "presale:event.cart.add" cart_namespace=cart_namespace %}?next={{ request.path|urlencode }}">
|
||||
action="{% eventurl request.event "presale:event.cart.add" cart_namespace=cart_namespace %}?next={{ cart_redirect|urlencode }}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="subevent" value="{{ subevent.id|default_if_none:"" }}" />
|
||||
{% for tup in items_by_category %}
|
||||
|
||||
@@ -20,8 +20,7 @@
|
||||
</p>
|
||||
{% if event.presale_is_running or event.settings.show_items_outside_presale_period %}
|
||||
<form method="post"
|
||||
action="{% eventurl request.event "presale:event.cart.add" cart_namespace=cart_namespace %}?next={% eventurl request.event "presale:event.index" cart_namespace=cart_namespace %}{% if "iframe" in request.GET and not new_tab %}&iframe={{ request.GET.iframe }}{% endif %}{% if "take_cart_id" in request.GET and new_tab %}&take_cart_id={{ request.GET.take_cart_id }}{% endif %}"
|
||||
{% if new_tab %}target="_blank"{% else %}
|
||||
action="{% eventurl request.event "presale:event.cart.add" cart_namespace=cart_namespace %}?next={{ cart_redirect|urlencode }}{% if "iframe" in request.GET and not new_tab %}&iframe={{ request.GET.iframe }}{% endif %}{% if "take_cart_id" in request.GET and new_tab %}&take_cart_id={{ request.GET.take_cart_id }}{% endif %}" {% if new_tab %}target="_blank"{% else %}
|
||||
data-asynctask
|
||||
data-asynctask-headline="{% trans "We're now trying to reserve this for you!" %}"
|
||||
data-asynctask-text="{% blocktrans with time=event.settings.reservation_time %}Once the items are in your cart, you will have {{ time }} minutes to complete your purchase.{% endblocktrans %}"
|
||||
|
||||
@@ -425,6 +425,13 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, TemplateView):
|
||||
# Cookies are not supported! Lets just make the form open in a new tab
|
||||
)
|
||||
|
||||
if self.request.event.settings.redirect_to_checkout_directly:
|
||||
context['cart_redirect'] = eventreverse(self.request.event, 'presale:event.checkout.start')
|
||||
else:
|
||||
context['cart_redirect'] = eventreverse(self.request.event, 'presale:event.index')
|
||||
if context['cart_redirect'].startswith('https:'):
|
||||
context['cart_redirect'] = '/' + context['cart_redirect'].split('/', 3)[3]
|
||||
|
||||
return context
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
|
||||
@@ -23,7 +23,8 @@ from pretix.base.models.items import ItemBundle
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.ical import get_ical
|
||||
from pretix.presale.views.organizer import (
|
||||
EventListMixin, add_subevents_for_days, weeks_for_template,
|
||||
EventListMixin, add_subevents_for_days, filter_qs_by_attr,
|
||||
weeks_for_template,
|
||||
)
|
||||
|
||||
from . import (
|
||||
@@ -266,7 +267,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
|
||||
|
||||
ebd = defaultdict(list)
|
||||
add_subevents_for_days(
|
||||
self.request.event.subevents_annotated(self.request.sales_channel),
|
||||
filter_qs_by_attr(self.request.event.subevents_annotated(self.request.sales_channel), self.request),
|
||||
before, after, ebd, set(), self.request.event,
|
||||
kwargs.get('cart_namespace')
|
||||
)
|
||||
@@ -290,6 +291,12 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
|
||||
or not self.subevent
|
||||
)
|
||||
)
|
||||
if self.request.event.settings.redirect_to_checkout_directly:
|
||||
context['cart_redirect'] = eventreverse(self.request.event, 'presale:event.checkout.start')
|
||||
if context['cart_redirect'].startswith('https:'):
|
||||
context['cart_redirect'] = '/' + context['cart_redirect'].split('/', 3)[3]
|
||||
else:
|
||||
context['cart_redirect'] = self.request.path
|
||||
|
||||
return context
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ $gray-lighter: lighten(#000, 93.5%);
|
||||
$gray-lightest: lighten(#000, 97.25%);
|
||||
|
||||
$font-family-sans-serif: "Open Sans", "OpenSans", "Helvetica Neue", Helvetica, Arial, sans-serif !default;
|
||||
$text-color: #222222;
|
||||
$text-muted: #999999;
|
||||
$text-color: #222222 !default;
|
||||
$text-muted: #999999 !default;
|
||||
|
||||
$brand-primary: #7f5a91 !default;
|
||||
$brand-success: #50a167 !default;
|
||||
@@ -40,13 +40,13 @@ $navbar-inverse-link-hover-color: $gray-lighter;
|
||||
$navbar-inverse-brand-hover-color: $gray-lighter;
|
||||
$navbar-inverse-color: white;
|
||||
|
||||
$state-success-bg: white;
|
||||
$state-success-bg: white !default;
|
||||
$state-success-border: $brand-success;
|
||||
$state-info-bg: white;
|
||||
$state-info-bg: white !default;
|
||||
$state-info-border: $brand-info;
|
||||
$state-warning-bg: white;
|
||||
$state-warning-bg: white !default;
|
||||
$state-warning-border: $brand-warning;
|
||||
$state-danger-bg: white;
|
||||
$state-danger-bg: white !default;
|
||||
$state-danger-border: $brand-danger;
|
||||
$panel-success-border: tint($brand-success, 50%);
|
||||
$panel-success-heading-bg: tint($brand-success, 50%);
|
||||
@@ -54,5 +54,5 @@ $panel-danger-border: tint($brand-danger, 50%);
|
||||
$panel-danger-heading-bg: tint($brand-danger, 50%);
|
||||
$panel-warning-border: tint($brand-warning, 50%);
|
||||
$panel-warning-heading-bg: tint($brand-warning, 50%);
|
||||
$panel-default-border: #e5e5e5;
|
||||
$panel-default-heading-bg: #e5e5e5;
|
||||
$panel-default-border: #e5e5e5 !default;
|
||||
$panel-default-heading-bg: #e5e5e5 !default;
|
||||
|
||||
@@ -93,6 +93,67 @@ $(function () {
|
||||
|
||||
show = $("#id_type").val() == "B" && $("#id_required").prop("checked");
|
||||
$(".alert-required-boolean").toggle(show);
|
||||
|
||||
update_default_value_field()
|
||||
}
|
||||
|
||||
function update_default_value_field() {
|
||||
let input = $('#id_default_value');
|
||||
let parent = input.parent();
|
||||
|
||||
let field = input.prop("tagName") == 'DIV' ? input.children().first() : input;
|
||||
let common_attrs = ' name="default_value" placeholder="' + field.attr('placeholder') + '" title="' + field.attr('title') + '" id="id_default_value"';
|
||||
let value = field.val();
|
||||
switch ($("#id_type").val()) {
|
||||
case 'N':
|
||||
input.replaceWith('<input type="number" class="form-control" value="' + value + '" ' + common_attrs + '>');
|
||||
$('.form-group:has(#id_default_value)').show();
|
||||
break;
|
||||
case 'S':
|
||||
input.replaceWith('<input type="text" maxlength="190" class="form-control" value="' + value + '" ' + common_attrs + '>');
|
||||
$('.form-group:has(#id_default_value)').show();
|
||||
break;
|
||||
case 'T':
|
||||
input.replaceWith('<textarea cols="40" rows="10" class="form-control" ' + common_attrs + '>' + value + '</textarea>');
|
||||
$('.form-group:has(#id_default_value)').show();
|
||||
break;
|
||||
case 'B':
|
||||
let checked = (value === 'True' || value === 'on' ? 'checked' : '');
|
||||
input.replaceWith('<input type="checkbox" ' + common_attrs + ' ' + checked + '>');
|
||||
$('.form-group:has(#id_default_value)').show();
|
||||
break;
|
||||
case 'D':
|
||||
let dateField = input.replaceWith('<input type="text" class="form-control datepickerfield" value="' + value + '" ' + common_attrs + '>');
|
||||
form_handlers(parent);
|
||||
$('.form-group:has(#id_default_value)').show();
|
||||
break;
|
||||
case 'H':
|
||||
let timeField = input.replaceWith('<input type="text" class="form-control timepickerfield" value="' + value + '" ' + common_attrs + '>');
|
||||
form_handlers(parent);
|
||||
$('.form-group:has(#id_default_value)').show();
|
||||
break;
|
||||
case 'W':
|
||||
let split = value.split(' ');
|
||||
let date, time;
|
||||
if (split.length > 1) {
|
||||
date = split[0];
|
||||
time = split[1];
|
||||
} else {
|
||||
date = null;
|
||||
time = null;
|
||||
}
|
||||
let dtField = input.replaceWith('<div class="splitdatetimerow" id="id_default_value">\n' +
|
||||
'<input type="text" class="form-control splitdatetimepart datepickerfield" value="' + date + '" name="default_value_date" placeholder="' + field.attr('placeholder') + '" title="' + field.attr('title') + '">\n' +
|
||||
'<input type="text" class="form-control splitdatetimepart timepickerfield" value="' + time + '" name="default_value_time" placeholder="' + field.attr('placeholder') + '" title="' + field.attr('title') + '">\n' +
|
||||
'</div>\n');
|
||||
form_handlers(parent);
|
||||
$('.form-group:has(#id_default_value)').show();
|
||||
break;
|
||||
default:
|
||||
// file, choice, and multiple choice are not implemented
|
||||
$('.form-group:has(#id_default_value)').hide();
|
||||
input.val('')
|
||||
}
|
||||
}
|
||||
|
||||
var $val = $("#id_dependency_value");
|
||||
|
||||
@@ -764,7 +764,7 @@ TEST_INVOICE_RES = {
|
||||
"number": "DUMMY-00001",
|
||||
"is_cancellation": False,
|
||||
"invoice_from": "",
|
||||
"invoice_to": "Sample company\n\n\n \nNew Zealand",
|
||||
"invoice_to": "Sample company\n\n\n \nNew Zealand\nVAT-ID: DE123",
|
||||
"date": "2017-12-10",
|
||||
"refers": None,
|
||||
"locale": "en",
|
||||
@@ -2702,8 +2702,8 @@ def test_order_create_invoice(token_client, organizer, event, order):
|
||||
'number': 'DUMMY-00001',
|
||||
'is_cancellation': False,
|
||||
'invoice_from': '',
|
||||
'invoice_to': 'Sample company\n\n\n \nNew Zealand',
|
||||
'date': '2019-03-23',
|
||||
'invoice_to': 'Sample company\n\n\n \nNew Zealand\nVAT-ID: DE123',
|
||||
'date': now().date().isoformat(),
|
||||
'refers': None,
|
||||
'locale': 'en',
|
||||
'introductory_text': '',
|
||||
|
||||
@@ -251,6 +251,41 @@ class QuestionsTest(ItemFormTest):
|
||||
form_data)
|
||||
assert not doc.select(".alert-success")
|
||||
|
||||
def test_set_default_value(self):
|
||||
q = Question.objects.create(event=self.event1, question="What city are you from?", type=Question.TYPE_TEXT,
|
||||
required=True)
|
||||
doc = self.get_doc('/control/event/%s/%s/questions/%s/change' % (self.orga1.slug, self.event1.slug, q.id))
|
||||
form_data = extract_form_fields(doc.select('.container-fluid form')[0])
|
||||
form_data['default_value'] = 'Heidelberg'
|
||||
doc = self.post_doc('/control/event/%s/%s/questions/%s/change' % (self.orga1.slug, self.event1.slug, q.id),
|
||||
form_data)
|
||||
assert doc.select(".alert-success")
|
||||
q.refresh_from_db()
|
||||
assert q.default_value == 'Heidelberg'
|
||||
|
||||
def test_set_default_value_datetime(self):
|
||||
q = Question.objects.create(event=self.event1, question="What city are you from?", type=Question.TYPE_DATETIME,
|
||||
required=True)
|
||||
doc = self.get_doc('/control/event/%s/%s/questions/%s/change' % (self.orga1.slug, self.event1.slug, q.id))
|
||||
form_data = extract_form_fields(doc.select('.container-fluid form')[0])
|
||||
form_data['default_value_date'] = '2019-01-01'
|
||||
form_data['default_value_time'] = '10:00'
|
||||
doc = self.post_doc('/control/event/%s/%s/questions/%s/change' % (self.orga1.slug, self.event1.slug, q.id),
|
||||
form_data)
|
||||
assert doc.select(".alert-success")
|
||||
q.refresh_from_db()
|
||||
assert q.default_value == '2019-01-01T10:00:00+00:00'
|
||||
|
||||
def test_set_default_value_invalid(self):
|
||||
q = Question.objects.create(event=self.event1, question="What city are you from?", type=Question.TYPE_DATE,
|
||||
required=True)
|
||||
doc = self.get_doc('/control/event/%s/%s/questions/%s/change' % (self.orga1.slug, self.event1.slug, q.id))
|
||||
form_data = extract_form_fields(doc.select('.container-fluid form')[0])
|
||||
form_data['default_value'] = 'foobar'
|
||||
doc = self.post_doc('/control/event/%s/%s/questions/%s/change' % (self.orga1.slug, self.event1.slug, q.id),
|
||||
form_data)
|
||||
assert not doc.select(".alert-success")
|
||||
|
||||
|
||||
class QuotaTest(ItemFormTest):
|
||||
|
||||
|
||||
@@ -1985,6 +1985,59 @@ class QuestionsTestCase(BaseCheckoutTestCase, TestCase):
|
||||
self.q3: 'False',
|
||||
}, should_fail=True)
|
||||
|
||||
def test_datetime_defaultvalues(self):
|
||||
""" Test timezone and format handling for default values """
|
||||
q1 = Question.objects.create(
|
||||
event=self.event, question='When did you wake up today?', type=Question.TYPE_TIME,
|
||||
required=True, default_value='10:00'
|
||||
)
|
||||
q2 = Question.objects.create(
|
||||
event=self.event, question='When was your last haircut?', type=Question.TYPE_DATE,
|
||||
required=True, default_value='2019-01-01'
|
||||
)
|
||||
q3 = Question.objects.create(
|
||||
event=self.event, question='When are you going to arrive?', type=Question.TYPE_DATETIME,
|
||||
required=True, default_value='2019-01-01T10:00:00+00:00'
|
||||
)
|
||||
|
||||
self.ticket.questions.add(q1)
|
||||
self.ticket.questions.add(q2)
|
||||
self.ticket.questions.add(q3)
|
||||
cr = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=23, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
|
||||
assert doc.select('input[name="%s-question_%s"]' % (cr.id, q1.id))[0]['value'] == '10:00'
|
||||
assert doc.select('input[name="%s-question_%s"]' % (cr.id, q2.id))[0]['value'] == '2019-01-01'
|
||||
assert doc.select('input[name="%s-question_%s_0"]' % (cr.id, q3.id))[0]['value'] == '2019-01-01'
|
||||
assert doc.select('input[name="%s-question_%s_1"]' % (cr.id, q3.id))[0]['value'] == '10:00'
|
||||
|
||||
# set to different timezone, this should affect the datetime question's default value displayed
|
||||
self.event.settings.set('timezone', 'US/Central')
|
||||
|
||||
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
|
||||
assert doc.select('input[name="%s-question_%s"]' % (cr.id, q1.id))[0]['value'] == '10:00'
|
||||
assert doc.select('input[name="%s-question_%s"]' % (cr.id, q2.id))[0]['value'] == '2019-01-01'
|
||||
assert doc.select('input[name="%s-question_%s_0"]' % (cr.id, q3.id))[0]['value'] == '2019-01-01'
|
||||
assert doc.select('input[name="%s-question_%s_1"]' % (cr.id, q3.id))[0]['value'] == '04:00'
|
||||
|
||||
# set locale, this should affect the date format
|
||||
self.event.settings.set('locales', ['de'])
|
||||
self.event.save()
|
||||
|
||||
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
|
||||
assert doc.select('input[name="%s-question_%s"]' % (cr.id, q1.id))[0]['value'] == '10:00'
|
||||
assert doc.select('input[name="%s-question_%s"]' % (cr.id, q2.id))[0]['value'] == '01.01.2019'
|
||||
assert doc.select('input[name="%s-question_%s_0"]' % (cr.id, q3.id))[0]['value'] == '01.01.2019'
|
||||
assert doc.select('input[name="%s-question_%s_1"]' % (cr.id, q3.id))[0]['value'] == '04:00'
|
||||
|
||||
|
||||
class CheckoutBundleTest(BaseCheckoutTestCase, TestCase):
|
||||
def setUp(self):
|
||||
|
||||
@@ -681,6 +681,47 @@ class OrdersTest(TestCase):
|
||||
p.refresh_from_db()
|
||||
assert p.state == OrderPayment.PAYMENT_STATE_CREATED
|
||||
|
||||
def test_change_paymentmethod_to_same(self):
|
||||
p_old = self.order.payments.create(
|
||||
provider='banktransfer',
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED,
|
||||
amount=Decimal('10.00'),
|
||||
)
|
||||
self.client.post(
|
||||
'/%s/%s/order/%s/%s/pay/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
|
||||
{
|
||||
'payment': 'banktransfer'
|
||||
}
|
||||
)
|
||||
self.order.refresh_from_db()
|
||||
p_new = self.order.payments.last()
|
||||
assert p_new.provider == 'banktransfer'
|
||||
assert p_new.id != p_old.id
|
||||
assert p_new.state == OrderPayment.PAYMENT_STATE_CREATED
|
||||
p_old.refresh_from_db()
|
||||
assert p_old.state == OrderPayment.PAYMENT_STATE_CANCELED
|
||||
|
||||
def test_change_paymentmethod_cancel_old(self):
|
||||
self.event.settings.set('payment_banktransfer__enabled', True)
|
||||
p_old = self.order.payments.create(
|
||||
provider='testdummy',
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED,
|
||||
amount=Decimal('10.00'),
|
||||
)
|
||||
self.client.post(
|
||||
'/%s/%s/order/%s/%s/pay/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
|
||||
{
|
||||
'payment': 'banktransfer'
|
||||
}
|
||||
)
|
||||
self.order.refresh_from_db()
|
||||
p_new = self.order.payments.last()
|
||||
assert p_new.provider == 'banktransfer'
|
||||
assert p_new.id != p_old.id
|
||||
assert p_new.state == OrderPayment.PAYMENT_STATE_CREATED
|
||||
p_old.refresh_from_db()
|
||||
assert p_old.state == OrderPayment.PAYMENT_STATE_CANCELED
|
||||
|
||||
def test_change_paymentmethod_delete_fee(self):
|
||||
self.event.settings.set('payment_banktransfer__enabled', True)
|
||||
self.event.settings.set('payment_testdummy__enabled', True)
|
||||
|
||||
Reference in New Issue
Block a user