Compare commits

..

4 Commits

Author SHA1 Message Date
Raphael Michel
756c004d66 Improve dialog 2021-11-20 12:08:47 +01:00
Raphael Michel
61243e4a5a Show additional cookie info 2021-11-20 12:08:47 +01:00
Raphael Michel
c10a8575ad Start python-level API 2021-11-20 12:08:47 +01:00
Raphael Michel
202f34ad5b First steps 2021-11-20 12:08:47 +01:00
254 changed files with 84259 additions and 115442 deletions

View File

@@ -18,17 +18,17 @@ jobs:
name: Tests
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9"]
python-version: [3.6, 3.7, 3.8]
database: [sqlite, postgres, mysql]
exclude:
- database: mysql
python-version: "3.8"
python-version: 3.7
- database: sqlite
python-version: 3.7
- database: mysql
python-version: "3.9"
python-version: 3.6
- database: sqlite
python-version: "3.7"
- database: sqlite
python-version: "3.8"
python-version: 3.6
steps:
- uses: actions/checkout@v2
- uses: getong/mariadb-action@v1.1
@@ -55,7 +55,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install system dependencies
run: sudo apt update && sudo apt install gettext mariadb-client
run: sudo apt update && sudo apt install gettext mysql-client
- name: Install Python dependencies
run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
working-directory: ./src

View File

@@ -220,30 +220,12 @@ Example::
``user``, ``password``
The SMTP user data to use for the connection. Empty by default.
``tls``, ``ssl``
Use STARTTLS or SSL for the SMTP connection. Off by default.
``from``
The email address to set as ``From`` header in outgoing emails by the system.
Default: ``pretix@localhost``
``from_notifications``
The email address to set as ``From`` header in admin notification emails by the system.
Defaults to the value of ``from``.
``from_organizers``
The email address to set as ``From`` header in outgoing emails by the system sent on behalf of organizers.
Defaults to the value of ``from``.
``custom_sender_verification_required``
If this is on (the default), organizers need to verify email addresses they want to use as senders in their event.
``custom_sender_spf_string``
If this is set to a valid SPF string, pretix will show a warning if organizers use a sender address from a domain
that does not include this value.
``custom_smtp_allow_private_networks``
If this is off (the default), custom SMTP servers cannot be private network addresses.
``tls``, ``ssl``
Use STARTTLS or SSL for the SMTP connection. Off by default.
``admins``
Comma-separated list of email addresses that should receive a report about every error code 500 thrown by pretix.
@@ -300,7 +282,7 @@ You can use an existing memcached server as pretix's caching backend::
``location``
The location of memcached, either a host:port combination or a socket file.
If no memcached is configured, pretix will use redis for caching. If neither is configured, pretix will not use any caching.
If no memcached is configured, pretix will use Django's built-in local-memory caching method.
.. note:: If you use memcached and you deploy pretix across multiple servers, you should use *one*
shared memcached instance, not multiple ones, because cache invalidations would not be
@@ -463,10 +445,8 @@ You can configure the maximum file size for uploading various files::
max_size_image = 12
; Max upload size for favicons in MiB, defaults to 1 MiB
max_size_favicon = 2
; Max upload size for email attachments of manually sent emails in MiB, defaults to 10 MiB
; Max upload size for email attachments in MiB, defaults to 10 MiB
max_size_email_attachment = 15
; Max upload size for email attachments of automatically sent emails in MiB, defaults to 1 MiB
max_size_email_auto_attachment = 2
; Max upload size for other files in MiB, defaults to 10 MiB
; This includes all file upload type order questions
max_size_other = 100

View File

@@ -36,6 +36,9 @@ Linux and firewalls, we recommend that you start with `ufw`_.
SSL certificates can be obtained for free these days. We also *do not* provide support for HTTP-only
installations except for evaluation purposes.
.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
**MariaDB 10.2.7 or newer**.
.. warning:: By default, using `ufw` in conjunction will not have any effect. Please make sure to either bind the exposed
ports of your docker container explicitly to 127.0.0.1 or configure docker to respect any set up firewall
rules.
@@ -58,9 +61,6 @@ directory writable to the user that runs pretix inside the docker container::
Database
--------
.. warning:: **Please use PostgreSQL for all new installations**. If you need to go for MySQL, make sure you run
**MySQL 5.7 or newer** or **MariaDB 10.2.7 or newer**.
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. Please make sure that UTF8 is used as encoding for the best compatibility. You can check this with
the following command::
@@ -91,8 +91,6 @@ When using MySQL, make sure you set the character set of the database to ``utf8m
mysql > CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
You will also need to make sure that ``sql_mode`` in your ``my.cnf`` file does **not** include ``ONLY_FULL_GROUP_BY``.
Redis
-----

View File

@@ -34,6 +34,9 @@ Linux and firewalls, we recommend that you start with `ufw`_.
SSL certificates can be obtained for free these days. We also *do not* provide support for HTTP-only
installations except for evaluation purposes.
.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
**MariaDB 10.2.7 or newer**.
Unix user
---------
@@ -47,9 +50,6 @@ In this guide, all code lines prepended with a ``#`` symbol are commands that yo
Database
--------
.. warning:: **Please use PostgreSQL for all new installations**. If you need to go for MySQL, make sure you run
**MySQL 5.7 or newer** or **MariaDB 10.2.7 or newer**.
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. Please make sure that UTF8 is used as encoding for the
best compatibility. You can check this with the following command::
@@ -65,8 +65,6 @@ When using MySQL, make sure you set the character set of the database to ``utf8m
mysql > CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
You will also need to make sure that ``sql_mode`` in your ``my.cnf`` file does **not** include ``ONLY_FULL_GROUP_BY``.
Package dependencies
--------------------
@@ -144,7 +142,7 @@ If you're running MySQL, also install the client library::
(venv)$ pip3 install mysqlclient
Note that you need Python 3.7 or newer. You can find out your Python version using ``python -V``.
Note that you need Python 3.6 or newer. You can find out your Python version using ``python -V``.
We also need to create a data directory::
@@ -261,14 +259,14 @@ The following snippet is an example on how to configure a nginx proxy for pretix
}
location /static/ {
alias /var/pretix/venv/lib/python3.10/site-packages/pretix/static.dist/;
alias /var/pretix/venv/lib/python3.7/site-packages/pretix/static.dist/;
access_log off;
expires 365d;
add_header Cache-Control "public";
}
}
.. note:: Remember to replace the ``python3.10`` in the ``/static/`` path in the config
.. note:: Remember to replace the ``python3.7`` in the ``/static/`` path in the config
above with your python version.
We recommend reading about setting `strong encryption settings`_ for your web server.

View File

@@ -97,8 +97,7 @@ For example, if you want users to be redirected to ``https://example.org/order/r
either enter ``https://example.org`` or ``https://example.org/order/``.
The user will be redirected back to your page instead of pretix' order confirmation page after the payment,
**regardless of whether it was successful or not**. We will append an ``error=…`` query parameter with an error
message, but you should not rely on that and instead make sure you use our API to check if the payment actually
**regardless of whether it was successful or not**. Make sure you use our API to check if the payment actually
worked! Your final URL could look like this::
https://test.pretix.eu/democon/3vjrh/order/NSLEZ/ujbrnsjzbq4dzhck/pay/123/?return_url=https%3A%2F%2Fexample.org%2Forder%2Freturn%3Ftx_id%3D1234

View File

@@ -58,12 +58,6 @@ lines list of objects The actual invo
created before this field was introduced as well as for
all lines not created by a product (e.g. a shipping or
cancellation fee).
├ subevent integer Event series date ID used to create this line. Note that everything
about the subevent might have changed since the creation
of the invoice. Can be ``null`` for all invoice lines
created before this field was introduced as well as for
all lines not created by a product (e.g. a shipping or
cancellation fee) as well as for all events that are not a series.
├ fee_type string Fee type, e.g. ``shipping``, ``service``, ``payment``,
``cancellation``, ``giftcard``, or ``other. Can be ``null`` for
all invoice lines
@@ -126,10 +120,6 @@ internal_reference string Customer's refe
The attribute ``lines.event_location`` has been added.
.. versionchanged:: 4.6
The attribute ``lines.subevent`` has been added.
Endpoints
---------
@@ -195,7 +185,6 @@ Endpoints
"description": "Budget Ticket",
"item": 1234,
"variation": 245,
"subevent": null,
"fee_type": null,
"fee_internal_type": null,
"event_date_from": "2017-12-27T10:00:00Z",
@@ -285,7 +274,6 @@ Endpoints
"description": "Budget Ticket",
"item": 1234,
"variation": 245,
"subevent": null,
"fee_type": null,
"fee_internal_type": null,
"event_date_from": "2017-12-27T10:00:00Z",

View File

@@ -24,9 +24,6 @@ active boolean If ``false``, t
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
require_approval boolean If ``true``, orders with this variation will need to be
approved by the event organizer before they can be
paid.
require_membership boolean If ``true``, booking this variation requires an active membership.
require_membership_hidden boolean If ``true`` and ``require_membership`` is set, this variation will
be hidden from users without a valid membership.
@@ -79,7 +76,6 @@ Endpoints
"en": "S"
},
"active": true,
"require_approval": false,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
@@ -101,7 +97,6 @@ Endpoints
"en": "L"
},
"active": true,
"require_approval": false,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
@@ -152,7 +147,6 @@ Endpoints
"price": "10.00",
"original_price": null,
"active": true,
"require_approval": false,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
@@ -189,7 +183,6 @@ Endpoints
"value": {"en": "Student"},
"default_price": "10.00",
"active": true,
"require_approval": false,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
@@ -216,7 +209,6 @@ Endpoints
"price": "10.00",
"original_price": null,
"active": true,
"require_approval": false,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
@@ -274,7 +266,6 @@ Endpoints
"price": "10.00",
"original_price": null,
"active": false,
"require_approval": false,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],

View File

@@ -132,10 +132,6 @@ last_modified datetime Last modificati
The ``item`` and ``variation`` query parameters have been added.
.. versionchanged:: 4.6
The ``subevent`` query parameters has been added.
.. _order-position-resource:
@@ -437,7 +433,6 @@ List of all orders
recommend using this in combination with ``testmode=false``, since test mode orders can vanish at any time and
you will not notice it using this method.
:query datetime created_since: Only return orders that have been created since the given date.
:query integer subevent: Only return orders with a position that contains this subevent ID. *Warning:* Result will also include orders if they contain mixed subevents, and it will even return orders where the subevent is only contained in a canceled position.
:query datetime subevent_after: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive after, and it considers the **end** of the subevent (or its start, if the end is not set).
:query datetime subevent_before: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive before, and it considers the **start** of the subevent.
:query string exclude: Exclude a field from the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times.

View File

@@ -16,22 +16,15 @@ Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the tax rule
name multi-lingual string The tax rules' name
internal_name string An optional name that is only used in the backend
rate decimal (string) Tax rate in percent
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
the specified product price
eu_reverse_charge boolean If ``true``, EU reverse charge rules are applied
home_country string Merchant country (required for reverse charge), can be
``null`` or empty string
keep_gross_if_rate_changes boolean If ``true``, changes of the tax rate based on custom
rules keep the gross price constant (default is ``false``)
===================================== ========================== =======================================================
.. versionchanged:: 4.6
The ``internal_name`` and ``keep_gross_if_rate_changes`` attributes have been added.
Endpoints
---------
@@ -63,11 +56,9 @@ Endpoints
{
"id": 1,
"name": {"en": "VAT"},
"internal_name": "VAT",
"rate": "19.00",
"price_includes_tax": true,
"eu_reverse_charge": false,
"keep_gross_if_rate_changes": false,
"home_country": "DE"
}
]
@@ -103,11 +94,9 @@ Endpoints
{
"id": 1,
"name": {"en": "VAT"},
"internal_name": "VAT",
"rate": "19.00",
"price_includes_tax": true,
"eu_reverse_charge": false,
"keep_gross_if_rate_changes": false,
"home_country": "DE"
}
@@ -151,11 +140,9 @@ Endpoints
{
"id": 1,
"name": {"en": "VAT"},
"internal_name": "VAT",
"rate": "19.00",
"price_includes_tax": true,
"eu_reverse_charge": false,
"keep_gross_if_rate_changes": false,
"home_country": "DE"
}
@@ -198,11 +185,9 @@ Endpoints
{
"id": 1,
"name": {"en": "VAT"},
"internal_name": "VAT",
"rate": "20.00",
"price_includes_tax": true,
"eu_reverse_charge": false,
"keep_gross_if_rate_changes": false,
"home_country": "DE"
}

View File

@@ -2,7 +2,7 @@ Algorithms
==========
The business logic inside pretix is full of complex algorithms making decisions based on all the hundreds of settings
and input parameters available. Some of them are documented here as graphs, either because fully understanding them is very important
and input parameters available. Some of them are documented here as graphs, either because fully understanding them is very
when working on features close to them, or because they also need to be re-implemented by client-side components like our
ticket scanning apps and we want to ensure the implementations are as similar as possible to avoid confusion.

View File

@@ -1,119 +0,0 @@
.. highlight:: python
:linenothreshold: 5
.. _`cookieconsent`:
Handling cookie consent
=======================
pretix includes an optional feature to handle cookie consent explicitly to comply with EU regulations.
If your plugin sets non-essential cookies or includes a third-party service that does so, you should
integrate with this feature.
Server-side integration
-----------------------
First, you need to declare that you are using non-essential cookies by responding to the following
signal:
.. automodule:: pretix.presale.signals
:members: register_cookie_providers
You are expected to return a list of ``CookieProvider`` objects instantiated from the following class:
.. class:: pretix.presale.cookies.CookieProvider
.. py:attribute:: CookieProvider.identifier
A short and unique identifier used to distinguish this cookie provider form others (required).
.. py:attribute:: CookieProvider.provider_name
A human-readable name of the entity of feature responsible for setting the cookie (required).
.. py:attribute:: CookieProvider.usage_classes
A list of enum values from the ``pretix.presale.cookies.UsageClass`` enumeration class, such as
``UsageClass.ANALYTICS``, ``UsageClass.MARKETING``, or ``UsageClass.SOCIAL`` (required).
.. py:attribute:: CookieProvider.privacy_url
A link to a privacy policy (optional).
Here is an example of such a receiver:
.. code-block:: python
@receiver(register_cookie_providers)
def recv_cookie_providers(sender, request, **kwargs):
return [
CookieProvider(
identifier='google_analytics',
provider_name='Google Analytics',
usage_classes=[UsageClass.ANALYTICS],
)
]
JavaScript-side integration
---------------------------
The server-side integration only causes the cookie provider to show up in the cookie dialog. You still
need to care about actually enforcing the consent state.
You can access the consent state through the ``window.pretix.cookie_consent`` variable. Whenever the
value changes, a ``pretix:cookie-consent:change`` event is fired on the ``document`` object.
The variable will generally have one of the following states:
.. rst-class:: rest-resource-table
================================================================ =====================================================
State Interpretation
================================================================ =====================================================
``pretix === undefined || pretix.cookie_consent === undefined`` Your JavaScript has loaded before the cookie consent
script. Wait for the event to be fired, then try again,
do not yet set a cookie.
``pretix.cookie_consent === null`` The cookie consent mechanism has not been enabled. This
usually means that you can set cookies however you like.
``pretix.cookie_consent[identifier] === undefined`` The cookie consent mechanism is loaded, but has no data
on your cookie yet, wait for the event to be fired, do not
yet set a cookie.
``pretix.cookie_consent[identifier] === true`` The user has consented to your cookie.
``pretix.cookie_consent[identifier] === false`` The user has actively rejected your cookie.
================================================================ =====================================================
If you are integrating e.g. a tracking provider with native cookie consent support such
as Facebook's Pixel, you can integrate it like this:
.. code-block:: javascript
var consent = (window.pretix || {}).cookie_consent;
if (consent !== null && !(consent || {}).facebook) {
fbq('consent', 'revoke');
}
fbq('init', ...);
document.addEventListener('pretix:cookie-consent:change', function (e) {
fbq('consent', (e.detail || {}).facebook ? 'grant' : 'revoke');
})
If you have a JavaScript function that you only want to load if consent for a specific ``identifier``
is given, you can wrap it like this:
.. code-block:: javascript
var consent_identifier = "youridentifier";
var consent = (window.pretix || {}).cookie_consent;
if (consent === null || (consent || {})[consent_identifier] === true) {
// Cookie consent tool is either disabled or consent is given
addScriptElement(src);
return;
}
// Either cookie consent tool has not loaded yet or consent is not given
document.addEventListener('pretix:cookie-consent:change', function onChange(e) {
var consent = e.detail || {};
if (consent === null || consent[consent_identifier] === true) {
addScriptElement(src);
document.removeEventListener('pretix:cookie-consent:change', onChange);
}
})

View File

@@ -17,7 +17,6 @@ Contents:
shredder
import
customview
cookieconsent
auth
general
quality

View File

@@ -62,8 +62,6 @@ The provider class
.. autoattribute:: public_name
.. autoattribute:: confirm_button_name
.. autoattribute:: is_enabled
.. autoattribute:: priority

View File

