Compare commits

..

32 Commits

Author SHA1 Message Date
johan12345
df25ae3c7c add tests in test_checkout.py 2019-03-27 21:33:26 +01:00
johan12345
d686b386e3 add tests in test_items.py 2019-03-27 21:07:26 +01:00
johan12345
bdda68c82d move JS to the right location 2019-03-27 20:54:04 +01:00
johan12345
2aa36ee2a7 reorder migrations 2019-03-27 20:34:29 +01:00
johan12345
43affc22ab remove unused import 2019-03-27 18:43:44 +01:00
Johan von Forstner
60575377d7 move migration 2019-03-27 18:43:44 +01:00
Raphael Michel
5ec8c7ed96 Store date(times) in ISO formats and UTC 2019-03-27 18:43:44 +01:00
johan12345
ef55a018f8 fix datetime fields 2019-03-27 18:43:04 +01:00
johan12345
6bf9327f87 improvements after review 2019-03-27 18:42:42 +01:00
johan12345
f761f93550 add migration 2019-03-27 18:42:42 +01:00
Johan von Forstner
c996289563 fix default answers for booleans 2019-03-27 18:42:42 +01:00
Johan von Forstner
89bbed42e6 replace form field for default value depending on question type 2019-03-27 18:42:42 +01:00
Johan von Forstner
8e22c0f3a4 Show initial values in form 2019-03-27 18:42:01 +01:00
Johan von Forstner
3483c522da Add default_value model field, validation and form field 2019-03-27 18:41:40 +01:00
Raphael Michel
5f15ebc46f Fix TypeError in offset calculation
sentry issue PRETIXEU-ZB
2019-03-27 18:12:49 +01:00
Raphael Michel
3415fd947a Hotfix: Redirect with a / 2019-03-27 17:46:14 +01:00
Raphael Michel
a70a42c273 Hotfix: Do not use absolute URLs 2019-03-27 17:02:22 +01:00
Raphael Michel
697cdfd5c9 Allow to redirect to checkout directly after adding a product to the cart 2019-03-27 16:45:15 +01:00
Raphael Michel
d8a7de8b23 Allow to filter subevents by attributes in query parameters 2019-03-27 16:15:16 +01:00
Raphael Michel
9f7f0e74ff Fix arrow position in month button 2019-03-27 16:15:16 +01:00
Martin Gross
7ef289da45 Minor JSON spelling mistakes 2019-03-27 15:41:56 +01:00
Raphael Michel
e82bc732a3 Docs: Fix spelling issues 2019-03-27 12:08:22 +01:00
Raphael Michel
4636ccac3b Add signals html_page_header, sass_preamble, sass_postamble 2019-03-27 09:14:51 +01:00
Raphael Michel
e3518bfb4b Fix date-dependent test 2019-03-26 10:20:26 +01:00
Raphael Michel
b2471169af Bank transfer: Improve error message 2019-03-26 09:46:40 +01:00
Raphael Michel
487418678c Banktransfer: Workaround for OrderPayment.MultipleObjectsReturned
Fix sentry issue PRETIXEU-Z7
2019-03-26 09:44:26 +01:00
Raphael Michel
d4795868d6 Correcly cancel payments when starting a new one 2019-03-26 09:41:03 +01:00
Raphael Michel
45af18a23d Work around SubEvent.DoesNotExist in refresh_quota_caches
Fix PRETIXEU-Z8
2019-03-26 09:06:34 +01:00
Raphael Michel
a6de586b80 Make ItemBundle.designated_price non-nullable 2019-03-23 23:42:58 +01:00
Raphael Michel
e6859fa82b Docs: Allow "subnet" in word list 2019-03-23 15:25:39 +01:00
Raphael Michel
2d5e14e517 Fix error in tests 2019-03-23 15:06:29 +01:00
Raphael Michel
7219575b84 Fix #1066 -- Change installation tutorials to PostgreSQL
This is the recommended database server so this documentation should use that
2019-03-23 15:04:12 +01:00
46 changed files with 553 additions and 340 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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.
===================================== ========================== =======================================================

View File

@@ -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": []
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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.
===================================== ========================== =======================================================

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -95,6 +95,7 @@ renderers
reportlab
SaaS
screenshot
scss
searchable
selectable
serializers
@@ -110,6 +111,7 @@ subdomains
subevent
subevents
submodule
subnet
subpath
Symfony
systemd

View File

@@ -1,5 +1,4 @@
from .answers import * # noqa
from .dekodi import * # noqa
from .invoices import * # noqa
from .json import * # noqa
from .mail import * # noqa

View File

@@ -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

View File

@@ -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

View 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'),
),
]

View 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'),
),
]

View File

@@ -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:

View File

@@ -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

View File

@@ -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()

View File

@@ -97,6 +97,10 @@ DEFAULTS = {
'default': '30',
'type': int
},
'redirect_to_checkout_directly': {
'default': 'False',
'type': bool
},
'payment_explanation': {
'default': '',
'type': LazyI18nString

View File

@@ -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']

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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" %}

View File

@@ -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(

View File

@@ -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'])

View File

@@ -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">

View File

@@ -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

View File

@@ -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"]
)

View File

@@ -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(

View File

@@ -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">

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -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 %}"

View File

@@ -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):

View File

@@ -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

View File

@@ -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;

View File

@@ -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");

View File

@@ -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': '',

View File

@@ -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):

View File

@@ -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):

View File

@@ -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)