@@ -1,301 +0,0 @@
Secrets Import
==============
Usually, pretix generates ticket secrets (i.e. the QR code used for scanning) itself. You can read more about this
process at :ref:`secret_generators`.
With the "Secrets Import" plugin, you can upload your own list of secrets to be used instead. This is useful for
integrating with third-party check-in systems.
API Resource description
-------------------------
The secrets import plugin provides a HTTP API that allows you to create new secrets.
The imported secret resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the secret
secret string Actual string content of the secret (QR code content)
used boolean Whether the secret was already used for a ticket. If ``true``,
the secret can no longer be deleted. Secrets are never used
twice, even if an order is canceled or deleted.
item integer Internal ID of a product, or ``null``. If set, the secret
will only be used for tickets of this product.
variation integer Internal ID of a product variation, or ``null``. If set, the secret
will only be used for tickets of this product variation.
subevent integer Internal ID of an event series date, or ``null``. If set, the secret
will only be used for tickets of this event series date.
===================================== ========================== =======================================================
API Endpoints
-------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/imported_secrets/
Returns a list of all secrets imported for an event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/imported_secrets/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"secret": "foobar",
"used": false,
"item": null,
"variation": null,
"subevent": null
}
]
}
:query page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to view it.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/imported_secrets/(id)/
Returns information on one secret, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/imported_secrets/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"secret": "foobar",
"used": false,
"item": null,
"variation": null,
"subevent": null
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param id: The ``id`` field of the secret to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/secret does not exist **or** you have no permission to view it.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/imported_secrets/
Create a new secret.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/imported_secrets/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 166
{
"secret": "foobar",
"used": false,
"item": null,
"variation": null,
"subevent": null
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"secret": "foobar",
"used": false,
"item": null,
"variation": null,
"subevent": null
}
:param organizer: The ``slug`` field of the organizer to a create new secret for
:param event: The ``slug`` field of the event to create a new secret for
:statuscode 201: no error
:statuscode 400: The secret could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create secrets.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/imported_secrets/bulk_create/
Create new secrets in bulk (up to 500 per request). The request either succeeds or fails entirely.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/imported_secrets/bulk_create/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 166
[
{
"secret": "foobar",
"used": false,
"item": null,
"variation": null,
"subevent": null
},
{
"secret": "baz",
"used": false,
"item": null,
"variation": null,
"subevent": null
}
]
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
[
{
"id": 1,
"secret": "foobar",
"used": false,
"item": null,
"variation": null,
"subevent": null
},
{
"id": 2,
"secret": "baz",
"used": false,
"item": null,
"variation": null,
"subevent": null
}
]
:param organizer: The ``slug`` field of the organizer to create new secrets for
:param event: The ``slug`` field of the event to create new secrets for
:statuscode 201: no error
:statuscode 400: The secrets could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create secrets.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/imported_secrets/(id)/
Update a secret. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/imported_secrets/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 34
{
"item": 2
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"id": 1,
"secret": "foobar",
"used": false,
"item": 2,
"variation": null,
"subevent": null
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the secret to modify
:statuscode 200: no error
:statuscode 400: The secret could not be modified due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/secret does not exist **or** you have no permission to change it.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/imported_secrets/(id)/
Delete a secret. You can only delete secrets that have not yet been used.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/imported_secrets/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the secret to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/secret does not exist **or** you have no permission to change it **or** the secret has already been used

View File

@@ -17,5 +17,4 @@ If you want to **create** a plugin, please go to the
campaigns
certificates
digital
imported_secrets
webinar

View File

@@ -203,4 +203,4 @@ Then, please contact support@pretix.eu and we will enable DKIM for your domain o
.. _Sender Policy Framework: https://en.wikipedia.org/wiki/Sender_Policy_Framework
.. _SPF specification: http://www.open-spf.org/SPF_Record_Syntax
.. _SPF specification: http://www.openspf.org/SPF_Record_Syntax

View File

@@ -1,5 +1,3 @@
.. _secret_generators:
Ticket secret generators
========================

View File

@@ -309,10 +309,6 @@ Currently, the following attributes are understood by pretix itself:
always be modified. Note that this is not a security feature and can easily be overridden by users, so do not rely
on this for authentication.
* If ``data-consent="…"`` is given, the cookie consent mechanism will be initialized with consent for the given cookie
providers. All other providers will be disabled, no consent dialog will be shown. This is useful if you already
asked the user for consent and don't want them to be asked again. Example: ``data-consent="facebook,google_analytics"``
Any configured pretix plugins might understand more data fields. For example, if the appropriate plugins on pretix
Hosted or pretix Enterprise are active, you can pass the following fields:

View File

@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "4.7.0.dev0"
__version__ = "4.5.0.dev0"

View File

@@ -637,7 +637,7 @@ class SubEventSerializer(I18nAwareModelSerializer):
class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
class Meta:
model = TaxRule
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country', 'internal_name', 'keep_gross_if_rate_changes')
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country')
class EventSettingsSerializer(SettingsSerializer):
@@ -713,6 +713,7 @@ class EventSettingsSerializer(SettingsSerializer):
'ticket_download_require_validated_email',
'ticket_secret_length',
'mail_prefix',
'mail_from',
'mail_from_name',
'mail_attach_ical',
'mail_attach_tickets',

View File

@@ -58,9 +58,8 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
class Meta:
model = ItemVariation
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price', 'original_price', 'require_approval',
'require_membership', 'require_membership_types',
'require_membership_hidden', 'available_from', 'available_until',
'position', 'default_price', 'price', 'original_price',
'require_membership', 'require_membership_types', 'require_membership_hidden', 'available_from', 'available_until',
'sales_channels', 'hide_without_voucher',)
def __init__(self, *args, **kwargs):
@@ -75,9 +74,8 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
class Meta:
model = ItemVariation
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price', 'original_price', 'require_approval',
'require_membership', 'require_membership_types',
'require_membership_hidden', 'available_from', 'available_until',
'position', 'default_price', 'price', 'original_price',
'require_membership', 'require_membership_types', 'require_membership_hidden', 'available_from', 'available_until',
'sales_channels', 'hide_without_voucher',)
def __init__(self, *args, **kwargs):

View File

@@ -1426,7 +1426,7 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
class Meta:
model = InvoiceLine
fields = ('position', 'description', 'item', 'variation', 'subevent', 'attendee_name', 'event_date_from',
fields = ('position', 'description', 'item', 'variation', 'attendee_name', 'event_date_from',
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_name', 'fee_type',
'fee_internal_type', 'event_location')

View File

@@ -94,7 +94,6 @@ with scopes_disabled():
search = django_filters.CharFilter(method='search_qs')
item = django_filters.CharFilter(field_name='all_positions', lookup_expr='item_id')
variation = django_filters.CharFilter(field_name='all_positions', lookup_expr='variation_id')
subevent = django_filters.CharFilter(field_name='all_positions', lookup_expr='subevent_id')
class Meta:
model = Order
@@ -659,13 +658,12 @@ class OrderViewSet(viewsets.ModelViewSet):
_order_placed_email(
request.event, order, payment.payment_provider if payment else None, email_template,
log_entry, invoice, payment, is_free=free_flow
log_entry, invoice, payment
)
if email_attendees:
for p in order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
_order_placed_email_attendee(request.event, order, p, email_attendees_template, log_entry,
is_free=free_flow)
_order_placed_email_attendee(request.event, order, p, email_attendees_template, log_entry)
if not free_flow and order.status == Order.STATUS_PAID and payment:
payment._send_paid_mail(invoice, None, '')

View File

@@ -146,8 +146,7 @@ class NativeAuthBackend(BaseAuthBackend):
d = OrderedDict([
('email', forms.EmailField(label=_("E-mail"), max_length=254,
widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))),
('password', forms.CharField(label=_("Password"), widget=forms.PasswordInput,
max_length=4096)),
('password', forms.CharField(label=_("Password"), widget=forms.PasswordInput)),
])
return d

View File

@@ -25,7 +25,6 @@ from datetime import timedelta
from decimal import Decimal
from itertools import groupby
from smtplib import SMTPResponseException
from typing import TypeVar
import css_inline
from django.conf import settings
@@ -33,7 +32,6 @@ from django.core.mail.backends.smtp import EmailBackend
from django.db.models import Count
from django.dispatch import receiver
from django.template.loader import get_template
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import (
get_language, gettext_lazy as _, pgettext_lazy,
@@ -51,23 +49,23 @@ from pretix.base.templatetags.rich_text import markdown_compile_email
logger = logging.getLogger('pretix.base.email')
T = TypeVar("T", bound=EmailBackend)
class CustomSMTPBackend(EmailBackend):
def test_custom_smtp_backend(backend: T, from_addr: str) -> None:
try:
backend.open()
backend.connection.ehlo_or_helo_if_needed()
(code, resp) = backend.connection.mail(from_addr, [])
if code != 250:
logger.warning('Error testing mail settings, code %d, resp: %s' % (code, resp))
raise SMTPResponseException(code, resp)
(code, resp) = backend.connection.rcpt('testdummy@pretix.eu')
if (code != 250) and (code != 251):
logger.warning('Error testing mail settings, code %d, resp: %s' % (code, resp))
raise SMTPResponseException(code, resp)
finally:
backend.close()
def test(self, from_addr):
try:
self.open()
self.connection.ehlo_or_helo_if_needed()
(code, resp) = self.connection.mail(from_addr, [])
if code != 250:
logger.warn('Error testing mail settings, code %d, resp: %s' % (code, resp))
raise SMTPResponseException(code, resp)
(code, resp) = self.connection.rcpt('testdummy@pretix.eu')
if (code != 250) and (code != 251):
logger.warn('Error testing mail settings, code %d, resp: %s' % (code, resp))
raise SMTPResponseException(code, resp)
finally:
self.close()
class BaseHTMLMailRenderer:
@@ -165,20 +163,9 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
has_addons=Count('addons')
))
htmlctx['cart'] = [(k, list(v)) for k, v in groupby(
sorted(
positions,
key=lambda op: (
(op.addon_to.positionid if op.addon_to_id else op.positionid),
op.positionid
)
),
key=lambda op: (
op.item,
op.variation,
op.subevent,
op.attendee_name,
(op.pk if op.addon_to_id else None),
(op.pk if op.has_addons else None)
positions, key=lambda op: (
op.item, op.variation, op.subevent, op.attendee_name,
(op.pk if op.addon_to_id else None), (op.pk if op.has_addons else None)
)
)]
@@ -465,15 +452,6 @@ def base_placeholders(sender, **kwargs):
}
),
),
SimpleFunctionalMailTextPlaceholder(
'event_location', ['event_or_subevent'], lambda event_or_subevent: str(event_or_subevent.location or ''),
lambda event: str(event.location or ''),
),
SimpleFunctionalMailTextPlaceholder(
'event_admission_time', ['event_or_subevent'],
lambda event_or_subevent: date_format(event_or_subevent.date_admission, 'TIME_FORMAT') if event_or_subevent.date_admission else '',
lambda event: date_format(event.date_admission, 'TIME_FORMAT') if event.date_admission else '',
),
SimpleFunctionalMailTextPlaceholder(
'subevent', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: str(waiting_list_entry.subevent or event),
@@ -643,10 +621,6 @@ def base_placeholders(sender, **kwargs):
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
v
))
ph.append(SimpleFunctionalMailTextPlaceholder(
'meta_%s' % k, ['event_or_subevent'], lambda event_or_subevent, k=k: event_or_subevent.meta_data[k],
v
))
return ph

View File

@@ -118,27 +118,6 @@ class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
self.cleaned_data[k] = self.initial[k]
return super().save()
def clean(self):
d = super().clean()
# There is logic in HierarkeyForm.save() to only persist fields that changed. HierarkeyForm determines if
# something changed by comparing `self._s.get(name)` to `value`. This leaves an edge case open for multi-lingual
# text fields. On the very first load, the initial value in `self._s.get(name)` will be a LazyGettextProxy-based
# string. However, only some of the languages are usually visible, so even if the user does not change anything
# at all, it will be considered a changed value and stored. We do not want that, as it makes it very hard to add
# languages to an organizer/event later on. So we trick it and make sure nothing gets changed in that situation.
for name, field in self.fields.items():
if isinstance(field, i18nfield.forms.I18nFormField):
value = d.get(name)
if not value:
continue
current = self._s.get(name, as_type=type(value))
if name not in self.changed_data:
d[name] = current
return d
def get_new_filename(self, name: str) -> str:
from pretix.base.models import Event

View File

@@ -154,7 +154,6 @@ class RegistrationForm(forms.Form):
widget=forms.PasswordInput(attrs={
'autocomplete': 'new-password' # see https://bugs.chromium.org/p/chromium/issues/detail?id=370363#c7
}),
max_length=4096,
required=True
)
password_repeat = forms.CharField(
@@ -162,7 +161,6 @@ class RegistrationForm(forms.Form):
widget=forms.PasswordInput(attrs={
'autocomplete': 'new-password' # see https://bugs.chromium.org/p/chromium/issues/detail?id=370363#c7
}),
max_length=4096,
required=True
)
keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False)
@@ -206,13 +204,11 @@ class PasswordRecoverForm(forms.Form):
password = forms.CharField(
label=_('Password'),
widget=forms.PasswordInput,
max_length=4096,
required=True
)
password_repeat = forms.CharField(
label=_('Repeat password'),
widget=forms.PasswordInput,
max_length=4096,
widget=forms.PasswordInput
)
def __init__(self, user_id=None, *args, **kwargs):

View File

@@ -333,41 +333,23 @@ class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
def guess_country(event):
# Try to guess the initial country from either the country of the merchant
# or the locale. This will hopefully save at least some users some scrolling :)
locale = get_language_without_region()
country = event.settings.region or event.settings.invoice_address_from_country
if not country:
country = get_country_by_locale(get_language_without_region())
valid_countries = countries.countries
if '-' in locale:
parts = locale.split('-')
# TODO: does this actually work?
if parts[1].upper() in valid_countries:
country = Country(parts[1].upper())
elif parts[0].upper() in valid_countries:
country = Country(parts[0].upper())
else:
if locale.upper() in valid_countries:
country = Country(locale.upper())
return country
def get_country_by_locale(locale):
country = None
valid_countries = countries.countries
if '-' in locale:
parts = locale.split('-')
# TODO: does this actually work?
if parts[1].upper() in valid_countries:
country = Country(parts[1].upper())
elif parts[0].upper() in valid_countries:
country = Country(parts[0].upper())
else:
if locale.upper() in valid_countries:
country = Country(locale.upper())
return country
def guess_phone_prefix(event):
with language(get_babel_locale()):
country = str(guess_country(event))
return get_phone_prefix(country)
def get_phone_prefix(country):
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
if country in values:
return prefix
return None
class QuestionCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
option_template_name = 'pretixbase/forms/widgets/checkbox_option_with_links.html'
@@ -798,26 +780,25 @@ class BaseQuestionsForm(forms.Form):
if q.valid_datetime_max:
field.validators.append(MaxDateTimeValidator(q.valid_datetime_max))
elif q.type == Question.TYPE_PHONENUMBER:
if initial:
with language(get_babel_locale()):
default_country = guess_country(event)
default_prefix = None
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
if str(default_country) in values:
default_prefix = prefix
try:
initial = PhoneNumber().from_string(initial.answer)
initial = PhoneNumber().from_string(initial.answer) if initial else "+{}.".format(default_prefix)
except NumberParseException:
initial = None
if not initial:
phone_prefix = guess_phone_prefix(event)
if phone_prefix:
initial = "+{}.".format(phone_prefix)
field = PhoneNumberField(
label=label, required=required,
help_text=help_text,
# We now exploit an implementation detail in PhoneNumberPrefixWidget to allow us to pass just
# a country code but no number as an initial value. It's a bit hacky, but should be stable for
# the future.
initial=initial,
widget=WrappedPhoneNumberPrefixWidget()
)
field = PhoneNumberField(
label=label, required=required,
help_text=help_text,
# We now exploit an implementation detail in PhoneNumberPrefixWidget to allow us to pass just
# a country code but no number as an initial value. It's a bit hacky, but should be stable for
# the future.
initial=initial,
widget=WrappedPhoneNumberPrefixWidget()
)
field.question = q
if answers:
# Cache the answer object for later use
@@ -888,12 +869,6 @@ class BaseQuestionsForm(forms.Form):
if question_is_required(q) and not answer and answer != 0 and not field.errors:
raise ValidationError({'question_%d' % q.pk: [_('This field is required.')]})
# Strip invisible question from cleaned_data so they don't end up in the database
for q in question_cache.values():
answer = d.get('question_%d' % q.pk)
if q.dependency_question_id and not question_is_visible(q.dependency_question_id, q.dependency_values) and answer is not None:
d['question_%d' % q.pk] = None
return d
@@ -1069,11 +1044,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.instance.vat_id_validated = True
self.instance.vat_id = normalized_id
except VATIDFinalError as e:
if self.all_optional:
self.instance.vat_id_validated = False
messages.warning(self.request, e.message)
else:
raise ValidationError(e.message)
raise ValidationError(e.message)
except VATIDTemporaryError as e:
self.instance.vat_id_validated = False
if self.request and self.vat_warning:

View File

@@ -86,6 +86,14 @@ class TimePickerWidget(forms.TimeInput):
class UploadedFileWidget(forms.ClearableFileInput):
def __init__(self, *args, **kwargs):
# Browsers can't recognize that the server already has a file uploaded
# Don't mark this input as being required if we already have an answer
# (this needs to be done via the attrs, otherwise we wouldn't get the "required" star on the field label)
attrs = kwargs.get('attrs', {})
if kwargs.get('required') and kwargs.get('initial'):
attrs.update({'required': None})
kwargs.update({'attrs': attrs})
self.position = kwargs.pop('position')
self.event = kwargs.pop('event')
self.answer = kwargs.pop('answer')
@@ -117,15 +125,6 @@ class UploadedFileWidget(forms.ClearableFileInput):
'answer': self.answer.pk,
})
def get_context(self, name, value, attrs):
# Browsers can't recognize that the server already has a file uploaded
# Don't mark this input as being required if we already have an answer
# (this needs to be done via the attrs, otherwise we wouldn't get the "required" star on the field label)
ctx = super().get_context(name, value, attrs)
if ctx['widget']['is_initial']:
ctx['widget']['attrs']['required'] = False
return ctx
def format_value(self, value):
if self.is_initial(value):
return self.FakeFile(value, self.position, self.event, self.answer)

View File

@@ -23,9 +23,7 @@ from decimal import Decimal
from django.core.management.base import BaseCommand
from django.db import models
from django.db.models import (
Case, Count, F, OuterRef, Q, Subquery, Sum, Value, When,
)
from django.db.models import Case, F, OuterRef, Q, Subquery, Sum, Value, When
from django.db.models.functions import Coalesce
from django_scopes import scopes_disabled
@@ -47,18 +45,6 @@ class Command(BaseCommand):
output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
position_cnt=Case(
When(Q(status__in=('e', 'c')) | Q(require_approval=True), then=Value(0)),
default=Coalesce(
Subquery(
OrderPosition.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(p=Count('*')).values('p'),
output_field=models.IntegerField()
), Value(0), output_field=models.IntegerField()
),
output_field=models.IntegerField()
),
fee_total=Coalesce(
Subquery(
OrderFee.objects.filter(
@@ -75,15 +61,6 @@ class Command(BaseCommand):
output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
tx_cnt=Coalesce(
Subquery(
Transaction.objects.filter(
order=OuterRef('pk'),
item__isnull=False,
).order_by().values('order').annotate(p=Sum(F('count'))).values('p'),
output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
).annotate(
correct_total=Case(
When(Q(status=Order.STATUS_CANCELED) | Q(status=Order.STATUS_EXPIRED) | Q(require_approval=True),
@@ -93,15 +70,13 @@ class Command(BaseCommand):
),
).exclude(
total=F('position_total') + F('fee_total'),
tx_total=F('correct_total'),
tx_cnt=F('position_cnt')
tx_total=F('correct_total')
).select_related('event')
for o in qs:
if abs(o.tx_total - o.correct_total) < Decimal('0.00001') and abs(o.position_total + o.fee_total - o.total) < Decimal('0.00001') \
and o.tx_cnt == o.position_cnt:
if abs(o.tx_total - o.correct_total) < Decimal('0.00001') and abs(o.position_total + o.fee_total - o.total) < Decimal('0.00001'):
# Ignore SQLite which treats Decimals like floats…
continue
print(f"Error in order {o.full_code}: status={o.status}, sum(positions)+sum(fees)={o.position_total + o.fee_total}, "
f"order.total={o.total}, sum(transactions)={o.tx_total}, expected={o.correct_total}, pos_cnt={o.position_cnt}, tx_pos_cnt={o.tx_cnt}")
f"order.total={o.total}, sum(transactions)={o.tx_total}, expected={o.correct_total}")
self.stderr.write(self.style.SUCCESS(f'Check completed.'))

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.9 on 2021-12-13 14:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0204_orderposition_backfill_is_bundled'),
]
operations = [
migrations.AddField(
model_name='itemvariation',
name='require_approval',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 3.2.9 on 2022-01-12 10:59
import phonenumber_field.modelfields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0205_itemvariation_require_approval'),
]
operations = [
migrations.AddField(
model_name='customer',
name='phone',
field=phonenumber_field.modelfields.PhoneNumberField(max_length=128, null=True, region=None),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 3.2.4 on 2022-01-19 14:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0206_customer_phone'),
]
operations = [
migrations.AddField(
model_name='taxrule',
name='internal_name',
field=models.CharField(max_length=190, null=True),
),
migrations.AddField(
model_name='taxrule',
name='keep_gross_if_rate_changes',
field=models.BooleanField(default=False),
),
]

View File

@@ -29,7 +29,6 @@ from django.db.models import F, Q
from django.utils.crypto import get_random_string, salted_hmac
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager, scopes_disabled
from phonenumber_field.modelfields import PhoneNumberField
from pretix.base.banlist import banned
from pretix.base.models.base import LoggedModel
@@ -46,7 +45,6 @@ class Customer(LoggedModel):
organizer = models.ForeignKey(Organizer, related_name='customers', on_delete=models.CASCADE)
identifier = models.CharField(max_length=190, db_index=True, unique=True)
email = models.EmailField(db_index=True, null=True, blank=False, verbose_name=_('E-mail'), max_length=190)
phone = PhoneNumberField(null=True, blank=True, verbose_name=_('Phone number'))
password = models.CharField(verbose_name=_('Password'), max_length=128)
name_cached = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
name_parts = models.JSONField(default=dict)
@@ -89,7 +87,6 @@ class Customer(LoggedModel):
self.name_parts = {}
self.name_cached = ''
self.email = None
self.phone = None
self.save()
self.all_logentries().update(data={}, shredded=True)
self.orders.all().update(customer=None)

View File

@@ -665,22 +665,21 @@ class Event(EventMixin, LoggedModel):
return locking.LockManager(self)
def get_mail_backend(self, timeout=None):
def get_mail_backend(self, timeout=None, force_custom=False):
"""
Returns an email server connection, either by using the system-wide connection
or by returning a custom one based on the event's settings.
"""
from pretix.base.email import CustomSMTPBackend
if self.settings.smtp_use_custom:
return get_connection(backend=settings.EMAIL_CUSTOM_SMTP_BACKEND,
host=self.settings.smtp_host,
port=self.settings.smtp_port,
username=self.settings.smtp_username,
password=self.settings.smtp_password,
use_tls=self.settings.smtp_use_tls,
use_ssl=self.settings.smtp_use_ssl,
fail_silently=False,
timeout=timeout)
if self.settings.smtp_use_custom or force_custom:
return CustomSMTPBackend(host=self.settings.smtp_host,
port=self.settings.smtp_port,
username=self.settings.smtp_username,
password=self.settings.smtp_password,
use_tls=self.settings.smtp_use_tls,
use_ssl=self.settings.smtp_use_ssl,
fail_silently=False, timeout=timeout)
else:
return get_connection(fail_silently=False)

View File

@@ -764,9 +764,6 @@ class ItemVariation(models.Model):
:type default_price: decimal.Decimal
:param original_price: The item's "original" price. Will not be used for any calculations, will just be shown.
:type original_price: decimal.Decimal
:param require_approval: If set to ``True``, orders containing this variation can only be processed and paid after
approval by an administrator
:type require_approval: bool
"""
item = models.ForeignKey(
Item,
@@ -802,13 +799,6 @@ class ItemVariation(models.Model):
help_text=_('If set, this will be displayed next to the current price to show that the current price is a '
'discounted one. This is just a cosmetic setting and will not actually impact pricing.')
)
require_approval = models.BooleanField(
verbose_name=_('Require approval'),
default=False,
help_text=_('If this variation is part of an order, the order will be put into an "approval" state and '
'will need to be confirmed by you before it can be paid and completed. You can use this e.g. for '
'discounted tickets that are only available to specific groups.'),
)
require_membership = models.BooleanField(
verbose_name=_('Require a valid membership'),
default=False,
@@ -842,7 +832,7 @@ class ItemVariation(models.Model):
blank=True,
)
hide_without_voucher = models.BooleanField(
verbose_name=_('Show only if a matching voucher is redeemed.'),
verbose_name=_('This variation will only be shown if a voucher matching the product is redeemed.'),
default=False,
help_text=_('This variation will be hidden from the event page until the user enters a voucher '
'that unlocks this variation.')

View File

@@ -950,7 +950,7 @@ class Order(LockModel, LoggedModel):
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
user: User=None, headers: dict=None, sender: str=None, invoices: list=None,
auth=None, attach_tickets=False, position: 'OrderPosition'=None, auto_email=True,
attach_ical=False, attach_other_files: list=None):
attach_ical=False):
"""
Sends an email to the user that placed this order. Basically, this method does two things:
@@ -994,8 +994,7 @@ class Order(LockModel, LoggedModel):
recipient, subject, template, context,
self.event, self.locale, self, headers=headers, sender=sender,
invoices=invoices, attach_tickets=attach_tickets,
position=position, auto_email=auto_email, attach_ical=attach_ical,
attach_other_files=attach_other_files,
position=position, auto_email=auto_email, attach_ical=attach_ical
)
except SendMailException:
raise
@@ -1442,15 +1441,6 @@ class AbstractPosition(models.Model):
lines = [r.strip() for r in lines if r]
return '\n'.join(lines).strip()
def requires_approval(self, invoice_address=None):
if self.item.require_approval:
return True
if self.variation and self.variation.require_approval:
return True
if self.item.tax_rule and self.item.tax_rule._require_approval(invoice_address):
return True
return False
class OrderPayment(models.Model):
"""
@@ -2326,7 +2316,7 @@ class OrderPosition(AbstractPosition):
def send_mail(self, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
user: User=None, headers: dict=None, sender: str=None, invoices: list=None,
auth=None, attach_tickets=False, attach_ical=False, attach_other_files: list=None):
auth=None, attach_tickets=False, attach_ical=False):
"""
Sends an email to the attendee. Basically, this method does two things:
@@ -2367,7 +2357,6 @@ class OrderPosition(AbstractPosition):
invoices=invoices,
attach_tickets=attach_tickets,
attach_ical=attach_ical,
attach_other_files=attach_other_files,
)
except SendMailException:
raise

View File

@@ -36,7 +36,6 @@ import string
from datetime import date, datetime, time
import pytz
from django.conf import settings
from django.core.mail import get_connection
from django.core.validators import MinLengthValidator, RegexValidator
from django.db import models
@@ -191,20 +190,21 @@ class Organizer(LoggedModel):
e.delete()
self.teams.all().delete()
def get_mail_backend(self, timeout=None):
def get_mail_backend(self, timeout=None, force_custom=False):
"""
Returns an email server connection, either by using the system-wide connection
or by returning a custom one based on the organizer's settings.
"""
if self.settings.smtp_use_custom:
return get_connection(backend=settings.EMAIL_CUSTOM_SMTP_BACKEND,
host=self.settings.smtp_host,
port=self.settings.smtp_port,
username=self.settings.smtp_username,
password=self.settings.smtp_password,
use_tls=self.settings.smtp_use_tls,
use_ssl=self.settings.smtp_use_ssl,
fail_silently=False, timeout=timeout)
from pretix.base.email import CustomSMTPBackend
if self.settings.smtp_use_custom or force_custom:
return CustomSMTPBackend(host=self.settings.smtp_host,
port=self.settings.smtp_port,
username=self.settings.smtp_username,
password=self.settings.smtp_password,
use_tls=self.settings.smtp_use_tls,
use_ssl=self.settings.smtp_use_ssl,
fail_silently=False, timeout=timeout)
else:
return get_connection(fail_silently=False)

View File

@@ -81,15 +81,6 @@ class TaxedPrice:
name=self.name,
)
def __eq__(self, other):
return (
self.gross == other.gross and
self.net == other.net and
self.tax == other.tax and
self.rate == other.rate and
self.name == other.name
)
TAXED_ZERO = TaxedPrice(
gross=Decimal('0.00'),
@@ -136,13 +127,8 @@ def cc_to_vat_prefix(country_code):
class TaxRule(LoggedModel):
event = models.ForeignKey('Event', related_name='tax_rules', on_delete=models.CASCADE)
internal_name = models.CharField(
verbose_name=_('Internal name'),
max_length=190,
null=True, blank=True,
)
name = I18nCharField(
verbose_name=_('Official name'),
verbose_name=_('Name'),
help_text=_('Should be short, e.g. "VAT"'),
max_length=190,
)
@@ -155,10 +141,6 @@ class TaxRule(LoggedModel):
verbose_name=_("The configured product prices include the tax amount"),
default=True,
)
keep_gross_if_rate_changes = models.BooleanField(
verbose_name=_("Keep gross amount constant if the tax rate changes based on the invoice address"),
default=False,
)
eu_reverse_charge = models.BooleanField(
verbose_name=_("Use EU reverse charge taxation rules"),
default=False,
@@ -216,8 +198,6 @@ class TaxRule(LoggedModel):
s = _('plus {rate}% {name}').format(rate=self.rate, name=self.name)
if self.eu_reverse_charge:
s += ' ({})'.format(_('reverse charge enabled'))
if self.internal_name:
return f'{self.internal_name} ({s})'
return str(s)
@property
@@ -231,7 +211,7 @@ class TaxRule(LoggedModel):
rule = self.get_matching_rule(invoice_address)
if rule.get('action', 'vat') == 'block':
raise self.SaleNotAllowed()
if rule.get('action', 'vat') in ('vat', 'require_approval') and rule.get('rate') is not None:
if rule.get('action', 'vat') == 'vat' and rule.get('rate') is not None:
return Decimal(rule.get('rate'))
return Decimal(self.rate)
@@ -248,19 +228,13 @@ class TaxRule(LoggedModel):
rate = override_tax_rate
elif invoice_address:
adjust_rate = self.tax_rate_for(invoice_address)
if (adjust_rate == gross_price_is_tax_rate or force_fixed_gross_price or self.keep_gross_if_rate_changes) and base_price_is == 'gross':
if (adjust_rate == gross_price_is_tax_rate or force_fixed_gross_price) and base_price_is == 'gross':
rate = adjust_rate
elif adjust_rate != rate:
if self.keep_gross_if_rate_changes:
normal_price = self.tax(base_price, base_price_is, currency, subtract_from_gross=subtract_from_gross)
base_price = normal_price.gross
base_price_is = 'gross'
subtract_from_gross = Decimal('0.00')
else:
normal_price = self.tax(base_price, base_price_is, currency, subtract_from_gross=subtract_from_gross)
base_price = normal_price.net
base_price_is = 'net'
subtract_from_gross = Decimal('0.00')
normal_price = self.tax(base_price, base_price_is, currency, subtract_from_gross=subtract_from_gross)
base_price = normal_price.net
base_price_is = 'net'
subtract_from_gross = Decimal('0.00')
rate = adjust_rate
if rate == Decimal('0.00'):
@@ -363,19 +337,12 @@ class TaxRule(LoggedModel):
return False
def _require_approval(self, invoice_address):
if self._custom_rules:
rule = self.get_matching_rule(invoice_address)
if rule.get('action', 'vat') == 'require_approval':
return True
return False
def _tax_applicable(self, invoice_address):
if self._custom_rules:
rule = self.get_matching_rule(invoice_address)
if rule.get('action', 'vat') == 'block':
raise self.SaleNotAllowed()
return rule.get('action', 'vat') in ('vat', 'require_approval')
return rule.get('action', 'vat') == 'vat'
if not self.eu_reverse_charge:
# No reverse charge rules? Always apply VAT!

View File

@@ -191,15 +191,6 @@ class BasePaymentProvider:
"""
return self.verbose_name
@property
def confirm_button_name(self) -> str:
"""
A label for the "confirm" button on the last page before a payment is started. This
is **not** used in the regular checkout flow, but only if the payment method is selected
for an existing order later on.
"""
return _("Pay now")
@property
def identifier(self) -> str:
"""

View File

@@ -273,11 +273,6 @@ class RelativeDateTimeField(forms.MultiValueField):
minutes_before=None
))
def has_changed(self, initial, data):
if initial is None:
initial = self.widget.decompress(initial)
return super().has_changed(initial, data)
def clean(self, value):
if value[0] == 'absolute' and not value[1]:
raise ValidationError(self.error_messages['incomplete'])

View File

@@ -426,10 +426,10 @@ class CartManager:
if not cp.includes_tax:
price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent,
cp_is_net=True, bundled_sum=bundled_sum)
price = TaxedPrice(net=price.net, gross=price.net, rate=Decimal('0'), tax=Decimal('0'), name='')
price = TaxedPrice(net=price.net, gross=price.net, rate=0, tax=0, name='')
pbv = self._get_price(cp.item, cp.variation, None, cp.price, cp.subevent,
cp_is_net=True, bundled_sum=bundled_sum)
pbv = TaxedPrice(net=pbv.net, gross=pbv.net, rate=Decimal('0'), tax=Decimal('0'), name='')
pbv = TaxedPrice(net=pbv.net, gross=pbv.net, rate=0, tax=0, name='')
else:
price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent,
bundled_sum=bundled_sum)
@@ -1106,11 +1106,10 @@ def update_tax_rates(event: Event, cart_id: str, invoice_address: InvoiceAddress
rate = pos.item.tax_rule.tax_rate_for(invoice_address)
if pos.tax_rate != rate:
if not pos.item.tax_rule.keep_gross_if_rate_changes:
current_net = pos.price - pos.tax_value
new_gross = pos.item.tax(current_net, base_price_is='net', invoice_address=invoice_address).gross
totaldiff += new_gross - pos.price
pos.price = new_gross
current_net = pos.price - pos.tax_value
new_gross = pos.item.tax(current_net, base_price_is='net', invoice_address=invoice_address).gross
totaldiff += new_gross - pos.price
pos.price = new_gross
pos.includes_tax = rate != Decimal('0.00')
pos.override_tax_rate = rate
pos.save(update_fields=['price', 'includes_tax', 'override_tax_rate'])

View File

@@ -102,7 +102,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
payment = ""
if invoice.event.settings.invoice_include_expire_date and invoice.order.status == Order.STATUS_PENDING:
if payment:
payment += "<br /><br />"
payment += "<br />"
payment += pgettext("invoice", "Please complete your payment before {expire_date}.").format(
expire_date=date_format(invoice.order.expires, "SHORT_DATE_FORMAT")
)

View File

@@ -35,7 +35,6 @@
import hashlib
import inspect
import logging
import mimetypes
import os
import re
import smtplib
@@ -52,7 +51,6 @@ from bs4 import BeautifulSoup
from celery import chain
from celery.exceptions import MaxRetriesExceededError
from django.conf import settings
from django.core.files.storage import default_storage
from django.core.mail import (
EmailMultiAlternatives, SafeMIMEMultipart, get_connection,
)
@@ -75,9 +73,8 @@ from pretix.base.services.tasks import TransactionAwareTask
from pretix.base.services.tickets import get_tickets_for_order
from pretix.base.signals import email_filter, global_email_filter
from pretix.celery_app import app
from pretix.helpers.hierarkey import clean_filename
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.ical import get_private_icals
from pretix.presale.ical import get_ical
logger = logging.getLogger('pretix.base.mail')
INVALID_ADDRESS = 'invalid-pretix-mail-address'
@@ -97,7 +94,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
context: Dict[str, Any] = None, event: Event = None, locale: str = None, order: Order = None,
position: OrderPosition = None, *, headers: dict = None, sender: str = None, organizer: Organizer = None,
customer: Customer = None, invoices: Sequence = None, attach_tickets=False, auto_email=True, user=None,
attach_ical=False, attach_cached_files: Sequence = None, attach_other_files: list=None):
attach_ical=False, attach_cached_files: Sequence = None):
"""
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
@@ -145,8 +142,6 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
:param attach_cached_files: A list of cached file to attach to this email.
:param attach_other_files: A list of file paths on our storage to attach.
:raises MailOrderException: on obvious, immediate failures. Not raising an exception does not necessarily mean
that the email has been sent, just that it has been queued by the email backend.
"""
@@ -217,8 +212,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
for bcc_mail in settings_holder.settings.mail_bcc.split(','):
bcc.append(bcc_mail.strip())
if settings_holder.settings.mail_from not in (settings.DEFAULT_FROM_EMAIL, settings.MAIL_FROM_ORGANIZERS) \
and settings_holder.settings.contact_mail and not headers.get('Reply-To'):
if settings_holder.settings.mail_from == settings.DEFAULT_FROM_EMAIL and settings_holder.settings.contact_mail and not headers.get('Reply-To'):
headers['Reply-To'] = settings_holder.settings.contact_mail
prefix = settings_holder.settings.get('mail_prefix')
@@ -307,7 +301,6 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
organizer=organizer.pk if organizer else None,
customer=customer.pk if customer else None,
attach_cached_files=[(cf.id if isinstance(cf, CachedFile) else cf) for cf in attach_cached_files] if attach_cached_files else [],
attach_other_files=attach_other_files,
)
if invoices:
@@ -345,8 +338,7 @@ class CustomEmail(EmailMultiAlternatives):
def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str,
event: int = None, position: int = None, headers: dict = None, bcc: List[str] = None,
invoices: List[int] = None, order: int = None, attach_tickets=False, user=None,
organizer=None, customer=None, attach_ical=False, attach_cached_files: List[int] = None,
attach_other_files: List[str] = None) -> bool:
organizer=None, customer=None, attach_ical=False, attach_cached_files: List[int] = None) -> bool:
email = CustomEmail(subject, body, sender, to=to, bcc=bcc, headers=headers)
if html is not None:
html_message = SafeMIMEMultipart(_subtype='related', encoding=settings.DEFAULT_CHARSET)
@@ -430,7 +422,18 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
}
)
if attach_ical:
for i, cal in enumerate(get_private_icals(event, [position] if position else order.positions.all())):
ical_events = set()
if event.has_subevents:
if position:
ical_events.add(position.subevent)
else:
for p in order.positions.all():
ical_events.add(p.subevent)
else:
ical_events.add(order.event)
for i, e in enumerate(ical_events):
cal = get_ical([e])
email.attach('event-{}.ics'.format(i), cal.serialize(), 'text/calendar')
email = email_filter.send_chained(event, 'message', message=email, order=order, user=user)
@@ -452,20 +455,6 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
logger.exception('Could not attach invoice to email')
pass
if attach_other_files:
for fname in attach_other_files:
ftype, _ = mimetypes.guess_type(fname)
data = default_storage.open(fname).read()
try:
email.attach(
clean_filename(os.path.basename(fname)),
data,
ftype
)
except:
logger.exception('Could not attach file to email')
pass
if attach_cached_files:
for cf in CachedFile.objects.filter(id__in=attach_cached_files):
if cf.file:

View File

@@ -148,7 +148,7 @@ def send_notification_mail(notification: Notification, user: User):
),
'body': body_plain,
'html': body_html,
'sender': settings.MAIL_FROM_NOTIFICATIONS,
'sender': settings.MAIL_FROM,
'headers': {},
'user': user.pk
})

View File

@@ -700,7 +700,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
invoice_address=address, force_custom_price=True, max_discount=max_discount)
changed_prices[cp.pk] = bprice
else:
bundled_sum = Decimal('0.00')
bundled_sum = 0
if not cp.addon_to_id:
for bundledp in cp.addons.all():
if bundledp.is_bundled:
@@ -856,7 +856,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
total=total,
testmode=True if sales_channel.testmode_supported and event.testmode else False,
meta_info=json.dumps(meta_info or {}),
require_approval=any(p.requires_approval(invoice_address=address) for p in positions),
require_approval=any(p.item.require_approval for p in positions),
sales_channel=sales_channel.identifier,
customer=customer,
)
@@ -932,7 +932,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
def _order_placed_email(event: Event, order: Order, pprov: BasePaymentProvider, email_template, log_entry: str,
invoice, payment: OrderPayment, is_free=False):
invoice, payment: OrderPayment):
email_context = get_email_context(event=event, order=order, payment=payment if pprov else None)
email_subject = _('Your order: %(code)s') % {'code': order.code}
try:
@@ -941,16 +941,13 @@ def _order_placed_email(event: Event, order: Order, pprov: BasePaymentProvider,
log_entry,
invoices=[invoice] if invoice and event.settings.invoice_email_attachment else [],
attach_tickets=True,
attach_ical=event.settings.mail_attach_ical and (not event.settings.mail_attach_ical_paid_only or is_free),
attach_other_files=[a for a in [
event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):]
] if a],
attach_ical=event.settings.mail_attach_ical
)
except SendMailException:
logger.exception('Order received email could not be sent')
def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosition, email_template, log_entry: str, is_free=False):
def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosition, email_template, log_entry: str):
email_context = get_email_context(event=event, order=order, position=position)
email_subject = _('Your event registration: %(code)s') % {'code': order.code}
@@ -961,10 +958,7 @@ def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosi
invoices=[],
attach_tickets=True,
position=position,
attach_ical=event.settings.mail_attach_ical and (not event.settings.mail_attach_ical_paid_only or is_free),
attach_other_files=[a for a in [
event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):]
] if a],
attach_ical=event.settings.mail_attach_ical
)
except SendMailException:
logger.exception('Order received email could not be sent to attendee')
@@ -1070,13 +1064,11 @@ def _perform_order(event: Event, payment_provider: str, position_ids: List[str],
email_attendees_template = event.settings.mail_text_order_placed_attendee
if sales_channel in event.settings.mail_sales_channel_placed_paid:
_order_placed_email(event, order, pprov, email_template, log_entry, invoice, payment,
is_free=free_order_flow)
_order_placed_email(event, order, pprov, email_template, log_entry, invoice, payment)
if email_attendees:
for p in order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
_order_placed_email_attendee(event, order, p, email_attendees_template, log_entry,
is_free=free_order_flow)
_order_placed_email_attendee(event, order, p, email_attendees_template, log_entry)
return order.id
@@ -2073,7 +2065,7 @@ class OrderChangeManager:
split_order.code = None
split_order.datetime = now()
split_order.secret = generate_secret()
split_order.require_approval = self.order.require_approval and any(p.requires_approval(invoice_address=self._invoice_address) for p in split_positions)
split_order.require_approval = self.order.require_approval and any(p.item.require_approval for p in split_positions)
split_order.save()
split_order.log_action('pretix.event.order.changed.split_from', user=self.user, auth=self.auth, data={
'original_order': self.order.code
@@ -2325,10 +2317,10 @@ class OrderChangeManager:
except TaxRule.SaleNotAllowed:
raise OrderError(self.error_messages['tax_rule_country_blocked'])
self._recalculate_total_and_payment_fee()
self._check_paid_price_change()
self._check_paid_to_free()
if self.order.status in (Order.STATUS_PENDING, Order.STATUS_PAID):
self._reissue_invoice()
self._check_paid_price_change()
self._check_paid_to_free()
self._clear_tickets_cache()
self.order.touch()
self.order.create_transactions()

View File

@@ -113,8 +113,10 @@ class QuotaAvailability:
be a few minutes outdated. In this case, you may not rely on the results in the ``count_*`` properties.
"""
now_dt = now_dt or now()
quota_ids_set = {q.id for q in self._queue}
if not quota_ids_set:
quotas = list(set(self._queue))
quotas_original = list(self._queue)
self._queue.clear()
if not quotas:
return
if allow_cache:
@@ -127,7 +129,7 @@ class QuotaAvailability:
elif settings.HAS_REDIS:
rc = get_redis_connection("redis")
quotas_by_event = defaultdict(list)
for q in [_q for _q in self._queue if _q.id in quota_ids_set]:
for q in quotas_original:
quotas_by_event[q.event_id].append(q)
for eventid, evquotas in quotas_by_event.items():
@@ -137,19 +139,16 @@ class QuotaAvailability:
data = [rv for rv in redisval.decode().split(',')]
# Except for some rare situations, we don't want to use cache entries older than 2 minutes
if time.time() - int(data[2]) < 120 or allow_cache_stale:
quota_ids_set.remove(q.id)
quotas_original.remove(q)
quotas.remove(q)
if data[1] == "None":
self.results[q] = int(data[0]), None
else:
self.results[q] = int(data[0]), int(data[1])
if not quota_ids_set:
if not quotas:
return
quotas = [_q for _q in self._queue if _q.id in quota_ids_set]
quotas_original = list(quotas)
self._queue.clear()
self._compute(quotas, now_dt)
for q in quotas_original:
@@ -285,16 +284,15 @@ class QuotaAvailability:
seq = Q(subevent_id__in=subevents)
if None in subevents:
seq |= Q(subevent__isnull=True)
quota_ids = {q.pk for q in quotas}
op_lookup = OrderPosition.objects.filter(
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING],
order__event_id__in=events,
).filter(seq).filter(
Q(
Q(variation_id__isnull=True) &
Q(item_id__in={i['item_id'] for i in q_items if i['quota_id'] in quota_ids})
Q(item_id__in={i['item_id'] for i in q_items if self._quota_objects[i['quota_id']] in quotas})
) | Q(
variation_id__in={i['itemvariation_id'] for i in q_vars if i['quota_id'] in quota_ids})
variation_id__in={i['itemvariation_id'] for i in q_vars if self._quota_objects[i['quota_id']] in quotas})
).order_by()
if any(q.release_after_exit for q in quotas):
op_lookup = op_lookup.annotate(
@@ -361,7 +359,6 @@ class QuotaAvailability:
func = 'GREATEST'
subevents = {q.subevent_id for q in quotas}
quota_ids = {q.pk for q in quotas}
seq = Q(subevent_id__in=subevents)
if None in subevents:
seq |= Q(subevent__isnull=True)
@@ -373,9 +370,10 @@ class QuotaAvailability:
Q(
Q(
Q(variation_id__isnull=True) &
Q(item_id__in={i['item_id'] for i in q_items if i['quota_id'] in quota_ids})
Q(item_id__in={i['item_id'] for i in q_items if self._quota_objects[i['quota_id']] in quotas})
) | Q(
variation_id__in={i['itemvariation_id'] for i in q_vars if i['quota_id'] in quota_ids}
variation_id__in={i['itemvariation_id'] for i in q_vars if
self._quota_objects[i['quota_id']] in quotas}
) | Q(
quota_id__in=[q.pk for q in quotas]
)
@@ -400,7 +398,6 @@ class QuotaAvailability:
def _compute_carts(self, quotas, q_items, q_vars, size_left, now_dt):
events = {q.event_id for q in quotas}
subevents = {q.subevent_id for q in quotas}
quota_ids = {q.pk for q in quotas}
seq = Q(subevent_id__in=subevents)
if None in subevents:
seq |= Q(subevent__isnull=True)
@@ -416,9 +413,9 @@ class QuotaAvailability:
Q(
Q(
Q(variation_id__isnull=True) &
Q(item_id__in={i['item_id'] for i in q_items if i['quota_id'] in quota_ids})
Q(item_id__in={i['item_id'] for i in q_items if self._quota_objects[i['quota_id']] in quotas})
) | Q(
variation_id__in={i['itemvariation_id'] for i in q_vars if i['quota_id'] in quota_ids}
variation_id__in={i['itemvariation_id'] for i in q_vars if self._quota_objects[i['quota_id']] in quotas}
)
)
).order_by().values('item_id', 'subevent_id', 'variation_id').annotate(c=Count('*'))
@@ -437,7 +434,6 @@ class QuotaAvailability:
def _compute_waitinglist(self, quotas, q_items, q_vars, size_left):
events = {q.event_id for q in quotas}
subevents = {q.subevent_id for q in quotas}
quota_ids = {q.pk for q in quotas}
seq = Q(subevent_id__in=subevents)
if None in subevents:
seq |= Q(subevent__isnull=True)
@@ -448,8 +444,9 @@ class QuotaAvailability:
Q(
Q(
Q(variation_id__isnull=True) &
Q(item_id__in={i['item_id'] for i in q_items if i['quota_id'] in quota_ids})
) | Q(variation_id__in={i['itemvariation_id'] for i in q_vars if i['quota_id'] in quota_ids})
Q(item_id__in={i['item_id'] for i in q_items if self._quota_objects[i['quota_id']] in quotas})
) | Q(variation_id__in={i['itemvariation_id'] for i in q_vars if
self._quota_objects[i['quota_id']] in quotas})
)
).order_by().values('item_id', 'subevent_id', 'variation_id').annotate(c=Count('*'))
for line in w_lookup:

View File

@@ -136,15 +136,11 @@ DEFAULTS = {
'type': int,
'form_class': forms.IntegerField,
'serializer_class': serializers.IntegerField,
'serializer_kwargs': dict(
min_value=1,
),
'form_kwargs': dict(
min_value=1,
required=True,
label=_("Maximum number of items per order"),
help_text=_("Add-on products will not be counted.")
),
)
},
'display_net_prices': {
'default': 'False',
@@ -372,12 +368,11 @@ DEFAULTS = {
'form_class': I18nFormField,
'serializer_class': I18nField,
'form_kwargs': dict(
label=_("Custom recipient field"),
label=_("Custom address field"),
widget=I18nTextInput,
help_text=_("If you want to add a custom text field, e.g. for a country-specific registration number, to "
"your invoice address form, please fill in the label here. This label will both be used for "
"asking the user to input their details as well as for displaying the value on the invoice. It will "
"be shown on the invoice below the headline. "
"asking the user to input their details as well as for displaying the value on the invoice. "
"The field will not be required.")
)
},
@@ -445,11 +440,9 @@ DEFAULTS = {
'type': int,
'form_class': forms.IntegerField,
'serializer_class': serializers.IntegerField,
'serializer_kwargs': dict(),
'form_kwargs': dict(
label=_("Minimum length of invoice number after prefix"),
help_text=_("The part of your invoice number after your prefix will be filled up with leading zeros up to this length, e.g. INV-001 or INV-00001."),
required=True,
)
},
'invoice_numbers_consecutive': {
@@ -513,7 +506,6 @@ DEFAULTS = {
MinValueValidator(12),
MaxValueValidator(64),
],
required=True,
widget=forms.NumberInput(
attrs={
'min': '12',
@@ -528,13 +520,9 @@ DEFAULTS = {
'type': int,
'form_class': forms.IntegerField,
'serializer_class': serializers.IntegerField,
'serializer_kwargs': dict(
min_value=0,
),
'form_kwargs': dict(
min_value=0,
label=_("Reservation period"),
required=True,
help_text=_("The number of minutes the items in a user's cart are reserved for this user."),
)
},
@@ -589,7 +577,6 @@ DEFAULTS = {
'form_kwargs': dict(
label=_("Set payment term"),
widget=forms.RadioSelect,
required=True,
choices=(
('days', _("in days")),
('minutes', _("in minutes"))
@@ -1104,13 +1091,9 @@ DEFAULTS = {
'type': int,
'serializer_class': serializers.IntegerField,
'form_class': forms.IntegerField,
'serializer_kwargs': dict(
min_value=1,
),
'form_kwargs': dict(
label=_("Waiting list response time"),
min_value=1,
required=True,
help_text=_("If a ticket voucher is sent to a person on the waiting list, it has to be redeemed within this "
"number of hours until it expires and can be re-assigned to the next person on the list."),
widget=forms.NumberInput(),
@@ -1573,32 +1556,6 @@ DEFAULTS = {
help_text=_("If enabled, we will attach an .ics calendar file to order confirmation emails."),
)
},
'mail_attach_ical_paid_only': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Attach calendar files only after order has been paid"),
help_text=_("Use this if you e.g. put a private access link into the calendar file to make sure people only "
"receive it after their payment was confirmed."),
)
},
'mail_attach_ical_description': {
'default': '',
'type': LazyI18nString,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Event description"),
widget=I18nTextarea,
help_text=_(
"You can use this to share information with your attendees, such as travel information or the link to a digital event. "
"If you keep it empty, we will put a link to the event shop, the admission time, and your organizer name in there. "
"We do not allow using placeholders with sensitive person-specific data as calendar entries are often shared with an "
"unspecified number of people."
),
)
},
'mail_prefix': {
'default': None,
'type': str,
@@ -1615,7 +1572,7 @@ DEFAULTS = {
'type': str
},
'mail_from': {
'default': settings.MAIL_FROM_ORGANIZERS,
'default': settings.MAIL_FROM,
'type': str,
'form_class': forms.EmailField,
'serializer_class': serializers.EmailField,
@@ -1730,30 +1687,6 @@ You can change your order details and view the status of your order at
Best regards,
Your {event} team"""))
},
'mail_attachment_new_order': {
'default': None,
'type': File,
'form_class': ExtFileField,
'form_kwargs': dict(
label=_('Attachment for new orders'),
ext_whitelist=(".pdf",),
max_size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT,
help_text=_('This file will be attached to the first email that we send for every new order. Therefore it will be '
'combined with the "Placed order", "Free order", or "Received order" texts from above. It will be sent '
'to both order contacts and attendees. You can use this e.g. to send your terms of service. Do not use '
'it to send non-public information as this file might be sent before payment is confirmed or the order '
'is approved. To avoid this vital email going to spam, you can only upload PDF files of up to {size} MB.').format(
size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT // (1024 * 1024),
)
),
'serializer_class': UploadedFileField,
'serializer_kwargs': dict(
allowed_types=[
'application/pdf'
],
max_size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT,
)
},
'mail_send_order_placed_attendee': {
'type': bool,
'default': 'False'

View File

@@ -85,6 +85,9 @@
-webkit-hyphens: auto;
hyphens: auto;
}
p:last-child {
margin-bottom: 0;
}
.footer {
padding: 10px;
@@ -113,9 +116,6 @@
width: 100%;
height: auto;
}
.content {
text-align: left;
}
.content table {
width: 100%;
@@ -142,8 +142,7 @@
}
.order-button {
padding-top: 5px;
text-align: center;
padding-top: 5px
}
.order-button a.button {
font-size: 12px;
@@ -174,7 +173,7 @@
body {
direction: rtl;
}
.content {
.content table td {
text-align: right;
}
{% endif %}

View File

@@ -1,6 +1,5 @@
{% load eventurl %}
{% load i18n %}
{% load oneline %}
{% if position %}
<div class="order-info">
@@ -108,10 +107,6 @@
{% if event.settings.show_times %}
{{ groupkey.2.date_from|date:"TIME_FORMAT" }}
{% endif %}
{% if groupkey.2.location %}
<br>
{{ groupkey.2.location|oneline }}
{% endif %}
{% endif %}
{% if groupkey.3 %} {# attendee name #}
<br>

View File

@@ -104,6 +104,9 @@
-webkit-hyphens: auto;
hyphens: auto;
}
p:last-child {
margin-bottom: 0;
}
.footer {
padding: 10px;
@@ -133,10 +136,6 @@
display: block;
}
.content {
text-align: left;
}
.content table {
width: 100%;
}
@@ -198,7 +197,7 @@
body {
direction: rtl;
}
.content {
.content table td {
text-align: right;
}
{% endif %}

View File

@@ -1,26 +0,0 @@
{% extends "error.html" %}
{% load i18n %}
{% load rich_text %}
{% load static %}
{% block title %}{% trans "Redirect" %}{% endblock %}
{% block content %}
<i class="fa fa-link fa-fw big-icon"></i>
<div class="error-details">
<h1>{% trans "Redirect" %}</h1>
<h3>
{% blocktrans trimmed with host="<strong>"|add:hostname|add:"</strong>"|safe %}
The link you clicked on wants to redirect you to a destination on the website {{ host }}.
{% endblocktrans %}
{% blocktrans trimmed %}
Please only proceed if you trust this website to be safe.
{% endblocktrans %}
</h3>
<p>
<a href="{{ url }}" class="btn btn-primary btn-lg">
{% blocktrans trimmed with host=hostname %}
Proceed to {{ host }}
{% endblocktrans %}
</a>
</p>
</div>
{% endblock %}

View File

@@ -1,31 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django import template
register = template.Library()
@register.filter
def oneline(value):
if not value:
return ''
return ', '.join([l.strip() for l in str(value).splitlines() if l and l.strip()])

View File

@@ -138,7 +138,7 @@ def truelink_callback(attrs, new=False):
text = re.sub(r'[^a-zA-Z0-9.\-/_ ]', '', attrs.get('_text')) # clean up link text
url = attrs.get((None, 'href'), '/')
href_url = urllib.parse.urlparse(url)
if (None, 'href') in attrs and URL_RE.match(text) and href_url.scheme not in ('tel', 'mailto'):
if URL_RE.match(text) and href_url.scheme not in ('tel', 'mailto'):
# link text looks like a url
if text.startswith('//'):
text = 'https:' + text
@@ -157,8 +157,6 @@ def abslink_callback(attrs, new=False):
Makes sure that all links will be absolute links and will be opened in a new page with no
window.opener attribute.
"""
if (None, 'href') not in attrs:
return attrs
url = attrs.get((None, 'href'), '/')
if not url.startswith('mailto:') and not url.startswith('tel:'):
attrs[None, 'href'] = urllib.parse.urljoin(settings.SITE_URL, url)

View File

@@ -30,85 +30,67 @@ from django.utils.translation import gettext as _
from django.views.decorators.csrf import requires_csrf_token
from sentry_sdk import last_event_id
from pretix.base.i18n import language
from pretix.base.middleware import get_language_from_request
def csrf_failure(request, reason=""):
try:
locale = get_language_from_request(request)
except:
locale = "en"
with language(locale): # Middleware might not have run, need to do this manually
t = get_template('csrffail.html')
c = {
'reason': reason,
'no_referer': reason == REASON_NO_REFERER,
'no_referer1': _(
"You are seeing this message because this HTTPS site requires a "
"'Referer header' to be sent by your Web browser, but none was "
"sent. This header is required for security reasons, to ensure "
"that your browser is not being hijacked by third parties."),
'no_referer2': _(
"If you have configured your browser to disable 'Referer' headers, "
"please re-enable them, at least for this site, or for HTTPS "
"connections, or for 'same-origin' requests."),
'no_cookie': reason == REASON_NO_CSRF_COOKIE,
'no_cookie1': _(
"You are seeing this message because this site requires a CSRF "
"cookie when submitting forms. This cookie is required for "
"security reasons, to ensure that your browser is not being "
"hijacked by third parties."),
'no_cookie2': _(
"If you have configured your browser to disable cookies, please "
"re-enable them, at least for this site, or for 'same-origin' "
"requests."),
}
return HttpResponseForbidden(t.render(c), content_type='text/html')
t = get_template('csrffail.html')
c = {
'reason': reason,
'no_referer': reason == REASON_NO_REFERER,
'no_referer1': _(
"You are seeing this message because this HTTPS site requires a "
"'Referer header' to be sent by your Web browser, but none was "
"sent. This header is required for security reasons, to ensure "
"that your browser is not being hijacked by third parties."),
'no_referer2': _(
"If you have configured your browser to disable 'Referer' headers, "
"please re-enable them, at least for this site, or for HTTPS "
"connections, or for 'same-origin' requests."),
'no_cookie': reason == REASON_NO_CSRF_COOKIE,
'no_cookie1': _(
"You are seeing this message because this site requires a CSRF "
"cookie when submitting forms. This cookie is required for "
"security reasons, to ensure that your browser is not being "
"hijacked by third parties."),
'no_cookie2': _(
"If you have configured your browser to disable cookies, please "
"re-enable them, at least for this site, or for 'same-origin' "
"requests."),
}
return HttpResponseForbidden(t.render(c), content_type='text/html')
@requires_csrf_token
def page_not_found(request, exception):
exception_repr = exception.__class__.__name__
# Try to get an "interesting" exception message, if any (and not the ugly
# Resolver404 dictionary)
try:
locale = get_language_from_request(request)
except:
locale = "en"
with language(locale): # Middleware might not have run, need to do this manually
exception_repr = exception.__class__.__name__
# Try to get an "interesting" exception message, if any (and not the ugly
# Resolver404 dictionary)
try:
message = exception.args[0]
except (AttributeError, IndexError):
pass
else:
if isinstance(message, (str, Promise)):
exception_repr = str(message)
context = {
'request_path': request.path,
'exception': exception_repr,
}
template = get_template('404.html')
body = template.render(context, request)
r = HttpResponseNotFound(body)
r.xframe_options_exempt = True
return r
message = exception.args[0]
except (AttributeError, IndexError):
pass
else:
if isinstance(message, (str, Promise)):
exception_repr = str(message)
context = {
'request_path': request.path,
'exception': exception_repr,
}
template = get_template('404.html')
body = template.render(context, request)
r = HttpResponseNotFound(body)
r.xframe_options_exempt = True
return r
@requires_csrf_token
def server_error(request):
try:
locale = get_language_from_request(request)
except:
locale = "en"
with language(locale): # Middleware might not have run, need to do this manually
try:
template = loader.get_template('500.html')
except TemplateDoesNotExist:
return HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html')
r = HttpResponseServerError(template.render({
'request': request,
'sentry_event_id': last_event_id(),
}))
r.xframe_options_exempt = True
return r
template = loader.get_template('500.html')
except TemplateDoesNotExist:
return HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html')
r = HttpResponseServerError(template.render({
'request': request,
'sentry_event_id': last_event_id(),
}))
r.xframe_options_exempt = True
return r

View File

@@ -355,29 +355,20 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
override[k].pop('initial', None)
return override_sets
@cached_property
def vat_id_validation_enabled(self):
return any([p.item.tax_rule and (p.item.tax_rule.eu_reverse_charge or p.item.tax_rule.custom_rules)
for p in self.positions])
@cached_property
def invoice_form(self):
if not self.address_asked and self.request.event.settings.invoice_name_required:
f = self.invoice_name_form_class(
data=self.request.POST if self.request.method == "POST" else None,
event=self.request.event,
instance=self.invoice_address,
validate_vat_id=False,
request=self.request,
instance=self.invoice_address, validate_vat_id=False,
all_optional=self.all_optional
)
elif self.address_asked:
f = self.invoice_form_class(
data=self.request.POST if self.request.method == "POST" else None,
event=self.request.event,
instance=self.invoice_address,
validate_vat_id=self.vat_id_validation_enabled,
request=self.request,
instance=self.invoice_address, validate_vat_id=False,
all_optional=self.all_optional,
)
else:

View File

@@ -23,38 +23,15 @@ import urllib.parse
from django.core import signing
from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
def _is_samesite_referer(request):
referer = request.META.get('HTTP_REFERER')
if referer is None:
return False
referer = urllib.parse.urlparse(referer)
# Make sure we have a valid URL for Referer.
if '' in (referer.scheme, referer.netloc):
return False
return (referer.scheme, referer.netloc) == (request.scheme, request.get_host())
def redir_view(request):
signer = signing.Signer(salt='safe-redirect')
try:
url = signer.unsign(request.GET.get('url', ''))
except signing.BadSignature:
return HttpResponseBadRequest('Invalid parameter')
if not _is_samesite_referer(request):
u = urllib.parse.urlparse(url)
return render(request, 'pretixbase/redirect.html', {
'hostname': u.hostname,
'url': url,
})
r = HttpResponseRedirect(url)
r['X-Robots-Tag'] = 'noindex'
return r

View File

@@ -48,7 +48,7 @@ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_scopes.forms import SafeModelMultipleChoiceField
from ...base.forms import I18nModelForm
from ...base.forms import I18nModelForm, SecretKeySettingsField
# Import for backwards compatibility with okd import paths
from ...base.forms.widgets import ( # noqa
@@ -373,6 +373,49 @@ class FontSelect(forms.RadioSelect):
option_template_name = 'pretixcontrol/font_option.html'
class SMTPSettingsMixin(forms.Form):
smtp_use_custom = forms.BooleanField(
label=_("Use custom SMTP server"),
help_text=_("All mail related to your event will be sent over the smtp server specified by you."),
required=False
)
smtp_host = forms.CharField(
label=_("Hostname"),
required=False,
widget=forms.TextInput(attrs={'placeholder': 'mail.example.org'})
)
smtp_port = forms.IntegerField(
label=_("Port"),
required=False,
widget=forms.TextInput(attrs={'placeholder': 'e.g. 587, 465, 25, ...'})
)
smtp_username = forms.CharField(
label=_("Username"),
widget=forms.TextInput(attrs={'placeholder': 'myuser@example.org'}),
required=False
)
smtp_password = SecretKeySettingsField(
label=_("Password"),
required=False,
)
smtp_use_tls = forms.BooleanField(
label=_("Use STARTTLS"),
help_text=_("Commonly enabled on port 587."),
required=False
)
smtp_use_ssl = forms.BooleanField(
label=_("Use SSL"),
help_text=_("Commonly enabled on port 465."),
required=False
)
def clean(self):
data = super().clean()
if data.get('smtp_use_tls') and data.get('smtp_use_ssl'):
raise ValidationError(_('You can activate either SSL or STARTTLS security, but not both at the same time.'))
return data
class ItemMultipleChoiceField(SafeModelMultipleChoiceField):
def label_from_instance(self, obj):
return str(obj) if obj.active else mark_safe(f'<strike class="text-muted">{escape(obj)}</strike>')

View File

@@ -43,7 +43,6 @@ from django.core.validators import validate_email
from django.db.models import Prefetch, Q, prefetch_related_objects
from django.forms import CheckboxSelectMultiple, formset_factory
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.timezone import get_current_timezone_name
@@ -64,7 +63,7 @@ from pretix.base.settings import (
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_event_settings,
)
from pretix.control.forms import (
MultipleLanguagesWidget, SlugWidget, SplitDateTimeField,
MultipleLanguagesWidget, SlugWidget, SMTPSettingsMixin, SplitDateTimeField,
SplitDateTimePickerWidget,
)
from pretix.control.forms.widgets import Select2
@@ -535,39 +534,34 @@ class EventSettingsForm(SettingsForm):
'og_image',
]
def _resolve_virtual_keys_input(self, data, prefix=''):
# set all dependants of virtual_keys and
# delete all virtual_fields to prevent them from being saved
for virtual_key in self.virtual_keys:
if prefix + virtual_key not in data:
continue
base_key = prefix + virtual_key.rsplit('_', 2)[0]
asked_key = base_key + '_asked'
required_key = base_key + '_required'
if data[prefix + virtual_key] == 'optional':
data[asked_key] = True
data[required_key] = False
elif data[prefix + virtual_key] == 'required':
data[asked_key] = True
data[required_key] = True
# Explicitly check for 'do_not_ask'.
# Do not overwrite as default-behaviour when no value for virtual field is transmitted!
elif data[prefix + virtual_key] == 'do_not_ask':
data[asked_key] = False
data[required_key] = False
# hierarkey.forms cannot handle non-existent keys in cleaned_data => do not delete, but set to None
if not prefix:
data[virtual_key] = None
return data
def clean(self):
data = super().clean()
settings_dict = self.event.settings.freeze()
settings_dict.update(data)
data = self._resolve_virtual_keys_input(data)
# set all dependants of virtual_keys and
# delete all virtual_fields to prevent them from being saved
for virtual_key in self.virtual_keys:
if virtual_key not in data:
continue
base_key = virtual_key.rsplit('_', 2)[0]
asked_key = base_key + '_asked'
required_key = base_key + '_required'
if data[virtual_key] == 'optional':
data[asked_key] = True
data[required_key] = False
elif data[virtual_key] == 'required':
data[asked_key] = True
data[required_key] = True
# Explicitly check for 'do_not_ask'.
# Do not overwrite as default-behaviour when no value for virtual field is transmitted!
elif data[virtual_key] == 'do_not_ask':
data[asked_key] = False
data[required_key] = False
# hierarkey.forms cannot handle non-existent keys in cleaned_data => do not delete, but set to None
data[virtual_key] = None
validate_event_settings(self.event, data)
return data
@@ -627,35 +621,6 @@ class EventSettingsForm(SettingsForm):
else:
self.initial[virtual_key] = 'do_not_ask'
@cached_property
def changed_data(self):
data = []
# We need to resolve the mapping between our "virtual" fields and the "real"fields here, otherwise
# they are detected as "changed" on every save even though they aren't.
in_data = self._resolve_virtual_keys_input(self.data.copy(), prefix=f'{self.prefix}-' if self.prefix else '')
for name, field in self.fields.items():
prefixed_name = self.add_prefix(name)
data_value = field.widget.value_from_datadict(in_data, self.files, prefixed_name)
if not field.show_hidden_initial:
# Use the BoundField's initial as this is the value passed to
# the widget.
initial_value = self[name].initial
else:
initial_prefixed_name = self.add_initial_prefix(name)
hidden_widget = field.hidden_widget()
try:
initial_value = field.to_python(hidden_widget.value_from_datadict(
self.data, self.files, initial_prefixed_name))
except ValidationError:
# Always assume data has changed if validation fails.
data.append(name)
continue
if field.has_changed(initial_value, data_value):
data.append(name)
return data
class CancelSettingsForm(SettingsForm):
auto_fields = [
@@ -865,15 +830,13 @@ def contains_web_channel_validate(val):
raise ValidationError(_("The online shop must be selected to receive these emails."))
class MailSettingsForm(SettingsForm):
class MailSettingsForm(SMTPSettingsMixin, SettingsForm):
auto_fields = [
'mail_prefix',
'mail_from',
'mail_from_name',
'mail_attach_ical',
'mail_attach_tickets',
'mail_attachment_new_order',
'mail_attach_ical_paid_only',
'mail_attach_ical_description',
]
mail_sales_channel_placed_paid = forms.MultipleChoiceField(
@@ -1081,8 +1044,7 @@ class MailSettingsForm(SettingsForm):
'mail_text_download_reminder_attendee': ['event', 'order', 'position'],
'mail_text_resend_link': ['event', 'order'],
'mail_text_waiting_list': ['event', 'waiting_list_entry'],
'mail_text_resend_all_links': ['event', 'orders'],
'mail_attach_ical_description': ['event', 'event_or_subevent'],
'mail_text_resend_all_links': ['event', 'orders']
}
def _set_field_placeholders(self, fn, base_parameters):
@@ -1217,7 +1179,6 @@ class TaxRuleLineForm(I18nForm):
('reverse', _('Reverse charge')),
('no', _('No VAT')),
('block', _('Sale not allowed')),
('require_approval', _('Order requires approval')),
],
)
rate = forms.DecimalField(
@@ -1251,7 +1212,7 @@ TaxRuleLineFormSet = formset_factory(
class TaxRuleForm(I18nModelForm):
class Meta:
model = TaxRule
fields = ['name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country', 'internal_name', 'keep_gross_if_rate_changes']
fields = ['name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country']
class WidgetCodeForm(forms.Form):

View File

@@ -811,213 +811,6 @@ class OrderSearchFilterForm(OrderFilterForm):
)
class OrderPaymentSearchFilterForm(forms.Form):
orders = {'id': 'id', 'local_id': 'local_id', 'state': 'state', 'amount': 'amount', 'order': 'order',
'created': 'created', 'payment_date': 'payment_date', 'provider': 'provider', 'info': 'info',
'fee': 'fee'}
query = forms.CharField(
label=_('Search for…'),
widget=forms.TextInput(attrs={
'placeholder': _('Search for…'),
'autofocus': 'autofocus'
}),
required=False,
)
event = forms.ModelChoiceField(
label=_('Event'),
queryset=Event.objects.none(),
required=False,
widget=Select2(
attrs={
'data-model-select2': 'event',
'data-select2-url': reverse_lazy('control:events.typeahead'),
'data-placeholder': _('All events')
}
)
)
organizer = forms.ModelChoiceField(
label=_('Organizer'),
queryset=Organizer.objects.none(),
required=False,
empty_label=_('All organizers'),
widget=Select2(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse_lazy('control:organizers.select2'),
'data-placeholder': _('All organizers')
}
),
)
state = forms.ChoiceField(
label=_('Status'),
required=False,
choices=[('', _('All payments'))] + list(OrderPayment.PAYMENT_STATES),
)
provider = forms.ChoiceField(
label=_('Payment provider'),
choices=[
('', _('All payment providers')),
],
required=False,
)
created_from = forms.DateField(
label=_('Payment created from'),
required=False,
widget=DatePickerWidget,
)
created_until = forms.DateField(
label=_('Payment created until'),
required=False,
widget=DatePickerWidget,
)
completed_from = forms.DateField(
label=_('Paid from'),
required=False,
widget=DatePickerWidget,
)
completed_until = forms.DateField(
label=_('Paid until'),
required=False,
widget=DatePickerWidget,
)
amount = forms.CharField(
label=_('Amount'),
required=False,
widget=forms.NumberInput(attrs={
'placeholder': _('Amount'),
}),
)
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request')
super().__init__(*args, **kwargs)
self.fields['ordering'] = forms.ChoiceField(
choices=sum([
[(a, a), ('-' + a, '-' + a)]
for a in self.orders.keys()
], []),
required=False
)
if self.request.user.has_active_staff_session(self.request.session.session_key):
self.fields['organizer'].queryset = Organizer.objects.all()
self.fields['event'].queryset = Event.objects.all()
else:
self.fields['organizer'].queryset = Organizer.objects.filter(
pk__in=self.request.user.teams.values_list('organizer', flat=True)
)
self.fields['event'].queryset = self.request.user.get_events_with_permission('can_view_orders')
self.fields['provider'].choices += get_all_payment_providers()
def filter_qs(self, qs):
fdata = self.cleaned_data
if fdata.get('created_from'):
date_start = make_aware(datetime.combine(
fdata.get('created_from'),
time(hour=0, minute=0, second=0, microsecond=0)
), get_current_timezone())
qs = qs.filter(created__gte=date_start)
if fdata.get('created_until'):
date_end = make_aware(datetime.combine(
fdata.get('created_until') + timedelta(days=1),
time(hour=0, minute=0, second=0, microsecond=0)
), get_current_timezone())
qs = qs.filter(created__lt=date_end)
if fdata.get('completed_from'):
date_start = make_aware(datetime.combine(
fdata.get('completed_from'),
time(hour=0, minute=0, second=0, microsecond=0)
), get_current_timezone())
qs = qs.filter(payment_date__gte=date_start)
if fdata.get('completed_until'):
date_end = make_aware(datetime.combine(
fdata.get('completed_until') + timedelta(days=1),
time(hour=0, minute=0, second=0, microsecond=0)
), get_current_timezone())
qs = qs.filter(payment_date__lt=date_end)
if fdata.get('event'):
qs = qs.filter(order__event=fdata.get('event'))
if fdata.get('organizer'):
qs = qs.filter(order__event__organizer=fdata.get('organizer'))
if fdata.get('state'):
qs = qs.filter(state=fdata.get('state'))
if fdata.get('provider'):
qs = qs.filter(provider=fdata.get('provider'))
if fdata.get('query'):
u = fdata.get('query')
matching_invoices = Invoice.objects.filter(
Q(invoice_no__iexact=u)
| Q(invoice_no__iexact=u.zfill(5))
| Q(full_invoice_no__iexact=u)
).values_list('order_id', flat=True)
matching_invoice_addresses = InvoiceAddress.objects.filter(
Q(
Q(name_cached__icontains=u) | Q(company__icontains=u)
)
).values_list('order_id', flat=True)
if "-" in u:
code = (Q(event__slug__icontains=u.rsplit("-", 1)[0])
& Q(code__icontains=Order.normalize_code(u.rsplit("-", 1)[1])))
else:
code = Q(code__icontains=Order.normalize_code(u))
matching_orders = Order.objects.filter(
Q(
code
| Q(email__icontains=u)
| Q(comment__icontains=u)
)
).values_list('id', flat=True)
mainq = (
Q(order__id__in=matching_invoices)
| Q(order__id__in=matching_invoice_addresses)
| Q(order__id__in=matching_orders)
)
qs = qs.filter(mainq)
if fdata.get('amount'):
amount = fdata.get('amount')
def is_decimal(value):
result = True
parts = value.split('.', maxsplit=1)
for part in parts:
result = result & part.isdecimal()
return result
if is_decimal(amount):
qs = qs.filter(amount=Decimal(amount))
if fdata.get('ordering'):
p = self.cleaned_data.get('ordering')
if p.startswith('-') and p not in self.orders:
qs = qs.order_by('-' + self.orders[p[1:]])
else:
qs = qs.order_by(self.orders[p])
else:
qs = qs.order_by('-created')
return qs
class SubEventFilterForm(FilterForm):
orders = {
'date_from': 'date_from',
@@ -2225,15 +2018,6 @@ class DeviceFilterForm(FilterForm):
],
required=False,
)
state = forms.ChoiceField(
label=_('Device status'),
choices=[
('', _('All devices')),
('active', _('Active devices')),
('revoked', _('Revoked devices'))
],
required=False
)
def __init__(self, *args, **kwargs):
request = kwargs.pop('request')
@@ -2263,11 +2047,6 @@ class DeviceFilterForm(FilterForm):
if fdata.get('gate'):
qs = qs.filter(gate=fdata['gate'])
if fdata.get('state') == 'active':
qs = qs.filter(revoked=False)
elif fdata.get('state') == 'revoked':
qs = qs.filter(revoked=True)
if fdata.get('ordering'):
qs = qs.order_by(self.get_order_by())
else:

View File

@@ -713,7 +713,6 @@ class ItemVariationForm(I18nModelForm):
'default_price',
'original_price',
'description',
'require_approval',
'require_membership',
'require_membership_hidden',
'require_membership_types',

View File

@@ -1,129 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import ipaddress
import socket
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from pretix.base.forms import SecretKeySettingsField, SettingsForm
class SMTPMailForm(SettingsForm):
mail_from = forms.EmailField(
label=_("Sender address"),
help_text=_("Sender address for outgoing emails"),
required=True,
)
smtp_host = forms.CharField(
label=_("Hostname"),
required=True,
widget=forms.TextInput(attrs={'placeholder': 'mail.example.org'})
)
smtp_port = forms.IntegerField(
label=_("Port"),
required=True,
widget=forms.TextInput(attrs={'placeholder': 'e.g. 587, 465, 25, ...'})
)
smtp_username = forms.CharField(
label=_("Username"),
widget=forms.TextInput(attrs={'placeholder': 'myuser@example.org'}),
required=False
)
smtp_password = SecretKeySettingsField(
label=_("Password"),
required=False,
)
smtp_use_tls = forms.BooleanField(
label=_("Use STARTTLS"),
help_text=_("Commonly enabled on port 587."),
required=False
)
smtp_use_ssl = forms.BooleanField(
label=_("Use SSL"),
help_text=_("Commonly enabled on port 465."),
required=False
)
def clean(self):
data = super().clean()
if data.get('smtp_use_tls') and data.get('smtp_use_ssl'):
raise ValidationError(_('You can activate either SSL or STARTTLS security, but not both at the same time.'))
for k, v in self.fields.items():
val = data.get(k)
if v._required and not val:
self.add_error(k, _('This field is required.'))
return data
def clean_smtp_host(self):
v = self.cleaned_data['smtp_host']
if not settings.MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS:
try:
if ipaddress.ip_address(v).is_private:
raise ValidationError(_('You are not allowed to use this mail server, please choose one with a '
'public IP address instead.'))
except ValueError:
try:
if ipaddress.ip_address(socket.gethostbyname(v)).is_private:
raise ValidationError(_('You are not allowed to use this mail server, please choose one with a '
'public IP address instead.'))
except OSError:
raise ValidationError(_('We were unable to resolve this hostname.'))
return v
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.obj.settings.mail_from in (settings.MAIL_FROM, settings.MAIL_FROM_ORGANIZERS):
self.initial.pop('mail_from')
for k, v in self.fields.items():
v._required = v.required
v.required = False
v.widget.is_required = False
class SimpleMailForm(SettingsForm):
mail_from = forms.EmailField(
label=_("Sender address"),
help_text=_("Sender address for outgoing emails"),
required=True,
)
def clean(self):
cleaned_data = super().clean()
for k, v in self.fields.items():
val = cleaned_data.get(k)
if v._required and not val:
self.add_error(k, _('This field is required.'))
return cleaned_data
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.obj.settings.mail_from in (settings.MAIL_FROM, settings.MAIL_FROM_ORGANIZERS):
self.initial.pop('mail_from')
for k, v in self.fields.items():
v._required = v.required
v.required = False
v.widget.is_required = False

View File

@@ -452,7 +452,7 @@ class OrderPositionChangeForm(forms.Form):
@staticmethod
def taxrule_label_from_instance(obj):
return f"{obj.internal_name or obj.name} ({obj.rate} %)"
return f"{obj.name} ({obj.rate} %)"
def __init__(self, *args, **kwargs):
instance = kwargs.pop('instance')

View File

@@ -44,23 +44,21 @@ from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes.forms import SafeModelMultipleChoiceField
from i18nfield.forms import I18nFormField, I18nTextarea
from phonenumber_field.formfields import PhoneNumberField
from pytz import common_timezones
from pretix.api.models import WebHook
from pretix.api.webhooks import get_all_webhook_events
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
from pretix.base.forms.questions import (
NamePartsFormField, WrappedPhoneNumberPrefixWidget, get_country_by_locale,
get_phone_prefix,
)
from pretix.base.forms.questions import NamePartsFormField
from pretix.base.forms.widgets import SplitDateTimePickerWidget
from pretix.base.models import (
Customer, Device, EventMetaProperty, Gate, GiftCard, Membership,
MembershipType, Organizer, Team,
)
from pretix.base.settings import PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS
from pretix.control.forms import ExtFileField, SplitDateTimeField
from pretix.control.forms import (
ExtFileField, SMTPSettingsMixin, SplitDateTimeField,
)
from pretix.control.forms.event import (
SafeEventMultipleChoiceField, multimail_validate,
)
@@ -356,8 +354,9 @@ class OrganizerSettingsForm(SettingsForm):
]
class MailSettingsForm(SettingsForm):
class MailSettingsForm(SMTPSettingsMixin, SettingsForm):
auto_fields = [
'mail_from',
'mail_from_name',
]
@@ -536,21 +535,11 @@ class CustomerUpdateForm(forms.ModelForm):
class Meta:
model = Customer
fields = ['is_active', 'name_parts', 'email', 'is_verified', 'phone', 'locale']
fields = ['is_active', 'name_parts', 'email', 'is_verified', 'locale']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.instance.phone and (self.instance.organizer.settings.region or self.instance.locale):
country_code = self.instance.organizer.settings.region or get_country_by_locale(self.instance.locale)
phone_prefix = get_phone_prefix(country_code)
if phone_prefix:
self.initial['phone'] = "+{}.".format(phone_prefix)
self.fields['phone'] = PhoneNumberField(
label=_('Phone'),
required=False,
widget=WrappedPhoneNumberPrefixWidget()
)
self.fields['name_parts'] = NamePartsFormField(
max_length=255,
required=False,
@@ -576,13 +565,6 @@ class CustomerUpdateForm(forms.ModelForm):
return self.cleaned_data
class CustomerCreateForm(CustomerUpdateForm):
class Meta:
model = Customer
fields = ['identifier', 'is_active', 'name_parts', 'email', 'is_verified', 'locale']
class MembershipUpdateForm(forms.ModelForm):
class Meta:

View File

@@ -43,6 +43,7 @@ from django.urls import get_script_prefix, resolve, reverse
from django.utils.encoding import force_str
from django.utils.translation import gettext as _
from django_scopes import scope
from hijack.templatetags.hijack_tags import is_hijacked
from pretix.base.models import Event, Organizer
from pretix.base.models.auth import SuperuserPermissionSet, User
@@ -182,7 +183,7 @@ class AuditLogMiddleware:
def __call__(self, request):
if request.path.startswith(get_script_prefix() + 'control') and request.user.is_authenticated:
if getattr(request.user, "is_hijacked", False):
if is_hijacked(request):
hijack_history = request.session.get('hijack_history', False)
hijacker = get_object_or_404(User, pk=hijack_history[0])
ss = hijacker.get_active_staff_session(request.session.get('hijacker_session'))

View File

@@ -343,24 +343,10 @@ def get_global_navigation(request):
'icon': 'group',
},
{
'label': _('Search'),
'label': _('Order search'),
'url': reverse('control:search.orders'),
'active': False,
'active': 'search.orders' in url.url_name,
'icon': 'search',
'children': [
{
'label': _('Orders'),
'url': reverse('control:search.orders'),
'active': 'search.orders' in url.url_name,
'icon': 'search',
},
{
'label': _('Payments'),
'url': reverse('control:search.payments'),
'active': 'search.payments' in url.url_name,
'icon': 'search',
},
]
},
{
'label': _('User settings'),

View File

@@ -1,5 +1,6 @@
{% load compress %}
{% load i18n %}
{% load hijack_tags %}
{% load static %}
<!DOCTYPE html>
<html{% if rtl %} dir="rtl" class="rtl"{% endif %}>
@@ -38,7 +39,7 @@
</div>
{% endfor %}
{% endif %}
{% if request.user.is_hijacked %}
{% if request|is_hijacked %}
<div class="impersonate-warning">
<span class="fa fa-user-secret"></span>
{% blocktrans with user=request.user%}You are currently working on behalf of {{ user }}.{% endblocktrans %}

View File

@@ -1,6 +1,7 @@
{% load compress %}
{% load static %}
{% load i18n %}
{% load hijack_tags %}
{% load statici18n %}
{% load eventsignal %}
{% load eventurl %}
@@ -350,7 +351,7 @@
</ul>
</div>
{% endif %}
{% if request.user.is_hijacked %}
{% if request|is_hijacked %}
<div class="impersonate-warning">
<span class="fa fa-user-secret"></span>
{% blocktrans with user=request.user%}You are currently working on behalf of {{ user }}.{% endblocktrans %}

View File

@@ -74,17 +74,17 @@
{{ c.datetime|date:"SHORT_DATETIME_FORMAT" }}
{% if c.type == "exit" %}
{% if c.auto_checked_in %}
<span class="fa fa-fw fa-hourglass-end" data-toggle="tooltip"
<span class="fa fa-fw fa-hourglass-end" data-toggle="tooltip_html"
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically marked not present: {{ date }}{% endblocktrans %}"></span>
{% endif %}
{% elif c.forced and c.successful %}
<span class="fa fa-fw fa-warning" data-toggle="tooltip"
<span class="fa fa-fw fa-warning" data-toggle="tooltip_html"
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}"></span>
{% elif c.forced and not c.successful %}
<br>
<small class="text-muted">{% trans "Failed in offline mode" %}</small>
{% elif c.auto_checked_in %}
<span class="fa fa-fw fa-magic" data-toggle="tooltip"
<span class="fa fa-fw fa-magic" data-toggle="tooltip_html"
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}"></span>
{% endif %}
</td>

View File

@@ -1,14 +0,0 @@
{% load i18n %}{% blocktrans with code=code instance=instance %}Hello,
someone requested to use {{ address }} as a sender address on {{ instance }}.
This will allow them to send emails that are shown to originate from this email address.
If that was you, please enter the following confirmation code:
{{ code }}
If this was not requested by you, you can safely ignore this email.
Best regards,
Your {{ instance }} team
{% endblocktrans %}

View File

@@ -1,127 +0,0 @@
{% extends basetpl %}
{% load i18n %}
{% load bootstrap3 %}
{% load hierarkey_form %}
{% load static %}
{% block title %}{% trans "Organizer" %}{% endblock %}
{% block content %}
<h1>{% trans "E-mail sending" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<div class="panel-group" id="email">
<div class="panel panel-default">
<div class="accordion-radio">
<div class="panel-heading">
<p class="panel-title">
<input type="radio" name="mode" value="system"
data-parent="#email"
{% if mode == "system" %}checked="checked"{% endif %}
id="input_mode_system"
data-toggle="radiocollapse" data-target="#mode_system"/>
<label for="input_mode_system"><strong>{% trans "Use system default" %}</strong></label>
</p>
</div>
</div>
<div id="mode_system"
class="panel-collapse collapsed {% if mode == "system" %}in{% endif %}">
<div class="panel-body form-horizontal">
<p>
{% blocktrans trimmed %}
E-mails will be sent through the system's default server. They will show the following
sender information:
{% endblocktrans %}
</p>
<dl class="dl-horizontal">
<dt>{% trans "From" context "mail_header" %}</dt>
<dd>{{ object.settings.mail_from_name|default_if_none:object.name }}
&lt;{{ default_sender_address }}&gt;
</dd>
{% if object.settings.contact_mail %}
<dt>{% trans "Reply-To" context "mail_header" %}</dt>
<dd>{{ object.settings.contact_mail }}</dd>
{% endif %}
</dl>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="accordion-radio">
<div class="panel-heading">
<p class="panel-title">
<input type="radio" name="mode" value="simple"
data-parent="#email"
{% if mode == "simple" %}checked="checked"{% endif %}
id="input_mode_simple"
data-toggle="radiocollapse" data-target="#mode_simple"/>
<label for="input_mode_simple"><strong>{% trans "Use system email server with a custom sender address" %}</strong></label>
</p>
</div>
</div>
<div id="mode_simple"
class="panel-collapse collapsed {% if mode == "simple" %}in{% endif %}">
<div class="panel-body form-horizontal">
<p>
{% blocktrans trimmed %}
E-mails will be sent through the system's default server but with your own sender
address.
This will make your emails look more personalized and coming directly from you, but it
also might require some extra steps to ensure good deliverability.
{% endblocktrans %}
</p>
{% bootstrap_form simple_form layout="control" %}
</div>
</div>
</div>
<div class="panel panel-default">
<div class="accordion-radio">
<div class="panel-heading">
<p class="panel-title">
<input type="radio" name="mode" value="smtp"
data-parent="#email"
{% if mode == "smtp" %}checked="checked"{% endif %}
id="input_mode_smtp"
data-toggle="radiocollapse" data-target="#mode_smtp"/>
<label for="input_mode_smtp"><strong>{% trans "Use a custom SMTP server" %}</strong></label>
</p>
</div>
</div>
<div id="mode_smtp"
class="panel-collapse collapsed {% if mode == "smtp" %}in{% endif %}">
<div class="panel-body form-horizontal">
<p>
{% blocktrans trimmed %}
For full customization, you can configure your own SMTP server that will be used for
email sending.
{% endblocktrans %}
</p>
{% bootstrap_form smtp_form layout="control" %}
</div>
</div>
</div>
{% if request.event %}
<div class="panel panel-default">
<div class="accordion-radio">
<div class="panel-heading">
<p class="panel-title">
<input type="radio" name="mode" value="reset"
data-parent="#reset"
id="input_mode_reset"
data-toggle="radiocollapse" data-target="#mode_reset"/>
<label for="input_mode_reset"><strong>{% trans "Reset to organizer settings" %}</strong></label>
</p>
</div>
</div>
<div id="mode_reset"
class="panel-collapse collapsed {% if mode == "reset" %}in{% endif %}">
</div>
</div>
{% endif %}
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Continue" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -1,79 +0,0 @@
{% extends basetpl %}
{% load i18n %}
{% load bootstrap3 %}
{% load hierarkey_form %}
{% load static %}
{% block title %}{% trans "Organizer" %}{% endblock %}
{% block content %}
<h1>{% trans "E-mail sending" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% for k, v in request.POST.items %}
<input type="hidden" name="{{ k }}" value="{{ v }}">
{% endfor %}
<input type="hidden" name="state" value="save">
<div class="panel panel-default">
<div class="panel-heading">
<p class="panel-title">
<strong>{% trans "Use system email server with a custom sender address" %}</strong>
</p>
</div>
<div class="panel-body form-horizontal">
{% if spf_warning %}
<div class="alert alert-warning">
<p>
{{ spf_warning }}
</p>
{% if spf_record %}
<p>
{% trans "This is the SPF record we found on your domain:" %}
</p>
<pre><code>{{ spf_record }}</code></pre>
<p>
{% trans "To fix this, include the following part before the last word:" %}
</p>
<pre><code>{{ spf_key }}</code></pre>
{% else %}
<p>
{% trans "Your new SPF record could look like this:" %}
</p>
<pre><code>v=spf1 a mx {{ spf_key }} ~all</code></pre>
{% endif %}
<p>
{% trans "Please keep in mind that updates to DNS might require multiple hours to take effect." %}
</p>
</div>
{% elif spf_key %}
<div class="alert alert-success">
{% blocktrans trimmed %}
We found an SPF record on your domain that includes this system. Great!
{% endblocktrans %}
</div>
{% endif %}
{% if verification %}
<h3>{% trans "Verification" %}</h3>
<p>
{% blocktrans trimmed with recp=recp %}
We've sent an email to {{ recp }} with a confirmation code to verify that this email address
is owned by you. Please enter the verification code below:
{% endblocktrans %}
</p>
<div class="form-group">
<label class="col-md-3 control-label" for="id_verification">
{% trans "Verification code" %}
</label>
<div class="col-md-9">
<input type="text" name="verification" class="form-control">
</div>
</div>
{% endif %}
</div>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -1,42 +0,0 @@
{% extends basetpl %}
{% load i18n %}
{% load bootstrap3 %}
{% load hierarkey_form %}
{% load static %}
{% block title %}{% trans "Organizer" %}{% endblock %}
{% block content %}
<h1>{% trans "E-mail sending" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% for k, v in request.POST.items %}
<input type="hidden" name="{{ k }}" value="{{ v }}">
{% endfor %}
<input type="hidden" name="state" value="save">
<div class="panel panel-default">
<div class="panel-heading">
<p class="panel-title">
<strong>{% trans "Use a custom SMTP server" %}</strong>
</p>
</div>
<div class="panel-body form-horizontal">
<div class="alert alert-success">
{% blocktrans trimmed %}
A test connection to your SMTP server was successful. You can now save your new settings
to put them in use.
{% endblocktrans %}
</div>
{% if known_host_problem %}
<div class="alert alert-warning">
{{ known_host_problem }}
</div>
{% endif %}
</div>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -12,55 +12,17 @@
<div class="tabbed-form">
<fieldset>
<legend>{% trans "General" %}</legend>
{% bootstrap_field form.mail_prefix layout="control" %}
{% bootstrap_field form.mail_attach_tickets layout="control" %}
{% url "control:organizer.settings.mail" organizer=request.organizer.slug as org_url %}
{% propagated request.event org_url "mail_from" "smtp_use_custom" "smtp_host" "smtp_port" "smtp_username" "smtp_password" "smtp_use_tls" "smtp_use_ssl" %}
<div class="form-group">
<label class="col-md-3 control-label">
{% trans "Sending method" %}
</label>
<div class="col-md-9 static-form-row-with-btn">
{% if request.event.settings.smtp_use_custom %}
{% trans "Custom SMTP server" %}: {{ request.event.settings.smtp_host }}
{% else %}
{% trans "System-provided email server" %}
{% endif %}
&nbsp;&nbsp;
<a href="{% url "control:event.settings.mail.setup" organizer=request.organizer.slug event=request.event.slug %}"
class="btn btn-default">
<span class="fa fa-edit"></span>
{% trans "Edit" %}
</a>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label">
{% trans "Sender address" %}
</label>
<div class="col-md-9 static-form-row-with-btn">
{{ request.event.settings.mail_from }}
&nbsp;&nbsp;
<a href="{% url "control:event.settings.mail.setup" organizer=request.organizer.slug event=request.event.slug %}"
class="btn btn-default">
<span class="fa fa-edit"></span>
{% trans "Edit" %}
</a>
</div>
</div>
{% endpropagated %}
{% propagated request.event org_url "mail_from_name" "mail_text_signature" "mail_bcc" %}
{% propagated request.event org_url "mail_from" "mail_from_name" "mail_text_signature" "mail_bcc" %}
{% bootstrap_field form.mail_from layout="control" %}
{% bootstrap_field form.mail_from_name layout="control" %}
{% bootstrap_field form.mail_text_signature layout="control" %}
{% bootstrap_field form.mail_bcc layout="control" %}
{% endpropagated %}
{% bootstrap_field form.mail_sales_channel_placed_paid layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Calendar invites" %}</legend>
{% bootstrap_field form.mail_prefix layout="control" %}
{% bootstrap_field form.mail_attach_tickets layout="control" %}
{% bootstrap_field form.mail_attach_ical layout="control" %}
{% bootstrap_field form.mail_attach_ical_paid_only layout="control" %}
{% bootstrap_field form.mail_attach_ical_description layout="control" %}
{% bootstrap_field form.mail_sales_channel_placed_paid layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "E-mail design" %}</legend>
@@ -85,7 +47,6 @@
</fieldset>
<fieldset>
<legend>{% trans "E-mail content" %}</legend>
<h4>{% trans "Text" %}</h4>
<div class="panel-group" id="questions_group">
{% blocktrans asvar title_placed_order %}Placed order{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_placed" title=title_placed_order items="mail_text_order_placed,mail_send_order_placed_attendee,mail_text_order_placed_attendee" exclude="mail_send_order_placed_attendee" %}
@@ -120,14 +81,27 @@
{% blocktrans asvar title_require_approval %}Order approval process{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_require_approval items="mail_text_order_placed_require_approval,mail_text_order_approved,mail_text_order_approved_free,mail_text_order_denied" %}
</div>
<h4>{% trans "Attachments" %}</h4>
{% bootstrap_field form.mail_attachment_new_order layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "SMTP settings" %}</legend>
{% propagated request.event org_url "smtp_use_custom" "smtp_host" "smtp_port" "smtp_username" "smtp_password" "smtp_use_tls" "smtp_use_ssl" %}
{% bootstrap_field form.smtp_use_custom layout="control" %}
{% bootstrap_field form.smtp_host layout="control" %}
{% bootstrap_field form.smtp_port layout="control" %}
{% bootstrap_field form.smtp_username layout="control" %}
{% bootstrap_field form.smtp_password layout="control" %}
{% bootstrap_field form.smtp_use_tls layout="control" %}
{% bootstrap_field form.smtp_use_ssl layout="control" %}
{% endpropagated %}
</fieldset>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
<button type="submit" class="btn btn-default btn-save pull-left" name="test" value="1">
{% trans "Save and test custom SMTP connection" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -24,7 +24,6 @@
<fieldset>
<legend>{% trans "General" %}</legend>
{% bootstrap_field form.name layout="control" %}
{% bootstrap_field form.internal_name layout="control" %}
{% bootstrap_field form.rate addon_after="%" layout="control" %}
{% bootstrap_field form.price_includes_tax layout="control" %}
</fieldset>
@@ -40,7 +39,6 @@
</div>
{% bootstrap_field form.eu_reverse_charge layout="control" %}
{% bootstrap_field form.home_country layout="control" %}
{% bootstrap_field form.keep_gross_if_rate_changes layout="control" %}
<h3>{% trans "Custom taxation rules" %}</h3>
<div class="alert alert-warning">
{% blocktrans trimmed %}

View File

@@ -33,7 +33,7 @@
<tr>
<td>
<strong><a href="{% url "control:event.settings.tax.edit" organizer=request.event.organizer.slug event=request.event.slug rule=tr.id %}">
{{ tr.internal_name|default:tr.name }}
{{ tr.name }}
</a></strong>
</td>
<td>

View File

@@ -1,6 +1,6 @@
{% load i18n %}
<div class="quotabox availability" data-toggle="tooltip_html" data-placement="top"
title="{% trans "Quota:" %} {{ q.name|force_escape|force_escape }}<br>{% blocktrans with date=q.cached_availability_time|date:"SHORT_DATETIME_FORMAT" %}Numbers as of {{ date }}{% endblocktrans %}">
title="{% trans "Quota:" %} {{ q.name }}<br>{% blocktrans with date=q.cached_availability_time|date:"SHORT_DATETIME_FORMAT" %}Numbers as of {{ date }}{% endblocktrans %}">
{% if q.size|default_if_none:"NONE" == "NONE" %}
<div class="progress">
<div class="progress-bar progress-bar-success progress-bar-100">

View File

@@ -1,6 +1,6 @@
{% load i18n %}
<a class="quotabox" data-toggle="tooltip_html" data-placement="top"
title="{% trans "Quota:" %} {{ q.name|force_escape|force_escape }}{% if q.cached_avail.1 is not None %}<br>{% blocktrans with num=q.cached_avail.1 %}Currently available: {{ num }}{% endblocktrans %}{% endif %}"
title="{% trans "Quota:" %} {{ q.name }}{% if q.cached_avail.1 is not None %}<br>{% blocktrans with num=q.cached_avail.1 %}Currently available: {{ num }}{% endblocktrans %}{% endif %}"
href="{% url "control:event.items.quotas.show" event=q.event.slug organizer=q.event.organizer.slug quota=q.pk %}">
{% if q.size|default_if_none:"NONE" == "NONE" %}
<div class="progress">

View File

@@ -73,7 +73,6 @@
{% bootstrap_field form.available_until layout="control" %}
{% bootstrap_field form.sales_channels layout="control" %}
{% bootstrap_field form.hide_without_voucher layout="control" %}
{% bootstrap_field form.require_approval layout="control" %}
{% if form.require_membership %}
{% bootstrap_field form.require_membership layout="control" %}
<div data-display-dependency="#{{ form.require_membership.id_for_label }}">
@@ -145,7 +144,6 @@
{% bootstrap_field formset.empty_form.available_until layout="control" %}
{% bootstrap_field formset.empty_form.sales_channels layout="control" %}
{% bootstrap_field formset.empty_form.hide_without_voucher layout="control" %}
{% bootstrap_field formset.empty_form.require_approval layout="control" %}
{% if formset.empty_form.require_membership %}
{% bootstrap_field formset.empty_form.require_membership layout="control" %}
<div data-display-dependency="#{{ formset.empty_form.require_membership.id_for_label }}">

View File

@@ -46,7 +46,7 @@
{% for c in cat_list %}
<tbody data-dnd-url="{% url "control:event.items.reorder" organizer=request.event.organizer.slug event=request.event.slug %}">
{% for i in c.list %}
{% if forloop.counter0 == 0 and i.category %}<tr class="sortable-disabled"><th colspan="8" scope="colgroup" class="text-muted">{{ i.category }}</th></tr>{% endif %}
{% if forloop.counter0 == 0 and i.category %}<tr class="sortable-disabled"><th colspan="8" scope="colgroup" class="text-muted">{{ i.category.name }}</th></tr>{% endif %}
<tr data-dnd-id="{{ i.id }}" {% if not i.active %}class="row-muted"{% endif %}>
<td><strong>
{% if not i.active %}<strike>{% endif %}

View File

@@ -13,7 +13,7 @@
<ul>
{% for item in dependent %}
<li>
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.pk %}">{{ item }}</a>
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.pk %}">{{ item.name }}</a>
</li>
{% endfor %}
</ul>

View File

@@ -145,12 +145,7 @@
<strong>{% trans "Tax rule" %}</strong>
</div>
<div class="col-sm-5">
{% if position.tax_rule.internal_name %}
{{ position.tax_rule.internal_name }}
{% else %}
{{ position.tax_rule.name }}
{% endif %}
({{ position.tax_rule.rate }} %)
{{ position.tax_rule.name }} ({{ position.tax_rule.rate }} %)
</div>
<div class="col-sm-4 field-container">
{% bootstrap_field position.form.tax_rule layout='inline' %}

View File

@@ -360,19 +360,19 @@
{% if line.checkins.all %}
{% for c in line.all_checkins.all %}
{% if not c.successful %}
<span class="fa fa-fw fa-exclamation-circle text-danger" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Denied scan: {{ date }}{% endblocktrans %}<br>{{ c.get_error_reason_display }}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
<span class="fa fa-fw fa-exclamation-circle text-danger" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Denied scan: {{ date }}{% endblocktrans %}<br>{{ c.get_error_reason_display }}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% elif c.type == "exit" %}
{% if c.auto_checked_in %}
<span class="fa fa-fw text-success fa-hourglass-end" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically marked not present: {{ date }}{% endblocktrans %}"></span>
<span class="fa fa-fw text-success fa-hourglass-end" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically marked not present: {{ date }}{% endblocktrans %}"></span>
{% else %}
<span class="fa fa-fw text-success fa-sign-out" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Exit scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
<span class="fa fa-fw text-success fa-sign-out" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Exit scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% endif %}
{% elif c.forced %}
<span class="fa fa-fw fa-warning text-warning" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
<span class="fa fa-fw fa-warning text-warning" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% elif c.auto_checked_in %}
<span class="fa fa-fw fa-magic text-success" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
<span class="fa fa-fw fa-magic text-success" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% else %}
<span class="fa fa-fw fa-check text-success" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
<span class="fa fa-fw fa-check text-success" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% endif %}
{% endfor %}
{% endif %}

View File

@@ -48,10 +48,6 @@
</dd>
<dt>{% trans "Name" %}</dt>
<dd>{{ customer.name }}</dd>
{% if customer.phone %}
<dt>{% trans "Phone" %}</dt>
<dd>{{ customer.phone }}</dd>
{% endif %}
<dt>{% trans "Locale" %}</dt>
<dd>{{ display_locale }}</dd>
<dt>{% trans "Registration date" %}</dt>

View File

@@ -2,23 +2,15 @@
{% load i18n %}
{% load bootstrap3 %}
{% block title %}
{% if not customer.id %}
{% trans "New customer" %}
{% else %}
{% blocktrans trimmed with id=customer.identifier %}
Customer #{{ id }}
{% endblocktrans %}
{% endif %}
{% blocktrans trimmed with id=customer.identifier %}
Customer #{{ id }}
{% endblocktrans %}
{% endblock %}
{% block inner %}
<h1>
{% if not customer.id %}
{% trans "New customer" %}
{% else %}
{% blocktrans trimmed with id=customer.identifier %}
Customer #{{ id }}
{% endblocktrans %}
{% endif %}
{% blocktrans trimmed with id=customer.identifier %}
Customer #{{ id }}
{% endblocktrans %}
</h1>
<form class="form-horizontal" action="" method="post">
{% csrf_token %}

View File

@@ -15,8 +15,6 @@
No customer accounts have been created yet.
{% endblocktrans %}
</p>
<a href="{% url "control:organizer.customer.create" organizer=request.organizer.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new customer" %}</a>
</div>
{% else %}
<div class="panel panel-default">
@@ -43,10 +41,6 @@
</div>
</form>
</div>
<p>
<a href="{% url "control:organizer.customer.create" organizer=request.organizer.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new customer" %}</a>
</p>
<div class="table-responsive">
<table class="table table-condensed table-hover">
<thead>

View File

@@ -30,7 +30,7 @@
</div>
<form class="panel-body filter-form" action="" method="get">
<div class="row">
<div class="col-md-4 col-sm-6 col-xs-12">
<div class="col-md-6 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.query %}
</div>
<div class="col-md-3 col-sm-6 col-xs-12">
@@ -39,9 +39,6 @@
<div class="col-md-3 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.software_brand %}
</div>
<div class="col-md-2 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.state %}
</div>
</div>
<div class="text-right">
<button class="btn btn-primary btn-lg" type="submit">

View File

@@ -57,7 +57,6 @@
</div>
<form class="" method="post" action="">
{% csrf_token %}
<button type="submit" class="hidden">Add</button> <!-- Required because pressing enter in the text fields will submit the first button -->
<table class="panel-body table">
<thead>
<tr>
@@ -102,7 +101,7 @@
</td>
<td class="text-right form-inline">
<input type="text" class="form-control input-sm" placeholder="{% trans "Value" %}" name="value">
<button type="submit" class="btn btn-primary">
<button class="btn btn-primary">
<span class="fa fa-plus"></span>
</button>
</td>

View File

@@ -11,45 +11,13 @@
<h1>{% trans "E-mail settings" %}</h1>
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data"
mail-preview-url="{% url "control:organizer.settings.mail.preview" organizer=request.organizer.slug %}">
mail-preview-url="{% url "control:organizer.settings.mail.preview" organizer=request.organizer.slug %}">
{% csrf_token %}
{% bootstrap_form_errors form %}
<div class="tabbed-form">
<fieldset>
<legend>{% trans "General" %}</legend>
<div class="form-group">
<label class="col-md-3 control-label">
{% trans "Sending method" %}
</label>
<div class="col-md-9 static-form-row-with-btn">
{% if request.organizer.settings.smtp_use_custom %}
{% trans "Custom SMTP server" %}: {{ request.organizer.settings.smtp_host }}
{% else %}
{% trans "System-provided email server" %}
{% endif %}
&nbsp;&nbsp;
<a href="{% url "control:organizer.settings.mail.setup" organizer=request.organizer.slug %}"
class="btn btn-default">
<span class="fa fa-edit"></span>
{% trans "Edit" %}
</a>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label">
{% trans "Sender address" %}
</label>
<div class="col-md-9 static-form-row-with-btn">
{{ request.organizer.settings.mail_from }}
&nbsp;&nbsp;
<a href="{% url "control:organizer.settings.mail.setup" organizer=request.organizer.slug %}"
class="btn btn-default">
<span class="fa fa-edit"></span>
{% trans "Edit" %}
</a>
</div>
</div>
{% bootstrap_field form.mail_from layout="control" %}
{% bootstrap_field form.mail_from_name layout="control" %}
{% bootstrap_field form.mail_text_signature layout="control" %}
{% bootstrap_field form.mail_bcc layout="control" %}
@@ -67,11 +35,24 @@
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="reset" title=title_reset items="mail_text_customer_reset" %}
</div>
</fieldset>
<fieldset>
<legend>{% trans "SMTP settings" %}</legend>
{% bootstrap_field form.smtp_use_custom layout="control" %}
{% bootstrap_field form.smtp_host layout="control" %}
{% bootstrap_field form.smtp_port layout="control" %}
{% bootstrap_field form.smtp_username layout="control" %}
{% bootstrap_field form.smtp_password layout="control" %}
{% bootstrap_field form.smtp_use_tls layout="control" %}
{% bootstrap_field form.smtp_use_ssl layout="control" %}
</fieldset>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
<button type="submit" class="btn btn-default btn-save pull-left" name="test" value="1">
{% trans "Save and test custom SMTP connection" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -1,164 +0,0 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load eventurl %}
{% load urlreplace %}
{% load money %}
{% load bootstrap3 %}
{% block title %}{% trans "Payment search" %}{% endblock %}
{% block content %}
<h1>{% trans "Payment search" %}</h1>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Filter" %}
</h3>
</div>
<form class="panel-body filter-form" action="" method="get">
<div class="row">
<div class="col-md-12 col-sm-12 col-xs-12">
<div class="row">
<div class="col-md-6 col-sm-12 col-xs-12">
{% bootstrap_field filter_form.query %}
</div>
<div class="col-md-3 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.completed_from %}
</div>
<div class="col-md-3 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.completed_until %}
</div>
</div>
<div class="row">
<div class="col-md-3 col-sm-3 col-xs-4">
{% bootstrap_field filter_form.amount %}
</div>
<div class="col-md-5 col-sm-5 col-xs-8">
{% bootstrap_field filter_form.provider %}
</div>
<div class="col-md-4 col-sm-4 col-xs-12">
{% bootstrap_field filter_form.state %}
</div>
</div>
</div>
<div class="col-md-12 col-sm-12 col-xs-12">
<div class="row">
<div class="col-md-6 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.organizer %}
</div>
<div class="col-md-6 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.event %}
</div>
</div>
<div class="row">
<div class="col-md-3 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.created_from %}
</div>
<div class="col-md-3 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.created_until %}
</div>
</div>
</div>
</div>
<div class="text-right flip">
<button class="btn btn-primary btn-lg" type="submit">
<span class="fa fa-filter"></span>
{% trans "Filter" %}
</button>
</div>
</form>
</div>
<div class="table-responsive">
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>
{% trans "Payment ID" %}
</th>
<th>
{% trans "Order" %}
<a href="?{% url_replace request 'ordering' '-order' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'order' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th>
{% trans "Start date" %}
<a href="?{% url_replace request 'ordering' '-created' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'created' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th>
{% trans "Confirmation date" %}
<a href="?{% url_replace request 'ordering' '-payment_date' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'payment_date' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th>
{% trans "Payment provider" %}
<a href="?{% url_replace request 'ordering' '-provider' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'provider' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th class="text-right flip">
{% trans "Amount" %}
<a href="?{% url_replace request 'ordering' '-amount' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'amount' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th class="text-right flip">
{% trans "Status" %}
<a href="?{% url_replace request 'ordering' '-state' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'state' %}"><i class="fa fa-caret-up"></i></a>
</th>
</tr>
</thead>
<tbody>
{% for p in payments %}
<tr>
<td>{{ p.full_id }}</td>
<td>
<strong>
<a href="{% url "control:event.order" event=p.order.event.slug organizer=p.order.event.organizer.slug code=p.order.code %}">
{{ p.order.full_code }}</a>
</strong>
{% if p.order.testmode %}
<span class="label label-warning">{% trans "TEST MODE" %}</span>
{% endif %}
</td>
<td>
{% if p.migrated %}
<span class="label label-default" data-toggle="tooltip"
title="{% trans "This payment was created with an older version of pretix, therefore accurate data might not be available." %}">
{% trans "MIGRATED" %}
</span>
{% else %}
{{ p.created|date:"SHORT_DATETIME_FORMAT" }}
{% endif %}
</td>
<td>{{ p.payment_date|date:"SHORT_DATETIME_FORMAT" }}</td>
<td>{{ p.payment_provider.verbose_name }}</td>
<td class="text-right flip">{{ p.amount|money:p.order.event.currency }}</td>
<td class="text-right flip">
<span class="label label-{% if p.state == "created" or p.state == "pending" %}warning{% elif p.state == "confirmed" %}success{% else %}danger{% endif %}">
{{ p.get_state_display }}
</span>
</td>
</tr>
{% if staff_session %}
<tr>
<td colspan="1"></td>
<td colspan="6">
<a href="" class="btn btn-default btn-xs" data-expandpayment data-id="{{ p.pk }}">
<span class="fa-eye fa fa-fw"></span>
{% trans "Inspect" %}
</a>
</td>
</tr>
{% endif %}
{% empty %}
<tr>
<td colspan="7" class="text-center"><em>
{% trans "We couldn't find any payments that you have access to and that match your search query." %}
</em></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% include "pretixcontrol/pagination_huge.html" %}
{% endblock %}

View File

@@ -112,8 +112,6 @@ urlpatterns = [
re_path(r'^organizer/(?P<organizer>[^/]+)/edit$', organizer.OrganizerUpdate.as_view(), name='organizer.edit'),
re_path(r'^organizer/(?P<organizer>[^/]+)/settings/email$',
organizer.OrganizerMailSettings.as_view(), name='organizer.settings.mail'),
re_path(r'^organizer/(?P<organizer>[^/]+)/settings/email/setup$',
organizer.MailSettingsSetup.as_view(), name='organizer.settings.mail.setup'),
re_path(r'^organizer/(?P<organizer>[^/]+)/settings/email/preview$',
organizer.MailSettingsPreview.as_view(), name='organizer.settings.mail.preview'),
re_path(r'^organizer/(?P<organizer>[^/]+)/delete$', organizer.OrganizerDelete.as_view(), name='organizer.delete'),
@@ -135,8 +133,6 @@ urlpatterns = [
name='organizer.membershiptype.delete'),
re_path(r'^organizer/(?P<organizer>[^/]+)/customers$', organizer.CustomerListView.as_view(), name='organizer.customers'),
re_path(r'^organizer/(?P<organizer>[^/]+)/customers/select2$', typeahead.customer_select2, name='organizer.customers.select2'),
re_path(r'^organizer/(?P<organizer>[^/]+)/customer/add$',
organizer.CustomerCreateView.as_view(), name='organizer.customer.create'),
re_path(r'^organizer/(?P<organizer>[^/]+)/customer/(?P<customer>[^/]+)/$',
organizer.CustomerDetailView.as_view(), name='organizer.customer'),
re_path(r'^organizer/(?P<organizer>[^/]+)/customer/(?P<customer>[^/]+)/edit$',
@@ -196,7 +192,6 @@ urlpatterns = [
re_path(r'^events/typeahead/$', typeahead.event_list, name='events.typeahead'),
re_path(r'^events/typeahead/meta/$', typeahead.meta_values, name='events.meta.typeahead'),
re_path(r'^search/orders/$', search.OrderSearch.as_view(), name='search.orders'),
re_path(r'^search/payments/$', search.PaymentSearch.as_view(), name='search.payments'),
re_path(r'^event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include([
re_path(r'^$', dashboards.event_index, name='event.index'),
re_path(r'^widgets.json$', dashboards.event_index_widgets_lazy, name='event.index.widgets'),
@@ -216,7 +211,6 @@ urlpatterns = [
re_path(r'^settings/tickets/preview/(?P<output>[^/]+)$', event.TicketSettingsPreview.as_view(),
name='event.settings.tickets.preview'),
re_path(r'^settings/email$', event.MailSettings.as_view(), name='event.settings.mail'),
re_path(r'^settings/email/setup$', event.MailSettingsSetup.as_view(), name='event.settings.mail.setup'),
re_path(r'^settings/email/preview$', event.MailSettingsPreview.as_view(), name='event.settings.mail.preview'),
re_path(r'^settings/email/layoutpreview$', event.MailSettingsRendererPreview.as_view(),
name='event.settings.mail.preview.layout'),

View File

@@ -81,7 +81,6 @@ from pretix.control.forms.event import (
TicketSettingsForm, WidgetCodeForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views.mailsetup import MailSettingsSetupView
from pretix.control.views.user import RecentAuthenticationRequiredMixin
from pretix.helpers.database import rolledback_transaction
from pretix.multidomain.urlreverse import get_event_domain
@@ -638,29 +637,29 @@ class MailSettings(EventSettingsViewMixin, EventSettingsFormView):
k: form.cleaned_data.get(k) for k in form.changed_data
}
)
messages.success(self.request, _('Your changes have been saved.'))
if request.POST.get('test', '0').strip() == '1':
backend = self.request.event.get_mail_backend(force_custom=True, timeout=10)
try:
backend.test(self.request.event.settings.mail_from)
except Exception as e:
messages.warning(self.request, _('An error occurred while contacting the SMTP server: %s') % str(e))
else:
if form.cleaned_data.get('smtp_use_custom'):
messages.success(self.request, _('Your changes have been saved and the connection attempt to '
'your SMTP server was successful.'))
else:
messages.success(self.request, _('We\'ve been able to contact the SMTP server you configured. '
'Remember to check the "use custom SMTP server" checkbox, '
'otherwise your SMTP server will not be used.'))
else:
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
else:
messages.error(self.request, _('We could not save your changes. See below for details.'))
return self.get(request)
class MailSettingsSetup(EventPermissionRequiredMixin, MailSettingsSetupView):
permission = 'can_change_event_settings'
basetpl = 'pretixcontrol/event/base.html'
def get_success_url(self) -> str:
return reverse('control:event.settings.mail', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug
})
def log_action(self, data):
self.request.event.log_action(
'pretix.event.settings', user=self.request.user, data=data
)
class MailSettingsPreview(EventPermissionRequiredMixin, View):
permission = 'can_change_event_settings'

View File

@@ -1,279 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import logging
import dns.resolver
from django.conf import settings
from django.contrib import messages
from django.core.mail import get_connection
from django.shortcuts import redirect
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView
from pretix.base import email
from pretix.base.models import Event
from pretix.base.services.mail import mail
from pretix.control.forms.filter import OrganizerFilterForm
from pretix.control.forms.mailsetup import SimpleMailForm, SMTPMailForm
logger = logging.getLogger(__name__)
def get_spf_record(hostname):
try:
r = dns.resolver.Resolver()
for resp in r.query(hostname, 'TXT'):
data = b''.join(resp.strings).decode()
if data.lower().strip().startswith('v=spf1 '): # RFC7208, section 4.5
return data
except:
logger.exception("Could not fetch SPF record")
def _check_spf_record(not_found_lookup_parts, spf_record, depth):
if depth > 10: # prevent infinite loops
return
parts = spf_record.lower().split(" ") # RFC 7208, section 4.6.1
for p in parts:
try:
not_found_lookup_parts.remove(p)
except KeyError:
pass
if not not_found_lookup_parts: # save some DNS requests if we already found everything
return
for p in parts:
if p.startswith('include:') or p.startswith('+include:'):
_, hostname = p.split(':')
rec_record = get_spf_record(hostname)
if rec_record:
_check_spf_record(not_found_lookup_parts, rec_record, depth + 1)
def check_spf_record(lookup, spf_record):
"""
Check that all parts of lookup appear somewhere in the given SPF record, resolving
include: directives recursively
"""
not_found_lookup_parts = set(lookup.split(" "))
_check_spf_record(not_found_lookup_parts, spf_record, 0)
return not not_found_lookup_parts
class MailSettingsSetupView(TemplateView):
template_name = 'pretixcontrol/email_setup.html'
basetpl = None
@cached_property
def object(self):
return getattr(self.request, 'event', self.request.organizer)
@cached_property
def smtp_form(self):
return SMTPMailForm(
obj=self.object,
prefix='smtp',
data=self.request.POST if (self.request.method == "POST" and self.request.POST.get("mode") == "smtp") else None,
)
@cached_property
def simple_form(self):
return SimpleMailForm(
obj=self.object,
prefix='simple',
data=self.request.POST if (self.request.method == "POST" and self.request.POST.get("mode") == "simple") else None,
)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['basetpl'] = self.basetpl
ctx['object'] = self.object
ctx['smtp_form'] = self.smtp_form
ctx['simple_form'] = self.simple_form
ctx['default_sender_address'] = settings.MAIL_FROM_ORGANIZERS
if 'mode' in self.request.POST:
ctx['mode'] = self.request.POST.get('mode')
elif self.object.settings.smtp_use_custom:
ctx['mode'] = 'smtp'
elif self.object.settings.mail_from not in (settings.MAIL_FROM_ORGANIZERS, settings.MAIL_FROM):
ctx['mode'] = 'simple'
else:
ctx['mode'] = 'system'
return ctx
@cached_property
def filter_form(self):
return OrganizerFilterForm(data=self.request.GET, request=self.request)
def post(self, request, *args, **kwargs):
if request.POST.get('mode') == 'system':
if isinstance(self.object, Event) and 'mail_from' in self.object.organizer.settings._cache():
self.object.settings.mail_from = settings.MAIL_FROM_ORGANIZERS
else:
del self.object.settings.mail_from
self.object.settings.smtp_use_custom = False
del self.object.settings.smtp_host
del self.object.settings.smtp_port
del self.object.settings.smtp_username
del self.object.settings.smtp_password
del self.object.settings.smtp_use_tls
del self.object.settings.smtp_use_ssl
messages.success(request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
elif request.POST.get('mode') == 'reset':
del self.object.settings.mail_from
del self.object.settings.smtp_use_custom
del self.object.settings.smtp_host
del self.object.settings.smtp_port
del self.object.settings.smtp_username
del self.object.settings.smtp_password
del self.object.settings.smtp_use_tls
del self.object.settings.smtp_use_ssl
messages.success(request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
elif request.POST.get('mode') == 'simple':
if not self.simple_form.is_valid():
return super().get(request, *args, **kwargs)
session_key = f'sender_mail_verification_code_{self.request.path}_{self.simple_form.cleaned_data.get("mail_from")}'
allow_save = (
(not settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED or
('verification' in self.request.POST and self.request.POST.get('verification', '') == self.request.session.get(session_key, None))) and
(not settings.MAIL_CUSTOM_SENDER_SPF_STRING or self.request.POST.get('state') == 'save')
)
if allow_save:
for k, v in self.simple_form.cleaned_data.items():
self.object.settings.set(k, v)
self.log_action(self.simple_form.cleaned_data)
if session_key in request.session:
del request.session[session_key]
messages.success(request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
spf_warning = None
spf_record = None
if settings.MAIL_CUSTOM_SENDER_SPF_STRING:
hostname = self.simple_form.cleaned_data['mail_from'].split('@')[-1]
spf_record = get_spf_record(hostname)
if not spf_record:
spf_warning = _(
'We could not find an SPF record set for the domain you are trying to use. You can still '
'proceed, but it will increase the chance of emails going to spam or being rejected. We '
'strongly recommend setting an SPF record on the domain. You can do so through the DNS '
'settings at the provider you registered your domain with.'
)
elif not check_spf_record(settings.MAIL_CUSTOM_SENDER_SPF_STRING, spf_record):
spf_warning = _(
'We found an SPF record set for the domain you are trying to use, but it does not include this '
'system\'s email server. This means that there is a very high chance most of the emails will be '
'rejected or marked as spam. You should update the DNS settings of your domain to include '
'this system in the SPF record.'
)
if settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED:
if 'verification' in self.request.POST:
messages.error(request, _('The verification code was incorrect, please try again.'))
else:
self.request.session[session_key] = get_random_string(length=6, allowed_chars='1234567890')
mail(
self.simple_form.cleaned_data.get('mail_from'),
_('Sender address verification'),
'pretixcontrol/email/email_setup.txt',
{
'code': self.request.session[session_key],
'address': self.simple_form.cleaned_data.get('mail_from'),
'instance': settings.PRETIX_INSTANCE_NAME,
},
None,
locale=self.request.LANGUAGE_CODE,
user=self.request.user
)
return self.response_class(
request=self.request,
template='pretixcontrol/email_setup_simple.html',
context={
'basetpl': self.basetpl,
'object': self.object,
'verification': settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED,
'spf_warning': spf_warning,
'spf_record': spf_record,
'spf_key': settings.MAIL_CUSTOM_SENDER_SPF_STRING,
'recp': self.simple_form.cleaned_data.get('mail_from')
},
using=self.template_engine,
)
elif request.POST.get('mode') == 'smtp':
if not self.smtp_form.is_valid():
return super().get(request, *args, **kwargs)
if request.POST.get('state') == 'save':
for k, v in self.smtp_form.cleaned_data.items():
self.object.settings.set(k, v)
self.object.settings.smtp_use_custom = True
self.log_action({**self.smtp_form.cleaned_data, 'smtp_use_custom': True})
messages.success(request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
else:
backend = get_connection(
backend=settings.EMAIL_CUSTOM_SMTP_BACKEND,
host=self.smtp_form.cleaned_data['smtp_host'],
port=self.smtp_form.cleaned_data['smtp_port'],
username=self.smtp_form.cleaned_data.get('smtp_username', ''),
password=self.smtp_form.cleaned_data.get('smtp_password', ''),
use_tls=self.smtp_form.cleaned_data.get('smtp_use_tls', False),
use_ssl=self.smtp_form.cleaned_data.get('smtp_use_ssl', False),
fail_silently=False,
timeout=10,
)
try:
email.test_custom_smtp_backend(backend, self.smtp_form.cleaned_data.get('mail_from'))
except Exception as e:
messages.error(self.request, _('An error occurred while contacting the SMTP server: %s') % str(e))
return self.get(request, *args, **kwargs)
return self.response_class(
request=self.request,
template='pretixcontrol/email_setup_smtp.html',
context={
'basetpl': self.basetpl,
'object': self.object,
'known_host_problem': {
'smtp.gmail.com': _(
'We recommend not using Google Mail for transactional emails. If you try sending many '
'emails in a short amount of time, e.g. when sending information to all your ticket '
'buyers, there is a high chance Google will not deliver all of your emails since they '
'impose a maximum number of emails per time period.'
),
}.get(self.smtp_form.cleaned_data['smtp_host']),
},
using=self.template_engine,
)

View File

@@ -896,11 +896,6 @@ class OrderRefundView(OrderView):
if self.request.POST.get('manual_state') == 'done'
else OrderRefund.REFUND_STATE_CREATED
),
execution_date=(
now()
if self.request.POST.get('manual_state') == 'done'
else None
),
amount=manual_value,
comment=comment,
provider='manual'

View File

@@ -89,8 +89,8 @@ from pretix.control.forms.filter import (
)
from pretix.control.forms.orders import ExporterForm
from pretix.control.forms.organizer import (
CustomerCreateForm, CustomerUpdateForm, DeviceForm, EventMetaPropertyForm,
GateForm, GiftCardCreateForm, GiftCardUpdateForm, MailSettingsForm,
CustomerUpdateForm, DeviceForm, EventMetaPropertyForm, GateForm,
GiftCardCreateForm, GiftCardUpdateForm, MailSettingsForm,
MembershipTypeForm, MembershipUpdateForm, OrganizerDeleteForm,
OrganizerForm, OrganizerSettingsForm, OrganizerUpdateForm, TeamForm,
WebHookForm,
@@ -101,7 +101,6 @@ from pretix.control.permissions import (
)
from pretix.control.signals import nav_organizer
from pretix.control.views import PaginationMixin
from pretix.control.views.mailsetup import MailSettingsSetupView
from pretix.helpers.dicts import merge_dicts
from pretix.helpers.urls import build_absolute_uri as build_global_uri
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -262,6 +261,22 @@ class OrganizerMailSettings(OrganizerSettingsFormView):
k: form.cleaned_data.get(k) for k in form.changed_data
}
)
if request.POST.get('test', '0').strip() == '1':
backend = self.request.organizer.get_mail_backend(force_custom=True, timeout=10)
try:
backend.test(self.request.organizer.settings.mail_from)
except Exception as e:
messages.warning(self.request, _('An error occurred while contacting the SMTP server: %s') % str(e))
else:
if form.cleaned_data.get('smtp_use_custom'):
messages.success(self.request, _('Your changes have been saved and the connection attempt to '
'your SMTP server was successful.'))
else:
messages.success(self.request, _('We\'ve been able to contact the SMTP server you configured. '
'Remember to check the "use custom SMTP server" checkbox, '
'otherwise your SMTP server will not be used.'))
else:
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
else:
@@ -269,21 +284,6 @@ class OrganizerMailSettings(OrganizerSettingsFormView):
return self.get(request)
class MailSettingsSetup(OrganizerPermissionRequiredMixin, MailSettingsSetupView):
permission = 'can_change_organizer_settings'
basetpl = 'pretixcontrol/base.html'
def get_success_url(self):
return reverse('control:organizer.settings.mail', kwargs={
'organizer': self.request.organizer.slug,
})
def log_action(self, data):
self.request.organizer.log_action(
'pretix.organizer.settings', user=self.request.user, data=data
)
class MailSettingsPreview(OrganizerPermissionRequiredMixin, View):
permission = 'can_change_organizer_settings'
@@ -489,7 +489,6 @@ class OrganizerCreate(CreateView):
organizer=form.instance, name=_('Administrators'),
all_events=True, can_create_events=True, can_change_teams=True, can_manage_gift_cards=True,
can_change_organizer_settings=True, can_change_event_settings=True, can_change_items=True,
can_manage_customers=True,
can_view_orders=True, can_change_orders=True, can_view_vouchers=True, can_change_vouchers=True
)
t.members.add(self.request.user)
@@ -1195,10 +1194,6 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
'retry': True,
})
)
t.order.log_action('pretix.event.order.payment.started', {
'local_id': r.local_id,
'provider': r.provider
}, user=request.user)
try:
r.payment_provider.execute_payment(request, r)
except PaymentException as e:
@@ -1868,35 +1863,6 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
return ctx
class CustomerCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
template_name = 'pretixcontrol/organizers/customer_edit.html'
permission = 'can_manage_customers'
context_object_name = 'customer'
form_class = CustomerCreateForm
def get_form_kwargs(self):
ctx = super().get_form_kwargs()
c = Customer(organizer=self.request.organizer)
c.assign_identifier()
ctx['instance'] = c
return ctx
def form_valid(self, form):
r = super().form_valid(form)
form.instance.log_action('pretix.customer.created', user=self.request.user, data={
k: getattr(form.instance, k)
for k in form.changed_data
})
messages.success(self.request, _('Your changes have been saved.'))
return r
def get_success_url(self):
return reverse('control:organizer.customer', kwargs={
'organizer': self.request.organizer.slug,
'customer': self.object.identifier,
})
class CustomerUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
template_name = 'pretixcontrol/organizers/customer_edit.html'
permission = 'can_manage_customers'

View File

@@ -25,10 +25,8 @@ from django.utils.functional import cached_property
from django.views.generic import ListView
from pretix.base.models import Order, OrderPosition
from pretix.base.models.orders import CancellationRequest, OrderPayment
from pretix.control.forms.filter import (
OrderPaymentSearchFilterForm, OrderSearchFilterForm,
)
from pretix.base.models.orders import CancellationRequest
from pretix.control.forms.filter import OrderSearchFilterForm
from pretix.control.views import LargeResultSetPaginator, PaginationMixin
@@ -138,73 +136,3 @@ class OrderSearch(PaginationMixin, ListView):
).prefetch_related(
'event', 'event__organizer'
).select_related('invoice_address')
class PaymentSearch(PaginationMixin, ListView):
model = OrderPayment
paginator_class = LargeResultSetPaginator
context_object_name = 'payments'
template_name = 'pretixcontrol/search/payments.html'
@cached_property
def filter_form(self):
return OrderPaymentSearchFilterForm(data=self.request.GET, request=self.request)
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['filter_form'] = self.filter_form
return ctx
def get_queryset(self):
qs = OrderPayment.objects.using(settings.DATABASE_REPLICA)
if not self.request.user.has_active_staff_session(self.request.session.session_key):
qs = qs.filter(
Q(order__event_id__in=self.request.user.get_events_with_permission('can_view_orders').values_list('id', flat=True))
)
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
if self.filter_form.cleaned_data.get('query'):
"""
We need to work around a bug in PostgreSQL's (and likely MySQL's) query plan optimizer here.
The database lacks statistical data to predict how common our search filter is and therefore
assumes that it is cheaper to first ORDER *all* orders in the system (since we got an index on
datetime), then filter out with a full scan until OFFSET/LIMIT condition is fulfilled. If we
look for something rare (such as an email address used once within hundreds of thousands of
orders, this ends up to be pathologically slow.
For some search queries on pretix.eu, we see search times of >30s, just due to the ORDER BY and
LIMIT clause. Without them. the query runs in roughly 0.6s. This heuristical approach tries to
detect these cases and rewrite the query as a nested subquery that strongly suggests sorting
before filtering. However, since even that fails in some cases because PostgreSQL thinks it knows
better, we literally force it by evaluating the subquery explicitly. We only do this for n<=200,
to avoid memory leaks and problems with maximum parameter count on SQLite. In cases where the
search query yields lots of results, this will actually be slower since it requires two queries,
sorry.
Phew.
"""
page = self.kwargs.get(self.page_kwarg) or self.request.GET.get(self.page_kwarg) or 1
limit = self.get_paginate_by(None)
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 = OrderPayment.objects.using(settings.DATABASE_REPLICA).filter(
id__in=resultids
)
"""
We use prefetch_related here instead of select_related for a reason, even though select_related
would be the common choice for a foreign key and doesn't require an additional database query.
The problem is, that if our results contain the same event 25 times, select_related will create
25 Django objects which will all try to pull their ownsettings cache to show the event properly,
leading to lots of unnecessary queries. Due to the way prefetch_related works differently, it
will only create one shared Django object.
"""
return qs.prefetch_related('order', 'order__event', 'order__event__organizer')

View File

@@ -20,13 +20,9 @@
# <https://www.gnu.org/licenses/>.
#
import json
from contextlib import contextmanager
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import (
BACKEND_SESSION_KEY, get_user_model, load_backend, login,
)
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
@@ -34,7 +30,7 @@ from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.generic import ListView, TemplateView
from hijack import signals
from hijack.helpers import login_user, release_hijack
from pretix.base.auth import get_auth_backends
from pretix.base.models import User
@@ -46,25 +42,6 @@ from pretix.control.views import CreateView, UpdateView
from pretix.control.views.user import RecentAuthenticationRequiredMixin
def get_used_backend(request):
# vendored from hijack/views.py
backend_str = request.session[BACKEND_SESSION_KEY]
backend = load_backend(backend_str)
return backend
@contextmanager
def keep_session_age(session):
# vendored from hijack/views.py
try:
session_expiry = session["_session_expiry"]
except KeyError:
yield
else:
yield
session["_session_expiry"] = session_expiry
class UserListView(AdministratorPermissionRequiredMixin, ListView):
template_name = 'pretixcontrol/users/index.html'
context_object_name = 'users'
@@ -194,28 +171,7 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
'other_email': self.object.email
})
oldkey = request.session.session_key
hijacker = request.user
hijacked = self.object
hijack_history = request.session.get("hijack_history", [])
hijack_history.append(request.user._meta.pk.value_to_string(hijacker))
backend = get_used_backend(request)
backend = f"{backend.__module__}.{backend.__class__.__name__}"
with signals.no_update_last_login(), keep_session_age(request.session):
login(request, hijacked, backend=backend)
request.session["hijack_history"] = hijack_history
signals.hijack_started.send(
sender=None,
request=request,
hijacker=hijacker,
hijacked=hijacked,
)
login_user(request, self.object)
request.session['hijacker_session'] = oldkey
return redirect(reverse('control:index'))
@@ -224,26 +180,8 @@ class UserImpersonateStopView(LoginRequiredMixin, View):
def post(self, request, *args, **kwargs):
impersonated = request.user
hijs = request.session['hijacker_session']
hijack_history = request.session.get("hijack_history", [])
hijacked = request.user
user_pk = hijack_history.pop()
hijacker = get_object_or_404(get_user_model(), pk=user_pk)
backend = get_used_backend(request)
backend = f"{backend.__module__}.{backend.__class__.__name__}"
with signals.no_update_last_login(), keep_session_age(request.session):
login(request, hijacker, backend=backend)
request.session["hijack_history"] = hijack_history
signals.hijack_ended.send(
sender=None,
request=request,
hijacker=hijacker,
hijacked=hijacked,
)
release_hijack(request)
ss = request.user.get_active_staff_session(hijs)
if ss:
request.session.save()

View File

@@ -25,7 +25,3 @@ from django.apps import AppConfig
class PretixHelpersConfig(AppConfig):
name = 'pretix.helpers'
label = 'pretixhelpers'
def ready(self):
from .monkeypatching import monkeypatch_all_at_ready
monkeypatch_all_at_ready()

View File

@@ -32,11 +32,10 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
from django.template.defaultfilters import date as _date
from django.utils.html import format_html
from django.utils.translation import get_language, gettext_lazy as _
from pretix.helpers.templatetags.date_fast import date_fast as _date
def daterange(df, dt, as_html=False):
lng = get_language()

View File

@@ -39,4 +39,3 @@ SHORT_DATETIME_FORMAT = 'Y-m-d H:i'
TIME_FORMAT = 'H:i'
WEEK_FORMAT = '\\W W, o'
WEEK_DAY_FORMAT = 'D, M jS'
SHORT_MONTH_DAY_FORMAT = 'd.m.'

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