Compare commits

...

65 Commits

Author SHA1 Message Date
Mira Weller
f24a24baf6 deprecation notice for logentry_display and logentry_object_link 2024-12-18 11:28:18 +01:00
Mira Weller
579aacae39 Implement schema validation 2024-12-17 16:49:58 +01:00
Mira Weller
2497c12811 Code style 2024-12-17 16:45:51 +01:00
Mira Weller
2d814c6a1b Add license header 2024-12-17 16:44:38 +01:00
Mira Weller
9e29d2b847 Fix bug 2024-12-17 16:28:35 +01:00
Mira Weller
450b4232dd Fix bug 2024-12-17 16:14:21 +01:00
Mira Weller
f60dde3c3a Add documentation 2024-12-17 12:22:28 +01:00
Mira Weller
074c632260 Merge branch 'master' into logentrytype-registry
# Conflicts:
#	src/pretix/control/logdisplay.py
2024-12-10 14:27:22 +01:00
Raphael Michel
b8ad276f53 Fix duplicate field (Z#23168530) 2024-12-09 21:58:11 +01:00
Raphael Michel
e109c37738 API: use correct validation exception 2024-12-09 14:17:27 +01:00
Raphael Michel
4d597d5be3 Stripe: Remove option to enable method_sofort (#4638) 2024-12-09 13:51:32 +01:00
Mira
ae8ec42905 Fix backend validation if name is required as part of a required non-business invoice address (#4674)
---------

Co-authored-by: Raphael Michel <michel@rami.io>
2024-12-09 12:27:59 +01:00
Richard Schreiber
e5b89e9b08 [A11y] fix color-contrast for event-list status-bubbles 2024-12-06 15:51:40 +01:00
Mira
da91f5f117 Animation in <fieldset> accordion (#4682) 2024-12-06 12:46:40 +01:00
Richard Schreiber
ae29240e58 [A11y] Improve number inputs for screen-readers 2024-12-06 11:11:36 +01:00
Richard Schreiber
74edf10b04 Move cursor fix for disabled fieldsets from bootstrap to presale 2024-12-06 11:07:11 +01:00
Richard Schreiber
e2e0eca872 Fix accordion-radio fieldset css 2024-12-06 10:13:32 +01:00
Raphael Michel
6132e4a2c4 Remove re-implementations if i18nfield API integration 2024-12-05 17:56:30 +01:00
Raphael Michel
7df7d28518 Bump django-i18nfield to 1.9.5 2024-12-05 17:56:30 +01:00
Raphael Michel
11ab5c5eeb Event dashboard: Use intcomma in numbers (Z#23175343) (#4687) 2024-12-05 17:11:57 +01:00
CVZ-es
20211d2097 Translations: Update Spanish
Currently translated at 100.0% (5782 of 5782 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/es/

powered by weblate
2024-12-05 13:46:11 +01:00
CVZ-es
d760ad38bf Translations: Update French
Currently translated at 100.0% (232 of 232 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/fr/

powered by weblate
2024-12-05 13:46:11 +01:00
CVZ-es
69af2cee93 Translations: Update French
Currently translated at 100.0% (5782 of 5782 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fr/

powered by weblate
2024-12-05 13:46:11 +01:00
Raphael Michel
6b199a2b9c Allow to enter gift cards into the voucher input (Z#23171961) (#4670) 2024-12-05 13:43:46 +01:00
Richard Schreiber
94a64ba53a Hide empty panels (#4684) 2024-12-05 12:08:33 +01:00
Raphael Michel
70f06a8f40 Fix an incorrect exception handling 2024-12-04 16:59:58 +01:00
Mira Weller
a747ab154a Fix cursor on elements in disabled fieldsets' legends 2024-12-04 14:54:49 +01:00
Mira
6317233150 New accordion panels using <fieldset> (#4681) 2024-12-04 14:34:42 +01:00
Richard Schreiber
4d94158ff0 Improve organizer/event-series calendar UI on mobile 2024-12-04 08:17:52 +01:00
Raphael Michel
8f92eb2d2d remove debug statement 2024-12-03 12:40:29 +01:00
Richard Schreiber
f29896b267 [A11y] Fix missing aria-hidden and translation 2024-12-03 12:10:16 +01:00
Raphael Michel
2dc625cf31 Add the option to introduce rich-text placeholders (#4657)
* Add the option to introduce rich-text placeholders

* Add tests in test_format

* Add some css

* Block vs inline

* Some fixed css

* Update src/pretix/control/forms/event.py

Co-authored-by: Mira <weller@rami.io>

* Add missing docstring prat

---------

Co-authored-by: Mira <weller@rami.io>
2024-12-03 11:38:15 +01:00
dependabot[bot]
855226d37c Update ua-parser requirement from ==0.18.* to ==1.0.* (#4665)
Updates the requirements on [ua-parser](https://github.com/ua-parser/uap-python) to permit the latest version.
- [Release notes](https://github.com/ua-parser/uap-python/releases)
- [Commits](https://github.com/ua-parser/uap-python/compare/0.18.0...1.0.0)

---
updated-dependencies:
- dependency-name: ua-parser
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-03 11:18:37 +01:00
dependabot[bot]
648c0da9fe Update webauthn requirement from ==2.2.* to ==2.3.* (#4655)
Updates the requirements on [webauthn](https://github.com/duo-labs/py_webauthn) to permit the latest version.
- [Release notes](https://github.com/duo-labs/py_webauthn/releases)
- [Changelog](https://github.com/duo-labs/py_webauthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/duo-labs/py_webauthn/compare/v2.2.0...v2.3.0)

---
updated-dependencies:
- dependency-name: webauthn
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-03 11:01:05 +01:00
Raphael Michel
59e3494fa2 Add fee type for late fees (#4656) 2024-12-03 11:00:11 +01:00
Mira
c4ff57c07a Change error message for unavailable addon products (#4673)
This can not only happen in case of sold-out addons, but also if they are e.g. not available via the current sales channel.
2024-12-03 10:59:15 +01:00
Raphael Michel
cc4fbfe4c7 API: Allow to block/unblock seats in bulk (#4668)
* API: Allow to block/unblock seats in bulk

* Update doc/api/resources/seats.rst

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update doc/api/resources/seats.rst

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update doc/api/resources/seats.rst

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update src/pretix/api/views/event.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-12-02 16:03:11 +01:00
Raphael Michel
e99ee91573 Allow to use custom domains for some but not all events (Z#23153875) (#4627)
* Allow to use custom domains for some but not all events

* Update src/pretix/multidomain/urlreverse.py

* Apply suggestions from code review

Co-authored-by: Mira <weller@rami.io>

* Logging for domain config changes

---------

Co-authored-by: Mira <weller@rami.io>
2024-12-02 15:58:50 +01:00
Patrick Chilton
e2753686ee Translations: Update Hungarian
Currently translated at 10.8% (629 of 5782 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/hu/

powered by weblate
2024-12-02 15:58:41 +01:00
CVZ-es
33f8b9851e Translations: Update Spanish
Currently translated at 100.0% (5782 of 5782 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/es/

powered by weblate
2024-12-02 15:58:41 +01:00
CVZ-es
e3d8cf07af Translations: Update French
Currently translated at 100.0% (5782 of 5782 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fr/

powered by weblate
2024-12-02 15:58:41 +01:00
Mira Weller
0279ca7d94 Add missing error handling to addressform.js 2024-12-02 10:15:16 +01:00
Richard Schreiber
d1989c3cd3 Fix all-optional in address-form for resellers (#4672) 2024-12-02 09:46:33 +01:00
Raphael Michel
61cb2e15cf Fix validation crash of InvoiceNameForm 2024-11-29 20:08:36 +01:00
Mira Weller
f2ee1d00b3 Don't use animation for address information load indicator 2024-11-29 17:09:14 +01:00
Mira
e8e9698a31 Update address field logic (Z#23163120) (#4659)
* Move country-dependent JS logic to separate file (avoids code duplication for presale and control)
* Correctly apply "required" attribute to address state field
* Load address format information when selecting country
* Fix some other bugs and inconsistencies
2024-11-29 14:56:56 +01:00
Richard Schreiber
a1bf7be244 [A11y] Improve customer account pages (#4654) 2024-11-29 14:16:40 +01:00
Patrick Chilton
f4ca9a5681 Translations: Update Hungarian
Currently translated at 45.6% (106 of 232 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/hu/

powered by weblate
2024-11-29 09:22:29 +01:00
dependabot[bot]
e6d984538f Update django-statici18n requirement from ==2.5.* to ==2.6.* (#4664)
Updates the requirements on [django-statici18n](https://github.com/zyegfryed/django-statici18n) to permit the latest version.
- [Changelog](https://github.com/zyegfryed/django-statici18n/blob/main/docs/changelog.rst)
- [Commits](https://github.com/zyegfryed/django-statici18n/compare/v2.5.0...v2.6.0)

---
updated-dependencies:
- dependency-name: django-statici18n
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-29 09:22:04 +01:00
dependabot[bot]
9f1ee9157f Update protobuf requirement from ==5.28.* to ==5.29.* (#4666)
Updates the requirements on [protobuf](https://github.com/protocolbuffers/protobuf) to permit the latest version.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/main/protobuf_release.bzl)
- [Commits](https://github.com/protocolbuffers/protobuf/compare/v5.28.0-rc1...v5.29.0)

---
updated-dependencies:
- dependency-name: protobuf
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-29 09:21:54 +01:00
Raphael Michel
242e5af4b5 Bump version to 2024.12.0.dev0 2024-11-27 13:57:03 +01:00
Mira Weller
85fe94f84b fix imports 2024-10-11 17:25:25 +02:00
Mira Weller
80d1b30278 Merge remote-tracking branch 'origin/master' into logentrytype-registry 2024-10-11 14:11:27 +02:00
Mira Weller
fe67b70766 Fix circular imports 2024-09-23 18:45:49 +02:00
Mira Weller
47b84c06b0 code style 2024-09-03 12:06:18 +02:00
Mira Weller
204bc84e85 move logentrytypes to own module 2024-09-02 17:44:01 +02:00
Mira Weller
0487d5803b move CORE_MODULES to base settings so it's already available when registering types 2024-09-02 17:43:31 +02:00
Mira Weller
a94c89ba4f add shredder mixins 2024-09-02 17:43:31 +02:00
Mira Weller
2045009e2e refactor signal receiver active check 2024-09-02 17:43:31 +02:00
Mira Weller
9269a485a6 store plugin name for registered types 2024-09-02 17:43:31 +02:00
Mira Weller
166f50fcb0 refactor: simplify is_active / core_module logic 2024-09-02 17:43:31 +02:00
Mira Weller
a3358bae6b migrate from logentry_display signal to LogEntryTypes 2024-09-02 17:43:27 +02:00
Mira Weller
a3164a94b7 refactor: use logentry.parsed_data 2024-09-02 17:38:52 +02:00
Mira Weller
f56df892e3 remove dead code 2024-09-02 17:38:52 +02:00
Mira Weller
093a0db182 implement registration facility for log entry types 2024-09-02 17:38:52 +02:00
120 changed files with 4032 additions and 1939 deletions

View File

@@ -249,7 +249,7 @@ Endpoints
"orderposition": null,
"cartposition": null,
"voucher": null
},
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
@@ -260,3 +260,114 @@ Endpoints
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/seats/bulk_block/
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/seats/bulk_block/
Set the ``blocked`` attribute to ``true`` for a large number of seats at once.
You can pass either a list of ``id`` values or a list of ``seat_guid`` values.
You can pass up to 10,000 seats in one request.
The endpoint will return an error if you pass a seat ID that does not exist.
However, it will not return an error if one of the passed seats is already blocked or sold.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_block/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"ids": [12, 45, 56]
}
or
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_block/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"seat_guids": ["6c0e29e5-05d6-421f-99f3-afd01478ecad", "c2899340-e2e7-4d05-8100-000a4b6d7cf4"]
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param subevent_id: The ``id`` field of the subevent to modify
:statuscode 200: no error
:statuscode 400: The seat could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/seats/bulk_unblock/
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/seats/bulk_unblock/
Set the ``blocked`` attribute to ``false`` for a large number of seats at once.
You can pass either a list of ``id`` values or a list of ``seat_guid`` values.
You can pass up to 10,000 seats in one request.
The endpoint will return an error if you pass a seat ID that does not exist.
However, it will not return an error if one of the passed seat is already unblocked or is sold.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_unblock/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"ids": [12, 45, 56]
}
or
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_unblock/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"seat_guids": ["6c0e29e5-05d6-421f-99f3-afd01478ecad", "c2899340-e2e7-4d05-8100-000a4b6d7cf4"]
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param subevent_id: The ``id`` field of the subevent to modify
:statuscode 200: no error
:statuscode 400: The seat could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.

View File

@@ -121,6 +121,7 @@ This will automatically make pretix discover this plugin as soon as it is instal
through ``pip``. During development, you can just run ``python setup.py develop`` inside
your plugin source directory to make it discoverable.
.. _`Signals`:
Signals
-------
@@ -153,6 +154,28 @@ in the ``installed`` method:
Note that ``installed`` will *not* be called if the plugin is indirectly activated for an event
because the event is created with settings copied from another event.
.. _`Registries`:
Registries
----------
Many signals in pretix are used so that plugins can "register" a class, e.g. a payment provider or a
ticket renderer.
However, for some of them (types of :ref:`Log Entries <logging>`) we use a different method to keep track of them:
In a ``Registry``, classes are collected at application startup, along with a unique key (in case
of LogEntryType, the action_type) as well as which plugin registered them.
To register a class, you can use one of several decorator provided by the Registry object:
.. code-block:: python
@log_entry_types.new('my_pretix_plugin.some.action', _('Some action in My Pretix Plugin occured.'))
class MyPretixPluginLogEntryType(EventLogEntryType):
pass
All files in which classes are registered need to be imported in the ``AppConfig.ready`` as explained
in `Signals`_ above.
Views
-----

View File

@@ -20,7 +20,8 @@ To actually log an action, you can just call the ``log_action`` method on your o
.. code-block:: python
order.log_action('pretix.event.order.canceled', user=user, data={})
order.log_action('pretix.event.order.comment', user=user,
data={"new_comment": "Hello, world."})
The positional ``action`` argument should represent the type of action and should be globally unique, we
recommend to prefix it with your package name, e.g. ``paypal.payment.rejected``. The ``user`` argument is
@@ -72,24 +73,87 @@ following ready-to-include template::
{% include "pretixcontrol/includes/logs.html" with obj=order %}
We now need a way to translate the action codes like ``pretix.event.changed`` into human-readable
strings. The :py:attr:`pretix.base.signals.logentry_display` signals allows you to do so. A simple
strings. The :py:attr:`pretix.base.logentrytypes.log_entry_types` :ref:`Registry <Registries>` allows you to do so. A simple
implementation could look like:
.. code-block:: python
from django.utils.translation import gettext as _
from pretix.base.signals import logentry_display
from pretix.base.logentrytypes import log_entry_types
@receiver(signal=logentry_display)
def pretixcontrol_logentry_display(sender, logentry, **kwargs):
plains = {
'pretix.event.order.paid': _('The order has been marked as paid.'),
'pretix.event.order.refunded': _('The order has been refunded.'),
'pretix.event.order.canceled': _('The order has been canceled.'),
...
}
if logentry.action_type in plains:
return plains[logentry.action_type]
@log_entry_types.new_from_dict({
'pretix.event.order.comment': _('The order\'s internal comment has been updated to: {new_comment}'),
'pretix.event.order.paid': _('The order has been marked as paid.'),
# ...
})
class CoreOrderLogEntryType(OrderLogEntryType):
pass
Please note that you always need to define your own inherited LogEntryType class in your plugin. If you would just
register an instance of a LogEntryType class defined in pretix core, it is not correctly registered as belonging to
your plugin, leading to confusing user interface situations.
Customizing log entry display
""""""""""""""""""""""""""""""""""
The base LogEntryType classes allows for varying degree of customization in their descendants.
If you want to add another log message for an existing core object (e.g. an Order, Item or Voucher), you can inherit
from its predefined LogEntryType, e.g. `OrderLogEntryType` and just specify a new plaintext string. You can use format
strings to insert information from the LogEntry's `data` object as shown in the section above.
If you defined a new model object in your plugin, you should make sure proper object links in the user interface are
displayed for it. If your model object belongs logically to a pretix `Event`, you can inherit from `EventLogEntryType`,
and set the `object_link_*` fields accordingly. `object_link_viewname` refers to a django url name, which needs to
accept the arguments `organizer` and `event`, containing the respective slugs, and an argument named by `object_link_argname`.
The latter will contain the ID of the model object, if not customized by overriding `object_link_argvalue`.
If you want to customize the name displayed for the object (instead of the result of calling `str()` on it),
overwrite `object_link_display_name`.
.. code-block:: python
class OrderLogEntryType(EventLogEntryType):
object_link_wrapper = _('Order {val}')
object_link_viewname = 'control:event.order'
object_link_argname = 'code'
def object_link_argvalue(self, order):
return order.code
def object_link_display_name(self, order):
return order.code
To show more sophisticated message strings, e.g. varying the message depending on information from the LogEntry's
`data` object, overwrite the `display` method:
.. code-block:: python
@log_entry_types.new()
class PaypalEventLogEntryType(EventLogEntryType):
action_type = 'pretix.plugins.paypal.event'
def display(self, logentry):
event_type = logentry.parsed_data.get('event_type')
text = {
'PAYMENT.SALE.COMPLETED': _('Payment completed.'),
'PAYMENT.SALE.DENIED': _('Payment denied.'),
# ...
}.get(event_type, f"({event_type})")
return _('PayPal reported an event: {}').format(text)
.. automethod:: pretix.base.logentrytypes.LogEntryType.display
If your new model object does not belong to an `Event`, you need to implement
meow
.. autoclass:: pretix.base.logentrytypes.Registry
:members: new
.. autoclass:: pretix.base.logentrytypes.LogEntryTypeRegistry
:members: new, new_from_dict
Sending notifications
---------------------

View File

@@ -44,7 +44,7 @@ dependencies = [
"django-formtools==2.5.1",
"django-hierarkey==1.2.*",
"django-hijack==3.7.*",
"django-i18nfield==1.9.*,>=1.9.4",
"django-i18nfield==1.9.*,>=1.9.5",
"django-libsass==0.9",
"django-localflavor==4.0",
"django-markup",
@@ -53,7 +53,7 @@ dependencies = [
"django-phonenumber-field==7.3.*",
"django-redis==5.4.*",
"django-scopes==2.0.*",
"django-statici18n==2.5.*",
"django-statici18n==2.6.*",
"djangorestframework==3.15.*",
"dnspython==2.7.*",
"drf_ujson2==1.7.*",
@@ -76,7 +76,7 @@ dependencies = [
"phonenumberslite==8.13.*",
"Pillow==11.0.*",
"pretix-plugin-build",
"protobuf==5.28.*",
"protobuf==5.29.*",
"psycopg2-binary",
"pycountry",
"pycparser==2.22",
@@ -97,10 +97,10 @@ dependencies = [
"text-unidecode==1.*",
"tlds>=2020041600",
"tqdm==4.*",
"ua-parser==0.18.*",
"ua-parser==1.0.*",
"vat_moss_forked==2020.3.20.0.11.0",
"vobject==0.9.*",
"webauthn==2.2.*",
"webauthn==2.3.*",
"zeep==4.3.*"
]

View File

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

View File

@@ -75,6 +75,14 @@ FORMAT_MODULE_PATH = [
'pretix.helpers.formats',
]
CORE_MODULES = {
"pretix.base",
"pretix.presale",
"pretix.control",
"pretix.plugins.checkinlists",
"pretix.plugins.reports",
}
ALL_LANGUAGES = [
('en', _('English')),
('de', _('German')),

View File

@@ -35,7 +35,7 @@
import logging
from django.conf import settings
from django.core.exceptions import PermissionDenied, ValidationError
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
@@ -43,6 +43,7 @@ from django.utils.translation import gettext as _
from django_countries.serializers import CountryFieldMixin
from pytz import common_timezones
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.fields import ChoiceField, Field
from rest_framework.relations import SlugRelatedField
@@ -989,6 +990,40 @@ def prefetch_by_id(items, qs, id_attr, target_attr):
setattr(item, target_attr, result.get(getattr(item, id_attr)))
class SeatBulkBlockInputSerializer(serializers.Serializer):
ids = serializers.ListField(child=serializers.IntegerField(), required=False, allow_empty=True)
seat_guids = serializers.ListField(child=serializers.CharField(), required=False, allow_empty=True)
def to_internal_value(self, data):
data = super().to_internal_value(data)
if data.get("seat_guids") and data.get("ids"):
raise ValidationError("Please pass either seat_guids or ids.")
if data.get("seat_guids"):
seat_ids = data["seat_guids"]
if len(seat_ids) > 10000:
raise ValidationError({"seat_guids": ["Please do not pass over 10000 seats."]})
seats = {s.seat_guid: s for s in self.context["queryset"].filter(seat_guid__in=seat_ids)}
for s in seat_ids:
if s not in seats:
raise ValidationError({"seat_guids": [f"The seat '{s}' does not exist."]})
elif data.get("ids"):
seat_ids = data["ids"]
if len(seat_ids) > 10000:
raise ValidationError({"ids": ["Please do not pass over 10000 seats."]})
seats = self.context["queryset"].in_bulk(seat_ids)
for s in seat_ids:
if s not in seats:
raise ValidationError({"ids": [f"The seat '{s}' does not exist."]})
else:
raise ValidationError("Please pass either seat_guids or ids.")
return {"seats": seats.values()}
class SeatSerializer(I18nAwareModelSerializer):
orderposition = serializers.IntegerField(source='orderposition_id')
cartposition = serializers.IntegerField(source='cartposition_id')

View File

@@ -19,57 +19,8 @@
# 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.conf import settings
from django.core.validators import URLValidator
from i18nfield.fields import I18nCharField, I18nTextField
from i18nfield.strings import LazyI18nString
from rest_framework.exceptions import ValidationError
from rest_framework.fields import Field
from rest_framework.serializers import ModelSerializer
class I18nField(Field):
def __init__(self, **kwargs):
self.allow_blank = kwargs.pop('allow_blank', False)
self.trim_whitespace = kwargs.pop('trim_whitespace', True)
self.max_length = kwargs.pop('max_length', None)
self.min_length = kwargs.pop('min_length', None)
super().__init__(**kwargs)
def to_representation(self, value):
if hasattr(value, 'data'):
if isinstance(value.data, dict):
return value.data
elif value.data is None:
return None
else:
return {
settings.LANGUAGE_CODE: str(value.data)
}
elif value is None:
return None
else:
return {
settings.LANGUAGE_CODE: str(value)
}
def to_internal_value(self, data):
if isinstance(data, str):
return LazyI18nString(data)
elif isinstance(data, dict):
if any([k not in dict(settings.LANGUAGES) for k in data.keys()]):
raise ValidationError('Invalid languages included.')
return LazyI18nString(data)
else:
raise ValidationError('Invalid data type.')
class I18nAwareModelSerializer(ModelSerializer):
pass
I18nAwareModelSerializer.serializer_field_mapping[I18nCharField] = I18nField
I18nAwareModelSerializer.serializer_field_mapping[I18nTextField] = I18nField
from i18nfield.rest_framework import I18nAwareModelSerializer, I18nField
class I18nURLField(I18nField):
@@ -84,3 +35,10 @@ class I18nURLField(I18nField):
else:
URLValidator()(value.data)
return value
__all__ = [
"I18nAwareModelSerializer", # for backwards compatibility
"I18nField", # for backwards compatibility
"I18nURLField",
]

View File

@@ -40,6 +40,7 @@ from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import serializers, views, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import (
NotFound, PermissionDenied, ValidationError,
)
@@ -50,8 +51,9 @@ from pretix.api.auth.permission import EventCRUDPermission
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.event import (
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
EventSettingsSerializer, ItemMetaPropertiesSerializer, SeatSerializer,
SubEventSerializer, TaxRuleSerializer,
EventSettingsSerializer, ItemMetaPropertiesSerializer,
SeatBulkBlockInputSerializer, SeatSerializer, SubEventSerializer,
TaxRuleSerializer,
)
from pretix.api.views import ConditionalListView
from pretix.base.models import (
@@ -237,9 +239,9 @@ class EventViewSet(viewsets.ModelViewSet):
disabled = {m: 'disabled' for m in current_plugins_value if m not in updated_plugins_value}
changed = merge_dicts(enabled, disabled)
for module, action in changed.items():
for module, operation in changed.items():
serializer.instance.log_action(
'pretix.event.plugins.' + action,
'pretix.event.plugins.' + operation,
user=self.request.user,
auth=self.request.auth,
data={'plugin': module}
@@ -744,3 +746,24 @@ class SeatViewSet(ConditionalListView, viewsets.ModelViewSet):
auth=self.request.auth,
data={"seats": [serializer.instance.pk]},
)
def bulk_change_blocked(self, blocked):
s = SeatBulkBlockInputSerializer(
data=self.request.data,
context={"event": self.request.event, "queryset": self.get_queryset()},
)
s.is_valid(raise_exception=True)
seats = s.validated_data["seats"]
for seat in seats:
seat.blocked = blocked
Seat.objects.bulk_update(seats, ["blocked"], batch_size=1000)
return Response({})
@action(methods=["POST"], detail=False)
def bulk_block(self, request, *args, **kwargs):
return self.bulk_change_blocked(True)
@action(methods=["POST"], detail=False)
def bulk_unblock(self, request, *args, **kwargs):
return self.bulk_change_blocked(False)

View File

@@ -35,6 +35,7 @@ from django.utils.translation import get_language, gettext_lazy as _
from pretix.base.models import Event
from pretix.base.signals import register_html_mail_renderers
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.helpers.format import SafeFormatter, format_map
from pretix.base.services.placeholders import ( # noqa
get_available_placeholders, PlaceholderContext
@@ -79,7 +80,7 @@ class BaseHTMLMailRenderer:
return self.identifier
def render(self, plain_body: str, plain_signature: str, subject: str, order=None,
position=None) -> str:
position=None, context=None) -> str:
"""
This method should generate the HTML part of the email.
@@ -88,6 +89,7 @@ class BaseHTMLMailRenderer:
:param subject: The email subject.
:param order: The order if this email is connected to one, otherwise ``None``.
:param position: The order position if this email is connected to one, otherwise ``None``.
:param context: Context to use to render placeholders in the plain body
:return: An HTML string
"""
raise NotImplementedError()
@@ -134,8 +136,10 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
def compile_markdown(self, plaintext):
return markdown_compile_email(plaintext)
def render(self, plain_body: str, plain_signature: str, subject: str, order, position) -> str:
def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str:
body_md = self.compile_markdown(plain_body)
if context:
body_md = format_map(body_md, context=context, mode=SafeFormatter.MODE_RICH_TO_HTML)
htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME,
'site_url': settings.SITE_URL,

View File

@@ -54,6 +54,7 @@ from django.core.validators import (
from django.db.models import QuerySet
from django.forms import Select, widgets
from django.forms.widgets import FILE_INPUT_CONTRADICTION
from django.urls import reverse
from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.safestring import mark_safe
@@ -77,7 +78,7 @@ from pretix.base.i18n import (
get_babel_locale, get_language_without_region, language,
)
from pretix.base.models import InvoiceAddress, Item, Question, QuestionOption
from pretix.base.models.tax import VAT_ID_COUNTRIES, ask_for_vat_id
from pretix.base.models.tax import ask_for_vat_id
from pretix.base.services.tax import (
VATIDFinalError, VATIDTemporaryError, validate_vat_id,
)
@@ -276,6 +277,10 @@ class NamePartsFormField(forms.MultiValueField):
return value
def name_parts_is_empty(name_parts_dict):
return not any(k != "_scheme" and v for k, v in name_parts_dict.items())
class WrappedPhonePrefixSelect(Select):
initial = None
@@ -602,6 +607,7 @@ class BaseQuestionsForm(forms.Form):
questions = pos.item.questions_to_ask
event = kwargs.pop('event')
self.all_optional = kwargs.pop('all_optional', False)
self.attendee_addresses_required = event.settings.attendee_addresses_required and not self.all_optional
super().__init__(*args, **kwargs)
@@ -676,7 +682,7 @@ class BaseQuestionsForm(forms.Form):
if item.ask_attendee_data and event.settings.attendee_addresses_asked:
add_fields['street'] = forms.CharField(
required=event.settings.attendee_addresses_required and not self.all_optional,
required=self.attendee_addresses_required,
label=_('Address'),
widget=forms.Textarea(attrs={
'rows': 2,
@@ -686,7 +692,7 @@ class BaseQuestionsForm(forms.Form):
initial=(cartpos.street if cartpos else orderpos.street),
)
add_fields['zipcode'] = forms.CharField(
required=event.settings.attendee_addresses_required and not self.all_optional,
required=False,
max_length=30,
label=_('ZIP code'),
initial=(cartpos.zipcode if cartpos else orderpos.zipcode),
@@ -695,7 +701,7 @@ class BaseQuestionsForm(forms.Form):
}),
)
add_fields['city'] = forms.CharField(
required=event.settings.attendee_addresses_required and not self.all_optional,
required=False,
label=_('City'),
max_length=255,
initial=(cartpos.city if cartpos else orderpos.city),
@@ -707,11 +713,12 @@ class BaseQuestionsForm(forms.Form):
add_fields['country'] = CountryField(
countries=CachedCountries
).formfield(
required=event.settings.attendee_addresses_required and not self.all_optional,
required=self.attendee_addresses_required,
label=_('Country'),
initial=country,
widget=forms.Select(attrs={
'autocomplete': 'country',
'data-country-information-url': reverse('js_helpers.states'),
}),
)
c = [('', pgettext_lazy('address', 'Select state'))]
@@ -946,9 +953,9 @@ class BaseQuestionsForm(forms.Form):
d = super().clean()
if self.address_validation:
self.cleaned_data = d = validate_address(d, True)
self.cleaned_data = d = validate_address(d, all_optional=not self.attendee_addresses_required)
if d.get('city') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if d.get('street') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if not d.get('state'):
self.add_error('state', _('This field is required.'))
@@ -1005,7 +1012,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
'street': forms.Textarea(attrs={
'rows': 2,
'placeholder': _('Street and Number'),
'autocomplete': 'street-address'
'autocomplete': 'street-address',
}),
'beneficiary': forms.Textarea(attrs={'rows': 3}),
'country': forms.Select(attrs={
@@ -1021,7 +1028,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
'data-display-dependency': '#id_is_business_1',
'autocomplete': 'organization',
}),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1', 'data-countries-with-vat-id': ','.join(VAT_ID_COUNTRIES)}),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
'internal_reference': forms.TextInput,
}
labels = {
@@ -1055,6 +1062,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
])
self.fields['country'].choices = CachedCountries()
self.fields['country'].widget.attrs['data-country-information-url'] = reverse('js_helpers.states')
c = [('', pgettext_lazy('address', 'Select state'))]
fprefix = self.prefix + '-' if self.prefix else ''
@@ -1083,6 +1091,10 @@ class BaseInvoiceAddressForm(forms.ModelForm):
)
self.fields['state'].widget.is_required = True
self.fields['street'].required = False
self.fields['zipcode'].required = False
self.fields['city'].required = False
# Without JavaScript the VAT ID field is not hidden, so we empty the field if a country outside the EU is selected.
if cc and not ask_for_vat_id(cc) and fprefix + 'vat_id' in self.data:
self.data = self.data.copy()
@@ -1135,16 +1147,19 @@ class BaseInvoiceAddressForm(forms.ModelForm):
validate_address # local import to prevent impact on startup time
data = self.cleaned_data
if not data.get('is_business'):
data['company'] = ''
data['vat_id'] = ''
if data.get('is_business') and not ask_for_vat_id(data.get('country')):
data['vat_id'] = ''
if self.event.settings.invoice_address_required:
if self.address_validation and self.event.settings.invoice_address_required and not self.all_optional:
if data.get('is_business') and not data.get('company'):
raise ValidationError(_('You need to provide a company name.'))
if not data.get('is_business') and not data.get('name_parts'):
raise ValidationError({"company": _('You need to provide a company name.')})
if not data.get('is_business') and name_parts_is_empty(data.get('name_parts', {})):
raise ValidationError(_('You need to provide your name.'))
if not data.get('street') and not data.get('zipcode') and not data.get('city'):
raise ValidationError({"street": _('This field is required.')})
if 'vat_id' in self.changed_data or not data.get('vat_id'):
self.instance.vat_id_validated = False
@@ -1156,7 +1171,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
if all(
not v for k, v in data.items() if k not in ('is_business', 'country', 'name_parts')
) and len(data.get('name_parts', {})) == 1:
) and name_parts_is_empty(data.get('name_parts', {})):
# Do not save the country if it is the only field set -- we don't know the user even checked it!
self.cleaned_data['country'] = ''

View File

@@ -0,0 +1,288 @@
import json
#
# 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 collections import defaultdict
from functools import cached_property
import jsonschema
from django.urls import reverse
from django.utils.html import escape
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.signals import EventPluginRegistry
def make_link(a_map, wrapper, is_active=True, event=None, plugin_name=None):
if a_map:
if is_active:
a_map['val'] = '<a href="{href}">{val}</a>'.format_map(a_map)
elif event and plugin_name:
a_map['val'] = (
'<i>{val}</i> <a href="{plugin_href}">'
'<span data-toggle="tooltip" title="{errmes}" class="fa fa-warning fa-fw"></span></a>'
).format_map({
**a_map,
"errmes": _("The relevant plugin is currently not active. To activate it, click here to go to the plugin settings."),
"plugin_href": reverse('control:event.settings.plugins', kwargs={
'organizer': event.organizer.slug,
'event': event.slug,
}) + '#plugin_' + plugin_name,
})
else:
a_map['val'] = '<i>{val}</i> <span data-toggle="tooltip" title="{errmes}" class="fa fa-warning fa-fw"></span>'.format_map({
**a_map,
"errmes": _("The relevant plugin is currently not active."),
})
return wrapper.format_map(a_map)
class LogEntryTypeRegistry(EventPluginRegistry):
def new_from_dict(self, data):
"""
Register multiple instance of a LogEntryType class with different action_type
and plain text strings, as given by the items of the specified data dictionary.
This method is designed to be used as a decorator as follows:
.. code-block:: python
@log_entry_types.new_from_dict({
'pretix.event.item.added': _('The product has been created.'),
'pretix.event.item.changed': _('The product has been changed.'),
# ...
})
class CoreItemLogEntryType(ItemLogEntryType):
# ...
:param data: action types and descriptions
``{"some_action_type": "Plain text description", ...}``
"""
def reg(clz):
for action_type, plain in data.items():
self.register(clz(action_type=action_type, plain=plain))
return clz
return reg
"""
Registry for LogEntry types.
Each entry in this registry should be an instance of a subclass of ``LogEntryType``.
They are annotated with their ``action_type`` and the defining ``plugin``.
"""
log_entry_types = LogEntryTypeRegistry({'action_type': lambda o: getattr(o, 'action_type')})
def prepare_schema(schema):
def handle_properties(t):
return {"shred_properties": [k for k, v in t["properties"].items() if v["shred"]]}
def walk_tree(schema):
if type(schema) is dict:
new_keys = {}
for k, v in schema.items():
if k == "properties":
new_keys = handle_properties(schema)
walk_tree(v)
if schema.get("type") == "object" and "additionalProperties" not in new_keys:
new_keys["additionalProperties"] = False
schema.update(new_keys)
elif type(schema) is list:
for v in schema:
walk_tree(v)
walk_tree(schema)
return schema
class LogEntryType:
"""
Base class for a type of LogEntry, identified by its action_type.
"""
data_schema = None # {"type": "object", "properties": []}
def __init__(self, action_type=None, plain=None):
assert self.__module__ != LogEntryType.__module__ # must not instantiate base classes, only derived ones
if self.data_schema:
print(self.__class__.__name__, "has schema", self._prepared_schema)
if action_type:
self.action_type = action_type
if plain:
self.plain = plain
def display(self, logentry):
"""
Returns the message to be displayed for a given logentry of this type.
:return: `str` or `LazyI18nString`
"""
if hasattr(self, 'plain'):
plain = str(self.plain)
if '{' in plain:
data = defaultdict(lambda: '?', logentry.parsed_data)
return plain.format_map(data)
else:
return plain
def get_object_link_info(self, logentry) -> dict:
"""
Return information to generate a link to the content_object of a given logentry.
Not implemented in the base class, causing the object link to be omitted.
:return: `dict` with the keys `href` (containing a URL to view/edit the object) and `val` (containing the
escaped text for the anchor element)
"""
pass
def get_object_link(self, logentry):
a_map = self.get_object_link_info(logentry)
return make_link(a_map, self.object_link_wrapper)
object_link_wrapper = '{val}'
def validate_data(self, parsed_data):
if not self._prepared_schema:
return
jsonschema.validate(parsed_data, self._prepared_schema)
@cached_property
def _prepared_schema(self):
if self.data_schema:
return prepare_schema(self.data_schema)
def shred_pii(self, logentry):
"""
To be used for shredding personally identified information contained in the data field of a LogEntry of this
type.
"""
if self._prepared_schema:
def shred_fun(validator, value, instance, schema):
for key in value:
instance[key] = "##########"
v = jsonschema.validators.extend(jsonschema.validators.Draft202012Validator,
validators={"shred_properties": shred_fun})
data = logentry.parsed_data
jsonschema.validate(data, self._prepared_schema, v)
logentry.data = json.dumps(data)
else:
raise NotImplementedError
class EventLogEntryType(LogEntryType):
"""
Base class for any LogEntry type whose content_object is either an `Event` itself or belongs to a specific `Event`.
"""
def get_object_link_info(self, logentry) -> dict:
if hasattr(self, 'object_link_viewname') and hasattr(self, 'object_link_argname') and logentry.content_object:
return {
'href': reverse(self.object_link_viewname, kwargs={
'event': logentry.event.slug,
'organizer': logentry.event.organizer.slug,
self.object_link_argname: self.object_link_argvalue(logentry.content_object),
}),
'val': escape(self.object_link_display_name(logentry.content_object)),
}
def object_link_argvalue(self, content_object):
"""Return the identifier used in a link to content_object."""
return content_object.id
def object_link_display_name(self, content_object):
"""Return the display name to refer to content_object in the user interface."""
return str(content_object)
class OrderLogEntryType(EventLogEntryType):
object_link_wrapper = _('Order {val}')
object_link_viewname = 'control:event.order'
object_link_argname = 'code'
def object_link_argvalue(self, order):
return order.code
def object_link_display_name(self, order):
return order.code
class VoucherLogEntryType(EventLogEntryType):
object_link_wrapper = _('Voucher {val}')
object_link_viewname = 'control:event.voucher'
object_link_argname = 'voucher'
def object_link_display_name(self, voucher):
return voucher.code[:6]
class ItemLogEntryType(EventLogEntryType):
object_link_wrapper = _('Product {val}')
object_link_viewname = 'control:event.item'
object_link_argname = 'item'
class SubEventLogEntryType(EventLogEntryType):
object_link_wrapper = pgettext_lazy('subevent', 'Date {val}')
object_link_viewname = 'control:event.subevent'
object_link_argname = 'subevent'
class QuotaLogEntryType(EventLogEntryType):
object_link_wrapper = _('Quota {val}')
object_link_viewname = 'control:event.items.quotas.show'
object_link_argname = 'quota'
class DiscountLogEntryType(EventLogEntryType):
object_link_wrapper = _('Discount {val}')
object_link_viewname = 'control:event.items.discounts.edit'
object_link_argname = 'discount'
class ItemCategoryLogEntryType(EventLogEntryType):
object_link_wrapper = _('Category {val}')
object_link_viewname = 'control:event.items.categories.edit'
object_link_argname = 'category'
class QuestionLogEntryType(EventLogEntryType):
object_link_wrapper = _('Question {val}')
object_link_viewname = 'control:event.items.questions.show'
object_link_argname = 'question'
class TaxRuleLogEntryType(EventLogEntryType):
object_link_wrapper = _('Tax rule {val}')
object_link_viewname = 'control:event.settings.tax.edit'
object_link_argname = 'rule'
class NoOpShredderMixin:
def shred_pii(self, logentry):
pass
class ClearDataShredderMixin:
def shred_pii(self, logentry):
logentry.data = None

View File

@@ -81,7 +81,7 @@ class Command(BaseCommand):
try:
r = receiver(signal=periodic_task, sender=self)
except Exception as err:
if isinstance(Exception, KeyboardInterrupt):
if isinstance(err, KeyboardInterrupt):
raise err
if settings.SENTRY_ENABLED:
from sentry_sdk import capture_exception

View File

@@ -31,6 +31,7 @@ from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from pretix.base.logentrytypes import log_entry_types
from pretix.helpers.json import CustomJSONEncoder
@@ -124,7 +125,13 @@ class LoggingMixin:
if (sensitivekey in k) and v:
data[k] = "********"
type, meta = log_entry_types.get(action_type=action)
if not type:
raise TypeError("Undefined log entry type '%s'" % action)
logentry.data = json.dumps(data, cls=CustomJSONEncoder, sort_keys=True)
type.validate_data(json.loads(logentry.data))
elif data:
raise TypeError("You should only supply dictionaries as log data.")
if save:

View File

@@ -823,6 +823,9 @@ class Event(EventMixin, LoggedModel):
self.save()
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
if hasattr(other, 'alternative_domain_assignment'):
other.alternative_domain_assignment.domain.event_assignments.create(event=self)
if not self.all_sales_channels:
self.limit_sales_channels.set(
self.organizer.sales_channels.filter(

View File

@@ -33,16 +33,15 @@
# License for the specific language governing permissions and limitations under the License.
import json
import logging
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.html import escape
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.signals import logentry_object_link
from pretix.base.logentrytypes import log_entry_types, make_link
from pretix.base.signals import is_app_active, logentry_object_link
class VisibleOnlyManager(models.Manager):
@@ -92,6 +91,10 @@ class LogEntry(models.Model):
indexes = [models.Index(fields=["datetime", "id"])]
def display(self):
log_entry_type, meta = log_entry_types.get(action_type=self.action_type)
if log_entry_type:
return log_entry_type.display(self)
from ..signals import logentry_display
for receiver, response in logentry_display.send(self.event, logentry=self):
@@ -126,10 +129,18 @@ class LogEntry(models.Model):
@cached_property
def display_object(self):
from . import (
Discount, Event, Item, ItemCategory, Order, Question, Quota,
SubEvent, TaxRule, Voucher,
Discount, Event, Item, Order, Question, Quota, SubEvent, Voucher,
)
log_entry_type, meta = log_entry_types.get(action_type=self.action_type)
if log_entry_type:
link_info = log_entry_type.get_object_link_info(self)
if is_app_active(self.event, meta['plugin']):
return make_link(link_info, log_entry_type.object_link_wrapper)
else:
return make_link(link_info, log_entry_type.object_link_wrapper, is_active=False,
event=self.event, plugin_name=meta['plugin'] and getattr(meta['plugin'], 'name'))
try:
if self.content_type.model_class() is Event:
return ''
@@ -137,110 +148,15 @@ class LogEntry(models.Model):
co = self.content_object
except:
return ''
a_map = None
a_text = None
if isinstance(co, Order):
a_text = _('Order {val}')
a_map = {
'href': reverse('control:event.order', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'code': co.code
}),
'val': escape(co.code),
}
elif isinstance(co, Voucher):
a_text = _('Voucher {val}')
a_map = {
'href': reverse('control:event.voucher', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'voucher': co.id
}),
'val': escape(co.code[:6]),
}
elif isinstance(co, Item):
a_text = _('Product {val}')
a_map = {
'href': reverse('control:event.item', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'item': co.id
}),
'val': escape(co.name),
}
elif isinstance(co, SubEvent):
a_text = pgettext_lazy('subevent', 'Date {val}')
a_map = {
'href': reverse('control:event.subevent', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'subevent': co.id
}),
'val': escape(str(co))
}
elif isinstance(co, Quota):
a_text = _('Quota {val}')
a_map = {
'href': reverse('control:event.items.quotas.show', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'quota': co.id
}),
'val': escape(co.name),
}
elif isinstance(co, Discount):
a_text = _('Discount {val}')
a_map = {
'href': reverse('control:event.items.discounts.edit', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'discount': co.id
}),
'val': escape(co.internal_name),
}
elif isinstance(co, ItemCategory):
a_text = _('Category {val}')
a_map = {
'href': reverse('control:event.items.categories.edit', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'category': co.id
}),
'val': escape(co.name),
}
elif isinstance(co, Question):
a_text = _('Question {val}')
a_map = {
'href': reverse('control:event.items.questions.show', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'question': co.id
}),
'val': escape(co.question),
}
elif isinstance(co, TaxRule):
a_text = _('Tax rule {val}')
a_map = {
'href': reverse('control:event.settings.tax.edit', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'rule': co.id
}),
'val': escape(co.name),
}
for receiver, response in logentry_object_link.send(self.event, logentry=self):
if response:
return response
if a_text and a_map:
a_map['val'] = '<a href="{href}">{val}</a>'.format_map(a_map)
return a_text.format_map(a_map)
elif a_text:
return a_text
else:
for receiver, response in logentry_object_link.send(self.event, logentry=self):
if response:
return response
return ''
if isinstance(co, (Order, Voucher, Item, SubEvent, Quota, Discount, Question)):
logging.warning("LogEntryType missing or ill-defined: %s", self.action_type)
return ''
@cached_property
def parsed_data(self):

View File

@@ -159,10 +159,24 @@ class Membership(models.Model):
de = date_format(self.date_end, 'SHORT_DATE_FORMAT')
return f'{self.membership_type.name}: {self.attendee_name} ({ds} {de})'
@property
def percentage_used(self):
if self.membership_type.max_usages and self.usages:
return int(self.usages / self.membership_type.max_usages * 100)
return 0
@property
def attendee_name(self):
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
@property
def expired(self):
return time_machine_now() > self.date_end
@property
def not_yet_valid(self):
return time_machine_now() < self.date_start
def is_valid(self, ev=None, ticket_valid_from=None, valid_from_not_chosen=False):
if valid_from_not_chosen:
return not self.canceled and self.date_end >= time_machine_now()

View File

@@ -2275,6 +2275,7 @@ class OrderFee(models.Model):
FEE_TYPE_SERVICE = "service"
FEE_TYPE_CANCELLATION = "cancellation"
FEE_TYPE_INSURANCE = "insurance"
FEE_TYPE_LATE = "late"
FEE_TYPE_OTHER = "other"
FEE_TYPE_GIFTCARD = "giftcard"
FEE_TYPES = (
@@ -2283,6 +2284,7 @@ class OrderFee(models.Model):
(FEE_TYPE_SERVICE, _("Service fee")),
(FEE_TYPE_CANCELLATION, _("Cancellation fee")),
(FEE_TYPE_INSURANCE, _("Insurance fee")),
(FEE_TYPE_LATE, _("Late fee")),
(FEE_TYPE_OTHER, _("Other fees")),
(FEE_TYPE_GIFTCARD, _("Gift card")),
)
@@ -3204,9 +3206,9 @@ class InvoiceAddress(models.Model):
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'))
name_cached = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
name_parts = models.JSONField(default=dict)
street = models.TextField(verbose_name=_('Address'), blank=False)
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=False)
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
street = models.TextField(verbose_name=_('Address'), blank=True)
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=True)
city = models.CharField(max_length=255, verbose_name=_('City'), blank=True)
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
country = FastCountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'),
countries=CachedCountries)

View File

@@ -1419,50 +1419,51 @@ class GiftCardPayment(BasePaymentProvider):
def payment_refund_supported(self, payment: OrderPayment) -> bool:
return True
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str, None]:
from pretix.base.services.cart import add_payment_to_cart
def _add_giftcard_to_cart(self, cs, gc):
from pretix.base.services.cart import add_payment_to_cart_session
if gc.currency != self.event.currency:
raise ValidationError(_("This gift card does not support this currency."))
if gc.testmode and not self.event.testmode:
raise ValidationError(_("This gift card can only be used in test mode."))
if not gc.testmode and self.event.testmode:
raise ValidationError(_("Only test gift cards can be used in test mode."))
if gc.expires and gc.expires < time_machine_now():
raise ValidationError(_("This gift card is no longer valid."))
if gc.value <= Decimal("0.00"):
raise ValidationError(_("All credit on this gift card has been used."))
for p in cs.get('payments', []):
if p['provider'] == self.identifier and p['info_data']['gift_card'] == gc.pk:
raise ValidationError(_("This gift card is already used for your payment."))
add_payment_to_cart_session(
cs,
self,
max_value=gc.value,
info_data={
'gift_card': gc.pk,
'gift_card_secret': gc.secret,
}
)
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str, None]:
for p in get_cart(request):
if p.item.issue_giftcard:
messages.error(request, _("You cannot pay with gift cards when buying a gift card."))
return
cs = cart_session(request)
try:
gc = self.event.organizer.accepted_gift_cards.get(
secret=request.POST.get("giftcard").strip()
)
if gc.currency != self.event.currency:
messages.error(request, _("This gift card does not support this currency."))
cs = cart_session(request)
try:
self._add_giftcard_to_cart(cs, gc)
return True
except ValidationError as e:
messages.error(request, str(e.message))
return
if gc.testmode and not self.event.testmode:
messages.error(request, _("This gift card can only be used in test mode."))
return
if not gc.testmode and self.event.testmode:
messages.error(request, _("Only test gift cards can be used in test mode."))
return
if gc.expires and gc.expires < time_machine_now():
messages.error(request, _("This gift card is no longer valid."))
return
if gc.value <= Decimal("0.00"):
messages.error(request, _("All credit on this gift card has been used."))
return
for p in cs.get('payments', []):
if p['provider'] == self.identifier and p['info_data']['gift_card'] == gc.pk:
messages.error(request, _("This gift card is already used for your payment."))
return
add_payment_to_cart(
request,
self,
max_value=gc.value,
info_data={
'gift_card': gc.pk,
'gift_card_secret': gc.secret,
}
)
return True
except GiftCard.DoesNotExist:
if self.event.vouchers.filter(code__iexact=request.POST.get("giftcard")).exists():
messages.warning(request, _("You entered a voucher instead of a gift card. Vouchers can only be entered on the first page of the shop below "

View File

@@ -1426,6 +1426,28 @@ class CartManager:
raise CartError(err)
def add_payment_to_cart_session(cart_session, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
"""
:param cart_session: The current cart session.
:param provider: The instance of your payment provider.
:param min_value: The minimum value this payment instrument supports, or ``None`` for unlimited.
:param max_value: The maximum value this payment instrument supports, or ``None`` for unlimited. Highly discouraged
to use for payment providers which charge a payment fee, as this can be very user-unfriendly if
users need a second payment method just for the payment fee of the first method.
:param info_data: A dictionary of information that will be passed through to the ``OrderPayment.info_data`` attribute.
:return:
"""
cart_session.setdefault('payments', [])
cart_session['payments'].append({
'id': str(uuid.uuid4()),
'provider': provider.identifier,
'multi_use_supported': provider.multi_use_supported,
'min_value': str(min_value) if min_value is not None else None,
'max_value': str(max_value) if max_value is not None else None,
'info_data': info_data or {},
})
def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
"""
:param request: The current HTTP request context.
@@ -1440,16 +1462,7 @@ def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: D
from pretix.presale.views.cart import cart_session
cs = cart_session(request)
cs.setdefault('payments', [])
cs['payments'].append({
'id': str(uuid.uuid4()),
'provider': provider.identifier,
'multi_use_supported': provider.multi_use_supported,
'min_value': str(min_value) if min_value is not None else None,
'max_value': str(max_value) if max_value is not None else None,
'info_data': info_data or {},
})
add_payment_to_cart_session(cs, provider, min_value, max_value, info_data)
def get_fees(event, request, total, invoice_address, payments, positions):

View File

@@ -76,7 +76,7 @@ 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.format import format_map
from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.hierarkey import clean_filename
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.ical import get_private_icals
@@ -311,7 +311,13 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
try:
if plain_text_only:
body_html = None
elif 'context' in inspect.signature(renderer.render).parameters:
body_html = renderer.render(content_plain, signature, raw_subject, order, position, context)
elif 'position' in inspect.signature(renderer.render).parameters:
# Backwards compatibility
warnings.warn('Email renderer called without context argument because context argument is not '
'supported.',
DeprecationWarning)
body_html = renderer.render(content_plain, signature, raw_subject, order, position)
else:
# Backwards compatibility
@@ -323,6 +329,8 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
logger.exception('Could not render HTML body')
body_html = None
body_plain = format_map(body_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
send_task = mail_send_task.si(
to=[email] if isinstance(email, str) else list(email),
cc=cc,
@@ -655,7 +663,7 @@ def render_mail(template, context):
if isinstance(template, LazyI18nString):
body = str(template)
if context:
body = format_map(body, context)
body = format_map(body, context, mode=SafeFormatter.MODE_IGNORE_RICH)
else:
tpl = get_template(template)
body = tpl.render(context)

View File

@@ -26,6 +26,7 @@ from decimal import Decimal
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@@ -39,7 +40,8 @@ from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
from pretix.base.signals import (
register_mail_placeholders, register_text_placeholders,
)
from pretix.helpers.format import SafeFormatter
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.helpers.format import PlainHtmlAlternativeString, SafeFormatter
logger = logging.getLogger('pretix.base.services.placeholders')
@@ -107,6 +109,91 @@ class SimpleFunctionalTextPlaceholder(BaseTextPlaceholder):
return self._sample
class BaseRichTextPlaceholder(BaseTextPlaceholder):
"""
This is the base class for all placeholders which can render either to plain text
or to a rich HTML element.
"""
def __init__(self, identifier, args):
self._identifier = identifier
self._args = args
@property
def identifier(self):
return self._identifier
@property
def required_context(self):
return self._args
@property
def is_block(self):
return False
def render(self, context):
return PlainHtmlAlternativeString(
self.render_plain(**{k: context[k] for k in self._args}),
self.render_html(**{k: context[k] for k in self._args}),
self.is_block,
)
def render_html(self, **kwargs):
"""
HTML rendering of the placeholder. Should return "safe" HTML, i.e. everything needs to be
escaped.
"""
raise NotImplementedError
def render_plain(self, **kwargs):
"""
Plain text rendering of the placeholder.
"""
raise NotImplementedError
def render_sample(self, event):
return PlainHtmlAlternativeString(
self.render_sample_plain(event=event),
self.render_sample_html(event=event),
self.is_block,
)
def render_sample_html(self, event):
raise NotImplementedError
def render_sample_plain(self, event):
raise NotImplementedError
class SimpleButtonPlaceholder(BaseRichTextPlaceholder):
def __init__(self, identifier, args, url_func, text_func, sample_url_func, sample_text_func):
super().__init__(identifier, args)
self._url_func = url_func
self._text_func = text_func
self._sample_url_func = sample_url_func
self._sample_text_func = sample_text_func
def render_html(self, **context):
text = self._text_func(**{k: context[k] for k in self._args})
url = self._url_func(**{k: context[k] for k in self._args})
return f'<a href="{url}" class="button">{escape(text)}</a>'
def render_plain(self, **context):
text = self._text_func(**{k: context[k] for k in self._args})
url = self._url_func(**{k: context[k] for k in self._args})
return f'{text}: {url}'
def render_sample_html(self, event):
text = self._sample_text_func(event)
url = self._sample_url_func(event)
return f'<a href="{url}" class="button">{escape(text)}</a>'
def render_sample_plain(self, event):
text = self._sample_text_func(event)
url = self._sample_url_func(event)
return f'{text}: {url}'
class PlaceholderContext(SafeFormatter):
"""
Holds the contextual arguments and corresponding list of available placeholders for formatting
@@ -284,6 +371,27 @@ def base_placeholders(sender, **kwargs):
}
),
),
SimpleButtonPlaceholder(
'url_button', ['order', 'event'],
url_func=lambda order, event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_secret()
}
),
text_func=lambda order, event: _("View order details"),
sample_url_func=lambda event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'hash': '98kusd8ofsj8dnkd'
}
),
sample_text_func=lambda event: _("View order details"),
),
SimpleFunctionalTextPlaceholder(
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
@@ -348,6 +456,27 @@ def base_placeholders(sender, **kwargs):
}
),
),
SimpleButtonPlaceholder(
'url_button', ['event', 'position'],
url_func=lambda event, position: build_absolute_uri(
event,
'presale:event.order.position', kwargs={
'order': position.order.code,
'secret': position.web_secret,
'position': position.positionid
}
),
text_func=lambda event, position: _("View registration details"),
sample_url_func=lambda event: build_absolute_uri(
event,
'presale:event.order.position', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'position': '123'
}
),
sample_text_func=lambda event: _("View registration details"),
),
SimpleFunctionalTextPlaceholder(
'url_info_change', ['position', 'event'], lambda position, event: build_absolute_uri(
event,
@@ -603,8 +732,8 @@ def base_placeholders(sender, **kwargs):
class FormPlaceholderMixin:
def _set_field_placeholders(self, fn, base_parameters):
placeholders = get_available_placeholders(self.event, base_parameters)
def _set_field_placeholders(self, fn, base_parameters, rich=False):
placeholders = get_available_placeholders(self.event, base_parameters, rich=rich)
ht = format_placeholders_help_text(placeholders, self.event)
if self.fields[fn].help_text:
self.fields[fn].help_text += ' ' + str(ht)
@@ -615,7 +744,7 @@ class FormPlaceholderMixin:
)
def get_available_placeholders(event, base_parameters):
def get_available_placeholders(event, base_parameters, rich=False):
if 'order' in base_parameters:
base_parameters.append('invoice_address')
base_parameters.append('position_or_address')
@@ -624,6 +753,35 @@ def get_available_placeholders(event, base_parameters):
if not isinstance(val, (list, tuple)):
val = [val]
for v in val:
if isinstance(v, BaseRichTextPlaceholder) and not rich:
continue
if all(rp in base_parameters for rp in v.required_context):
params[v.identifier] = v
return params
def get_sample_context(event, context_parameters, rich=True):
context_dict = {}
lbl = _('This value will be replaced based on dynamic parameters.')
for k, v in get_available_placeholders(event, context_parameters, rich=rich).items():
sample = v.render_sample(event)
if isinstance(sample, PlainHtmlAlternativeString):
context_dict[k] = PlainHtmlAlternativeString(
sample.plain,
'<{el} class="placeholder placeholder-html" title="{title}">{html}</{el}>'.format(
el='div' if sample.is_block else 'span',
title=lbl,
html=sample.html,
)
)
elif str(sample).strip().startswith('* ') or str(sample).startswith(' '):
context_dict[k] = '<div class="placeholder" title="{}">{}</div>'.format(
lbl,
markdown_compile_email(str(sample))
)
else:
context_dict[k] = '<span class="placeholder" title="{}">{}</span>'.format(
lbl,
escape(sample)
)
return context_dict

View File

@@ -56,6 +56,7 @@ from django.utils.translation import (
from django_countries.fields import Country
from hierarkey.models import GlobalSettingsBase, Hierarkey
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from i18nfield.rest_framework import I18nField
from i18nfield.strings import LazyI18nString
from phonenumbers import PhoneNumber, parse
from rest_framework import serializers
@@ -63,7 +64,7 @@ from rest_framework import serializers
from pretix.api.serializers.fields import (
ListMultipleChoiceField, UploadedFileField,
)
from pretix.api.serializers.i18n import I18nField, I18nURLField
from pretix.api.serializers.i18n import I18nURLField
from pretix.base.forms import I18nMarkdownTextarea, I18nURLFormField
from pretix.base.models.tax import VAT_ID_COUNTRIES, TaxRule
from pretix.base.reldate import (

View File

@@ -33,14 +33,15 @@
# License for the specific language governing permissions and limitations under the License.
import warnings
from typing import Any, Callable, List, Tuple
from typing import TYPE_CHECKING, Any, Callable, List, Tuple
import django.dispatch
from django.apps import apps
from django.conf import settings
from django.dispatch.dispatcher import NO_RECEIVERS
from .models import Event
if TYPE_CHECKING:
from .models import Event
app_cache = {}
@@ -52,6 +53,50 @@ def _populate_app_cache():
app_cache[ac.name] = ac
def get_defining_app(o):
# If sentry packed this in a wrapper, unpack that
if "sentry" in o.__module__:
o = o.__wrapped__
# Find the Django application this belongs to
searchpath = o.__module__
# Core modules are always active
if any(searchpath.startswith(cm) for cm in settings.CORE_MODULES):
return 'CORE'
if not app_cache:
_populate_app_cache()
while True:
app = app_cache.get(searchpath)
if "." not in searchpath or app:
break
searchpath, _ = searchpath.rsplit(".", 1)
return app
def is_app_active(sender, app):
if app == 'CORE':
return True
excluded = settings.PRETIX_PLUGINS_EXCLUDE
if sender and app and app.name in sender.get_plugins() and app.name not in excluded:
if not hasattr(app, 'compatibility_errors') or not app.compatibility_errors:
return True
return False
def is_receiver_active(sender, receiver):
if sender is None:
# Send to all events!
return True
app = get_defining_app(receiver)
return is_app_active(sender, app)
class EventPluginSignal(django.dispatch.Signal):
"""
This is an extension to Django's built-in signals which differs in a way that it sends
@@ -59,40 +104,14 @@ class EventPluginSignal(django.dispatch.Signal):
Event.
"""
def _is_active(self, sender, receiver):
if sender is None:
# Send to all events!
return True
# If sentry packed this in a wrapper, unpack that
if "sentry" in receiver.__module__:
receiver = receiver.__wrapped__
# Find the Django application this belongs to
searchpath = receiver.__module__
core_module = any([searchpath.startswith(cm) for cm in settings.CORE_MODULES])
app = None
if not core_module:
while True:
app = app_cache.get(searchpath)
if "." not in searchpath or app:
break
searchpath, _ = searchpath.rsplit(".", 1)
# Only fire receivers from active plugins and core modules
excluded = settings.PRETIX_PLUGINS_EXCLUDE
if core_module or (sender and app and app.name in sender.get_plugins() and app.name not in excluded):
if not hasattr(app, 'compatibility_errors') or not app.compatibility_errors:
return True
return False
def send(self, sender: Event, **named) -> List[Tuple[Callable, Any]]:
def send(self, sender: "Event", **named) -> List[Tuple[Callable, Any]]:
"""
Send signal from sender to all connected receivers that belong to
plugins enabled for the given Event.
sender is required to be an instance of ``pretix.base.models.Event``.
"""
from .models import Event
if sender and not isinstance(sender, Event):
raise ValueError("Sender needs to be an event.")
@@ -104,12 +123,12 @@ class EventPluginSignal(django.dispatch.Signal):
_populate_app_cache()
for receiver in self._sorted_receivers(sender):
if self._is_active(sender, receiver):
if is_receiver_active(sender, receiver):
response = receiver(signal=self, sender=sender, **named)
responses.append((receiver, response))
return responses
def send_chained(self, sender: Event, chain_kwarg_name, **named) -> List[Tuple[Callable, Any]]:
def send_chained(self, sender: "Event", chain_kwarg_name, **named) -> List[Tuple[Callable, Any]]:
"""
Send signal from sender to all connected receivers. The return value of the first receiver
will be used as the keyword argument specified by ``chain_kwarg_name`` in the input to the
@@ -117,6 +136,7 @@ class EventPluginSignal(django.dispatch.Signal):
sender is required to be an instance of ``pretix.base.models.Event``.
"""
from .models import Event
if sender and not isinstance(sender, Event):
raise ValueError("Sender needs to be an event.")
@@ -128,12 +148,12 @@ class EventPluginSignal(django.dispatch.Signal):
_populate_app_cache()
for receiver in self._sorted_receivers(sender):
if self._is_active(sender, receiver):
if is_receiver_active(sender, receiver):
named[chain_kwarg_name] = response
response = receiver(signal=self, sender=sender, **named)
return response
def send_robust(self, sender: Event, **named) -> List[Tuple[Callable, Any]]:
def send_robust(self, sender: "Event", **named) -> List[Tuple[Callable, Any]]:
"""
Send signal from sender to all connected receivers. If a receiver raises an exception
instead of returning a value, the exception is included as the result instead of
@@ -141,6 +161,7 @@ class EventPluginSignal(django.dispatch.Signal):
sender is required to be an instance of ``pretix.base.models.Event``.
"""
from .models import Event
if sender and not isinstance(sender, Event):
raise ValueError("Sender needs to be an event.")
@@ -155,7 +176,7 @@ class EventPluginSignal(django.dispatch.Signal):
_populate_app_cache()
for receiver in self._sorted_receivers(sender):
if self._is_active(sender, receiver):
if is_receiver_active(sender, receiver):
try:
response = receiver(signal=self, sender=sender, **named)
except Exception as err:
@@ -178,7 +199,7 @@ class EventPluginSignal(django.dispatch.Signal):
class GlobalSignal(django.dispatch.Signal):
def send_chained(self, sender: Event, chain_kwarg_name, **named) -> List[Tuple[Callable, Any]]:
def send_chained(self, sender: "Event", chain_kwarg_name, **named) -> List[Tuple[Callable, Any]]:
"""
Send signal from sender to all connected receivers. The return value of the first receiver
will be used as the keyword argument specified by ``chain_kwarg_name`` in the input to the
@@ -202,6 +223,115 @@ class DeprecatedSignal(django.dispatch.Signal):
super().connect(receiver, sender=None, weak=True, dispatch_uid=None)
class Registry:
"""
A Registry is a collection of objects (entries), annotated with metadata. Entries can be searched and filtered by
metadata keys, and metadata is returned as part of the result.
Entry metadata is generated during registration using to the accessor functions given to the Registry
constructor.
Example:
.. code-block:: python
animal_sound_registry = Registry({"animal": lambda s: s.animal})
@animal_sound_registry.new("dog", "woof")
@animal_sound_registry.new("cricket", "chirp")
class AnimalSound:
def __init__(self, animal, sound):
self.animal = animal
self.sound = sound
def make_sound(self):
return self.sound
@animal_sound_registry.new()
class CatSound(AnimalSound):
def __init__(self):
super().__init__(animal="cat", sound=["meow", "meww", "miaou"])
def make_sound(self):
return random.choice(self.sound)
"""
def __init__(self, keys):
"""
:param keys: dictionary {key: accessor_function}
When a new entry is registered, all accessor functions are called with the new entry as parameter.
Their return value is stored as the metadata value for that key.
"""
self.registered_entries = list()
self.keys = keys
self.by_key = {key: {} for key in self.keys.keys()}
def register(self, *objs):
"""
Register one or more entries in this registry.
Usable as a regular method or as decorator on a class or function. If used on a class, the class type object
itself is registered, not an instance of the class. To register an instance, use the ``new`` method.
.. code-block:: python
@some_registry.register
def my_new_entry(foo):
# ...
"""
for obj in objs:
meta = {k: accessor(obj) for k, accessor in self.keys.items()}
tup = (obj, meta)
for key, value in meta.items():
self.by_key[key][value] = tup
self.registered_entries.append(tup)
def new(self, *args, **kwargs):
"""
Instantiate the decorated class with the given *args and **kwargs, and register the instance in this registry.
May be used multiple times.
.. code-block:: python
@animal_sound_registry.new("meow")
@animal_sound_registry.new("woof")
class AnimalSound:
def __init__(self, sound):
# ...
"""
def reg(clz):
obj = clz(*args, **kwargs)
self.register(obj)
return clz
return reg
def get(self, **kwargs):
(key, value), = kwargs.items()
return self.by_key.get(key).get(value, (None, None))
def filter(self, **kwargs):
return ((entry, meta)
for entry, meta in self.registered_entries
if all(value == meta[key] for key, value in kwargs.items())
)
class EventPluginRegistry(Registry):
"""
A Registry which automatically annotates entries with a "plugin" key, specifying which plugin
the entry is defined in. This allows the consumer of entries to determine whether an entry is
enabled for a given event, or filter only for entries defined by enabled plugins.
.. code-block:: python
logtype, meta = my_registry.find(action_type="foo.bar.baz")
# meta["plugin"] contains the django app name of the defining plugin
"""
def __init__(self, keys):
super().__init__({"plugin": lambda o: get_defining_app(o), **keys})
event_live_issues = EventPluginSignal()
"""
This signal is sent out to determine whether an event can be taken live. If you want to
@@ -507,41 +637,16 @@ logentry_display = EventPluginSignal()
"""
Arguments: ``logentry``
To display an instance of the ``LogEntry`` model to a human user,
``pretix.base.signals.logentry_display`` will be sent out with a ``logentry`` argument.
The first received response that is not ``None`` will be used to display the log entry
to the user. The receivers are expected to return plain text.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
**DEPRECTATION:** Please do not use this signal for new LogEntry types. Use the log_entry_types
registry instead, as described in https://docs.pretix.eu/en/latest/development/implementation/logging.html
"""
logentry_object_link = EventPluginSignal()
"""
Arguments: ``logentry``
To display the relationship of an instance of the ``LogEntry`` model to another model
to a human user, ``pretix.base.signals.logentry_object_link`` will be sent out with a
``logentry`` argument.
The first received response that is not ``None`` will be used to display the related object
to the user. The receivers are expected to return a HTML link. The internal implementation
builds the links like this::
a_text = _('Tax rule {val}')
a_map = {
'href': reverse('control:event.settings.tax.edit', kwargs={
'event': sender.slug,
'organizer': sender.organizer.slug,
'rule': logentry.content_object.id
}),
'val': escape(logentry.content_object.name),
}
a_map['val'] = '<a href="{href}">{val}</a>'.format_map(a_map)
return a_text.format_map(a_map)
Make sure that any user content in the HTML code you return is properly escaped!
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
**DEPRECTATION:** Please do not use this signal for new LogEntry types. Use the log_entry_types
registry instead, as described in https://docs.pretix.eu/en/latest/development/implementation/logging.html
"""
requiredaction_display = EventPluginSignal()

View File

@@ -131,6 +131,9 @@
text-align: left;
padding: 0;
}
.content table td.align-right {
text-align: right;
}
a.button {
display: inline-block;
@@ -178,6 +181,9 @@
pre, pre code {
white-space: pre-line;
}
.text-right, .content table td.text-right {
text-align: right;
}
{% if rtl %}
body {
@@ -186,6 +192,9 @@
.content {
text-align: right;
}
.text-right, .content table td.text-right {
text-align: left;
}
{% endif %}
{% block addcss %}{% endblock %}

View File

@@ -0,0 +1,34 @@
#
# 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
from django.utils.html import format_html
register = template.Library()
@register.simple_tag
def icon(key, *args, **kwargs):
return format_html(
'<span class="fa fa-{} {}" aria-hidden="true"></span>',
key,
kwargs["class"] if "class" in kwargs else "",
)

View File

@@ -52,12 +52,12 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
# would make the numbers incorrect. If this branch executes, it's likely a bug in
# pretix, but we won't show wrong numbers!
if hide_currency:
return floatformat(value, 2)
return floatformat(value, "2g")
else:
return '{} {}'.format(arg, floatformat(value, 2))
return '{} {}'.format(arg, floatformat(value, "2g"))
if hide_currency:
return floatformat(value, places)
return floatformat(value, f"{places}g")
locale_parts = translation.get_language().split("-", 1)
locale = locale_parts[0]
@@ -70,7 +70,7 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
try:
return format_currency(value, arg, locale=locale)
except:
return '{} {}'.format(arg, floatformat(value, places))
return '{} {}'.format(arg, floatformat(value, f"{places}g"))
@register.filter("money_numberfield")

View File

@@ -305,6 +305,7 @@ def markdown_compile_email(source, allowed_tags=ALLOWED_TAGS, allowed_attributes
source,
extensions=[
'markdown.extensions.sane_lists',
'markdown.extensions.tables',
EmailNl2BrExtension(),
LinkifyAndCleanExtension(
linker,

View File

@@ -0,0 +1,42 @@
#
# 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
from django.utils.html import format_html, mark_safe
register = template.Library()
@register.simple_tag
def textbubble(type, *args, **kwargs):
return format_html(
'<span class="textbubble-{}">{}',
type or "info",
"" if "icon" not in kwargs else format_html(
'<i class="fa fa-{}" aria-hidden="true"></i> ',
kwargs["icon"]
)
)
@register.simple_tag
def endtextbubble():
return mark_safe('</span>')

View File

@@ -22,16 +22,30 @@
import pycountry
from django.http import JsonResponse
from pretix.base.addressvalidation import (
COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED,
)
from pretix.base.models.tax import VAT_ID_COUNTRIES
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
def states(request):
cc = request.GET.get("country", "DE")
info = {
'street': {'required': True},
'zipcode': {'required': cc in COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED},
'city': {'required': cc in COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED},
'state': {'visible': cc in COUNTRIES_WITH_STATE_IN_ADDRESS, 'required': cc in COUNTRIES_WITH_STATE_IN_ADDRESS},
'vat_id': {'visible': cc in VAT_ID_COUNTRIES, 'required': False},
}
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
return JsonResponse({'data': []})
return JsonResponse({'data': [], **info, })
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
return JsonResponse({'data': [
{'name': s.name, 'code': s.code[3:]}
for s in sorted(statelist, key=lambda s: s.name)
]})
return JsonResponse({
'data': [
{'name': s.name, 'code': s.code[3:]}
for s in sorted(statelist, key=lambda s: s.name)
],
**info,
})

View File

@@ -35,7 +35,7 @@
# License for the specific language governing permissions and limitations under the License.
from decimal import Decimal
from urllib.parse import urlencode, urlparse
from urllib.parse import urlencode
from zoneinfo import ZoneInfo
import pycountry
@@ -76,8 +76,10 @@ from pretix.control.forms import (
)
from pretix.control.forms.widgets import Select2
from pretix.helpers.countries import CachedCountries
from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.multidomain.models import AlternativeDomainAssignment, KnownDomain
from pretix.multidomain.urlreverse import (
build_absolute_uri, get_organizer_domain,
)
from pretix.plugins.banktransfer.payment import BankTransfer
from pretix.presale.style import get_fonts
@@ -363,14 +365,9 @@ class EventUpdateForm(I18nModelForm):
def __init__(self, *args, **kwargs):
self.change_slug = kwargs.pop('change_slug', False)
self.domain = kwargs.pop('domain', False)
kwargs.setdefault('initial', {})
self.instance = kwargs['instance']
if self.domain and self.instance:
initial_domain = self.instance.domains.first()
if initial_domain:
kwargs['initial'].setdefault('domain', initial_domain.domainname)
super().__init__(*args, **kwargs)
if not self.change_slug:
@@ -379,48 +376,54 @@ class EventUpdateForm(I18nModelForm):
self.fields['location'].widget.attrs['placeholder'] = _(
'Sample Conference Center\nHeidelberg, Germany'
)
if self.domain:
try:
self.fields['domain'] = forms.CharField(
max_length=255,
label=_('Custom domain'),
label=_('Domain'),
initial=self.instance.domain.domainname,
required=False,
disabled=True,
help_text=_('You can configure this in your organizer settings.')
)
except KnownDomain.DoesNotExist:
domain = get_organizer_domain(self.instance.organizer)
try:
current_domain_assignment = self.instance.alternative_domain_assignment
except AlternativeDomainAssignment.DoesNotExist:
current_domain_assignment = None
self.fields['domain'] = forms.ChoiceField(
label=_('Domain'),
help_text=_('You can add more domains in your organizer account.'),
choices=[('', _('Same as organizer account') + (f" ({domain})" if domain else ""))] + [
(d.domainname, d.domainname) for d in self.instance.organizer.domains.filter(mode=KnownDomain.MODE_ORG_ALT_DOMAIN)
],
initial=current_domain_assignment.domain_id if current_domain_assignment else "",
required=False,
help_text=_('You need to configure the custom domain in the webserver beforehand.')
)
self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.all()
self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={
'data-inverse-dependency': '<[name$=all_sales_channels]',
}, choices=self.fields['limit_sales_channels'].widget.choices)
def clean_domain(self):
d = self.cleaned_data['domain']
if d:
if d == urlparse(settings.SITE_URL).hostname:
raise ValidationError(
_('You cannot choose the base domain of this installation.')
)
if KnownDomain.objects.filter(domainname=d).exclude(event=self.instance.pk).exists():
raise ValidationError(
_('This domain is already in use for a different event or organizer.')
)
return d
def save(self, commit=True):
instance = super().save(commit)
if self.domain:
current_domain = instance.domains.first()
if self.cleaned_data['domain']:
if current_domain and current_domain.domainname != self.cleaned_data['domain']:
current_domain.delete()
KnownDomain.objects.create(
organizer=instance.organizer, event=instance, domainname=self.cleaned_data['domain']
)
elif not current_domain:
KnownDomain.objects.create(
organizer=instance.organizer, event=instance, domainname=self.cleaned_data['domain']
)
elif current_domain:
current_domain.delete()
try:
current_domain_assignment = instance.alternative_domain_assignment
except AlternativeDomainAssignment.DoesNotExist:
current_domain_assignment = None
if self.cleaned_data['domain'] and not hasattr(instance, 'domain'):
domain = self.instance.organizer.domains.get(mode=KnownDomain.MODE_ORG_ALT_DOMAIN, domainname=self.cleaned_data["domain"])
AlternativeDomainAssignment.objects.update_or_create(
event=instance,
defaults={
"domain": domain,
}
)
instance.cache.clear()
elif current_domain_assignment:
current_domain_assignment.delete()
instance.cache.clear()
return instance
@@ -1382,7 +1385,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
self.event.meta_values_cached = self.event.meta_values.select_related('property').all()
for k, v in self.base_context.items():
self._set_field_placeholders(k, v)
self._set_field_placeholders(k, v, rich=k.startswith('mail_text_'))
for k, v in list(self.fields.items()):
if k.endswith('_attendee') and not event.settings.attendee_emails_asked:

View File

@@ -133,63 +133,108 @@ class OrganizerDeleteForm(forms.Form):
class OrganizerUpdateForm(OrganizerForm):
def __init__(self, *args, **kwargs):
self.domain = kwargs.pop('domain', False)
self.change_slug = kwargs.pop('change_slug', False)
kwargs.setdefault('initial', {})
self.instance = kwargs['instance']
if self.domain and self.instance:
initial_domain = self.instance.domains.filter(event__isnull=True).first()
if initial_domain:
kwargs['initial'].setdefault('domain', initial_domain.domainname)
super().__init__(*args, **kwargs)
if not self.change_slug:
self.fields['slug'].widget.attrs['readonly'] = 'readonly'
if self.domain:
self.fields['domain'] = forms.CharField(
max_length=255,
label=_('Custom domain'),
required=False,
help_text=_('You need to configure the custom domain in the webserver beforehand.')
)
def clean_domain(self):
d = self.cleaned_data['domain']
if d:
if d == urlparse(settings.SITE_URL).hostname:
raise ValidationError(
_('You cannot choose the base domain of this installation.')
)
if KnownDomain.objects.filter(domainname=d).exclude(organizer=self.instance.pk,
event__isnull=True).exists():
raise ValidationError(
_('This domain is already in use for a different event or organizer.')
)
return d
def clean_slug(self):
if self.change_slug:
return self.cleaned_data['slug']
return self.instance.slug
def save(self, commit=True):
instance = super().save(commit)
if self.domain:
current_domain = instance.domains.filter(event__isnull=True).first()
if self.cleaned_data['domain']:
if current_domain and current_domain.domainname != self.cleaned_data['domain']:
current_domain.delete()
KnownDomain.objects.create(organizer=instance, domainname=self.cleaned_data['domain'])
elif not current_domain:
KnownDomain.objects.create(organizer=instance, domainname=self.cleaned_data['domain'])
elif current_domain:
current_domain.delete()
instance.cache.clear()
for ev in instance.events.all():
ev.cache.clear()
class KnownDomainForm(forms.ModelForm):
class Meta:
model = KnownDomain
fields = ["domainname", "mode", "event"]
field_classes = {
"event": SafeModelChoiceField,
}
return instance
def __init__(self, *args, **kwargs):
self.organizer = kwargs.pop('organizer')
super().__init__(*args, **kwargs)
self.fields["event"].queryset = self.organizer.events.all()
if self.instance and self.instance.pk:
self.fields["domainname"].widget.attrs['readonly'] = 'readonly'
def clean_domainname(self):
if self.instance and self.instance.pk:
return self.instance.domainname
d = self.cleaned_data['domainname']
if d:
if d == urlparse(settings.SITE_URL).hostname:
raise ValidationError(
_('You cannot choose the base domain of this installation.')
)
if KnownDomain.objects.filter(domainname=d).exclude(organizer=self.instance.organizer).exists():
raise ValidationError(
_('This domain is already in use for a different event or organizer.')
)
return d
def clean(self):
d = super().clean()
if d["mode"] == KnownDomain.MODE_ORG_DOMAIN and d["event"]:
raise ValidationError(
_("Do not choose an event for this mode.")
)
if d["mode"] == KnownDomain.MODE_ORG_ALT_DOMAIN and d["event"]:
raise ValidationError(
_("Do not choose an event for this mode. You can assign events to this domain in event settings.")
)
if d["mode"] == KnownDomain.MODE_EVENT_DOMAIN and not d["event"]:
raise ValidationError(
_("You need to choose an event.")
)
return d
class BaseKnownDomainFormSet(forms.BaseInlineFormSet):
def __init__(self, *args, **kwargs):
self.organizer = kwargs.pop('organizer')
super().__init__(*args, **kwargs)
def _construct_form(self, i, **kwargs):
kwargs['organizer'] = self.organizer
return super()._construct_form(i, **kwargs)
@property
def empty_form(self):
form = self.form(
auto_id=self.auto_id,
prefix=self.add_prefix('__prefix__'),
empty_permitted=True,
use_required_attribute=False,
organizer=self.organizer,
)
self.add_fields(form, None)
return form
def clean(self):
super().clean()
data = [f.cleaned_data for f in self.forms]
if len([d for d in data if d.get("mode") == KnownDomain.MODE_ORG_DOMAIN and not d.get("DELETE")]) > 1:
raise ValidationError(_("You may set only one organizer domain."))
return data
KnownDomainFormset = inlineformset_factory(
Organizer, KnownDomain,
KnownDomainForm,
formset=BaseKnownDomainFormSet,
can_order=False, can_delete=True, extra=0
)
class SafeOrderPositionChoiceField(forms.ModelChoiceField):

View File

@@ -47,12 +47,20 @@ from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from i18nfield.strings import LazyI18nString
from pretix.base.logentrytypes import (
DiscountLogEntryType, EventLogEntryType, ItemCategoryLogEntryType,
ItemLogEntryType, LogEntryType, OrderLogEntryType, QuestionLogEntryType,
QuotaLogEntryType, TaxRuleLogEntryType, VoucherLogEntryType,
log_entry_types,
)
from pretix.base.models import (
Checkin, CheckinList, Event, ItemVariation, LogEntry, OrderPosition,
TaxRule,
)
from pretix.base.models.orders import PrintLog
from pretix.base.signals import logentry_display, orderposition_blocked_display
from pretix.base.signals import (
app_cache, logentry_display, orderposition_blocked_display,
)
from pretix.base.templatetags.money import money_filter
OVERVIEW_BANLIST = [
@@ -329,278 +337,6 @@ def _display_checkin(event, logentry):
@receiver(signal=logentry_display, dispatch_uid="pretixcontrol_logentry_display")
def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
plains = {
'pretix.object.cloned': _('This object has been created by cloning.'),
'pretix.organizer.changed': _('The organizer has been changed.'),
'pretix.organizer.settings': _('The organizer settings have been changed.'),
'pretix.organizer.footerlinks.changed': _('The footer links have been changed.'),
'pretix.organizer.export.schedule.added': _('A scheduled export has been added.'),
'pretix.organizer.export.schedule.changed': _('A scheduled export has been changed.'),
'pretix.organizer.export.schedule.deleted': _('A scheduled export has been deleted.'),
'pretix.organizer.export.schedule.executed': _('A scheduled export has been executed.'),
'pretix.organizer.export.schedule.failed': _('A scheduled export has failed: {reason}.'),
'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'),
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
'pretix.giftcards.acceptance.acceptor.invited': _('A new gift card acceptor has been invited.'),
'pretix.giftcards.acceptance.acceptor.removed': _('A gift card acceptor has been removed.'),
'pretix.giftcards.acceptance.issuer.removed': _('A gift card issuer has been removed or declined.'),
'pretix.giftcards.acceptance.issuer.accepted': _('A new gift card issuer has been accepted.'),
'pretix.webhook.created': _('The webhook has been created.'),
'pretix.webhook.changed': _('The webhook has been changed.'),
'pretix.webhook.retries.expedited': _('The webhook call retry jobs have been manually expedited.'),
'pretix.webhook.retries.dropped': _('The webhook call retry jobs have been dropped.'),
'pretix.ssoprovider.created': _('The SSO provider has been created.'),
'pretix.ssoprovider.changed': _('The SSO provider has been changed.'),
'pretix.ssoprovider.deleted': _('The SSO provider has been deleted.'),
'pretix.ssoclient.created': _('The SSO client has been created.'),
'pretix.ssoclient.changed': _('The SSO client has been changed.'),
'pretix.ssoclient.deleted': _('The SSO client has been deleted.'),
'pretix.membershiptype.created': _('The membership type has been created.'),
'pretix.membershiptype.changed': _('The membership type has been changed.'),
'pretix.membershiptype.deleted': _('The membership type has been deleted.'),
'pretix.saleschannel.created': _('The sales channel has been created.'),
'pretix.saleschannel.changed': _('The sales channel has been changed.'),
'pretix.saleschannel.deleted': _('The sales channel has been deleted.'),
'pretix.customer.created': _('The account has been created.'),
'pretix.customer.changed': _('The account has been changed.'),
'pretix.customer.membership.created': _('A membership for this account has been added.'),
'pretix.customer.membership.changed': _('A membership of this account has been changed.'),
'pretix.customer.membership.deleted': _('A membership of this account has been deleted.'),
'pretix.customer.anonymized': _('The account has been disabled and anonymized.'),
'pretix.customer.password.resetrequested': _('A new password has been requested.'),
'pretix.customer.password.set': _('A new password has been set.'),
'pretix.reusable_medium.created': _('The reusable medium has been created.'),
'pretix.reusable_medium.created.auto': _('The reusable medium has been created automatically.'),
'pretix.reusable_medium.changed': _('The reusable medium has been changed.'),
'pretix.reusable_medium.linked_orderposition.changed': _('The medium has been connected to a new ticket.'),
'pretix.reusable_medium.linked_giftcard.changed': _('The medium has been connected to a new gift card.'),
'pretix.email.error': _('Sending of an email has failed.'),
'pretix.event.comment': _('The event\'s internal comment has been updated.'),
'pretix.event.canceled': _('The event has been canceled.'),
'pretix.event.deleted': _('An event has been deleted.'),
'pretix.event.shredder.started': _('A removal process for personal data has been started.'),
'pretix.event.shredder.completed': _('A removal process for personal data has been completed.'),
'pretix.event.order.modified': _('The order details have been changed.'),
'pretix.event.order.unpaid': _('The order has been marked as unpaid.'),
'pretix.event.order.secret.changed': _('The order\'s secret has been changed.'),
'pretix.event.order.expirychanged': _('The order\'s expiry date has been changed.'),
'pretix.event.order.valid_if_pending.set': _('The order has been set to be usable before it is paid.'),
'pretix.event.order.valid_if_pending.unset': _('The order has been set to require payment before use.'),
'pretix.event.order.expired': _('The order has been marked as expired.'),
'pretix.event.order.paid': _('The order has been marked as paid.'),
'pretix.event.order.cancellationrequest.deleted': _('The cancellation request has been deleted.'),
'pretix.event.order.refunded': _('The order has been refunded.'),
'pretix.event.order.reactivated': _('The order has been reactivated.'),
'pretix.event.order.deleted': _('The test mode order {code} has been deleted.'),
'pretix.event.order.placed': _('The order has been created.'),
'pretix.event.order.placed.require_approval': _('The order requires approval before it can continue to be processed.'),
'pretix.event.order.approved': _('The order has been approved.'),
'pretix.event.order.denied': _('The order has been denied (comment: "{comment}").'),
'pretix.event.order.contact.changed': _('The email address has been changed from "{old_email}" '
'to "{new_email}".'),
'pretix.event.order.contact.confirmed': _('The email address has been confirmed to be working (the user clicked on a link '
'in the email for the first time).'),
'pretix.event.order.phone.changed': _('The phone number has been changed from "{old_phone}" '
'to "{new_phone}".'),
'pretix.event.order.customer.changed': _('The customer account has been changed.'),
'pretix.event.order.locale.changed': _('The order locale has been changed.'),
'pretix.event.order.invoice.generated': _('The invoice has been generated.'),
'pretix.event.order.invoice.regenerated': _('The invoice has been regenerated.'),
'pretix.event.order.invoice.reissued': _('The invoice has been reissued.'),
'pretix.event.order.comment': _('The order\'s internal comment has been updated.'),
'pretix.event.order.custom_followup_at': _('The order\'s follow-up date has been updated.'),
'pretix.event.order.checkin_attention': _('The order\'s flag to require attention at check-in has been '
'toggled.'),
'pretix.event.order.checkin_text': _('The order\'s check-in text has been changed.'),
'pretix.event.order.pretix.event.order.valid_if_pending': _('The order\'s flag to be considered valid even if '
'unpaid has been toggled.'),
'pretix.event.order.payment.changed': _('A new payment {local_id} has been started instead of the previous one.'),
'pretix.event.order.email.sent': _('An unidentified type email has been sent.'),
'pretix.event.order.email.error': _('Sending of an email has failed.'),
'pretix.event.order.email.attachments.skipped': _('The email has been sent without attached tickets since they '
'would have been too large to be likely to arrive.'),
'pretix.event.order.email.custom_sent': _('A custom email has been sent.'),
'pretix.event.order.position.email.custom_sent': _('A custom email has been sent to an attendee.'),
'pretix.event.order.email.download_reminder_sent': _('An email has been sent with a reminder that the ticket '
'is available for download.'),
'pretix.event.order.email.expire_warning_sent': _('An email has been sent with a warning that the order is about '
'to expire.'),
'pretix.event.order.email.order_canceled': _('An email has been sent to notify the user that the order has been canceled.'),
'pretix.event.order.email.event_canceled': _('An email has been sent to notify the user that the event has '
'been canceled.'),
'pretix.event.order.email.order_changed': _('An email has been sent to notify the user that the order has been changed.'),
'pretix.event.order.email.order_free': _('An email has been sent to notify the user that the order has been received.'),
'pretix.event.order.email.order_paid': _('An email has been sent to notify the user that payment has been received.'),
'pretix.event.order.email.order_denied': _('An email has been sent to notify the user that the order has been denied.'),
'pretix.event.order.email.order_approved': _('An email has been sent to notify the user that the order has '
'been approved.'),
'pretix.event.order.email.order_placed': _('An email has been sent to notify the user that the order has been received and requires payment.'),
'pretix.event.order.email.order_placed_require_approval': _('An email has been sent to notify the user that '
'the order has been received and requires '
'approval.'),
'pretix.event.order.email.resend': _('An email with a link to the order detail page has been resent to the user.'),
'pretix.event.order.email.payment_failed': _('An email has been sent to notify the user that the payment failed.'),
'pretix.event.order.payment.confirmed': _('Payment {local_id} has been confirmed.'),
'pretix.event.order.payment.canceled': _('Payment {local_id} has been canceled.'),
'pretix.event.order.payment.canceled.failed': _('Canceling payment {local_id} has failed.'),
'pretix.event.order.payment.started': _('Payment {local_id} has been started.'),
'pretix.event.order.payment.failed': _('Payment {local_id} has failed.'),
'pretix.event.order.quotaexceeded': _('The order could not be marked as paid: {message}'),
'pretix.event.order.overpaid': _('The order has been overpaid.'),
'pretix.event.order.refund.created': _('Refund {local_id} has been created.'),
'pretix.event.order.refund.created.externally': _('Refund {local_id} has been created by an external entity.'),
'pretix.event.order.refund.requested': _('The customer requested you to issue a refund.'),
'pretix.event.order.refund.done': _('Refund {local_id} has been completed.'),
'pretix.event.order.refund.canceled': _('Refund {local_id} has been canceled.'),
'pretix.event.order.refund.failed': _('Refund {local_id} has failed.'),
'pretix.event.export.schedule.added': _('A scheduled export has been added.'),
'pretix.event.export.schedule.changed': _('A scheduled export has been changed.'),
'pretix.event.export.schedule.deleted': _('A scheduled export has been deleted.'),
'pretix.event.export.schedule.executed': _('A scheduled export has been executed.'),
'pretix.event.export.schedule.failed': _('A scheduled export has failed: {reason}.'),
'pretix.control.auth.user.created': _('The user has been created.'),
'pretix.control.auth.user.new_source': _('A first login using {agent_type} on {os_type} from {country} has '
'been detected.'),
'pretix.user.settings.2fa.enabled': _('Two-factor authentication has been enabled.'),
'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'),
'pretix.user.settings.2fa.regenemergency': _('Your two-factor emergency codes have been regenerated.'),
'pretix.user.settings.2fa.emergency': _('A two-factor emergency code has been generated.'),
'pretix.user.settings.2fa.device.added': _('A new two-factor authentication device "{name}" has been added to '
'your account.'),
'pretix.user.settings.2fa.device.deleted': _('The two-factor authentication device "{name}" has been removed '
'from your account.'),
'pretix.user.settings.notifications.enabled': _('Notifications have been enabled.'),
'pretix.user.settings.notifications.disabled': _('Notifications have been disabled.'),
'pretix.user.settings.notifications.changed': _('Your notification settings have been changed.'),
'pretix.user.anonymized': _('This user has been anonymized.'),
'pretix.user.oauth.authorized': _('The application "{application_name}" has been authorized to access your '
'account.'),
'pretix.control.auth.user.forgot_password.mail_sent': _('Password reset mail sent.'),
'pretix.control.auth.user.forgot_password.recovered': _('The password has been reset.'),
'pretix.control.auth.user.forgot_password.denied.repeated': _('A repeated password reset has been denied, as '
'the last request was less than 24 hours ago.'),
'pretix.organizer.deleted': _('The organizer "{name}" has been deleted.'),
'pretix.voucher.added': _('The voucher has been created.'),
'pretix.voucher.sent': _('The voucher has been sent to {recipient}.'),
'pretix.voucher.added.waitinglist': _('The voucher has been created and sent to a person on the waiting list.'),
'pretix.voucher.expired.waitinglist': _('The voucher has been set to expire because the recipient removed themselves from the waiting list.'),
'pretix.voucher.changed': _('The voucher has been changed.'),
'pretix.voucher.deleted': _('The voucher has been deleted.'),
'pretix.voucher.redeemed': _('The voucher has been redeemed in order {order_code}.'),
'pretix.event.item.added': _('The product has been created.'),
'pretix.event.item.changed': _('The product has been changed.'),
'pretix.event.item.reordered': _('The product has been reordered.'),
'pretix.event.item.deleted': _('The product has been deleted.'),
'pretix.event.item.variation.added': _('The variation "{value}" has been created.'),
'pretix.event.item.variation.deleted': _('The variation "{value}" has been deleted.'),
'pretix.event.item.variation.changed': _('The variation "{value}" has been changed.'),
'pretix.event.item.addons.added': _('An add-on has been added to this product.'),
'pretix.event.item.addons.removed': _('An add-on has been removed from this product.'),
'pretix.event.item.addons.changed': _('An add-on has been changed on this product.'),
'pretix.event.item.bundles.added': _('A bundled item has been added to this product.'),
'pretix.event.item.bundles.removed': _('A bundled item has been removed from this product.'),
'pretix.event.item.bundles.changed': _('A bundled item has been changed on this product.'),
'pretix.event.item_meta_property.added': _('A meta property has been added to this event.'),
'pretix.event.item_meta_property.deleted': _('A meta property has been removed from this event.'),
'pretix.event.item_meta_property.changed': _('A meta property has been changed on this event.'),
'pretix.event.quota.added': _('The quota has been added.'),
'pretix.event.quota.deleted': _('The quota has been deleted.'),
'pretix.event.quota.changed': _('The quota has been changed.'),
'pretix.event.quota.closed': _('The quota has closed.'),
'pretix.event.quota.opened': _('The quota has been re-opened.'),
'pretix.event.category.added': _('The category has been added.'),
'pretix.event.category.deleted': _('The category has been deleted.'),
'pretix.event.category.changed': _('The category has been changed.'),
'pretix.event.category.reordered': _('The category has been reordered.'),
'pretix.event.question.added': _('The question has been added.'),
'pretix.event.question.deleted': _('The question has been deleted.'),
'pretix.event.question.changed': _('The question has been changed.'),
'pretix.event.question.reordered': _('The question has been reordered.'),
'pretix.event.discount.added': _('The discount has been added.'),
'pretix.event.discount.deleted': _('The discount has been deleted.'),
'pretix.event.discount.changed': _('The discount has been changed.'),
'pretix.event.taxrule.added': _('The tax rule has been added.'),
'pretix.event.taxrule.deleted': _('The tax rule has been deleted.'),
'pretix.event.taxrule.changed': _('The tax rule has been changed.'),
'pretix.event.checkinlist.added': _('The check-in list has been added.'),
'pretix.event.checkinlist.deleted': _('The check-in list has been deleted.'),
'pretix.event.checkinlists.deleted': _('The check-in list has been deleted.'), # backwards compatibility
'pretix.event.checkinlist.changed': _('The check-in list has been changed.'),
'pretix.event.settings': _('The event settings have been changed.'),
'pretix.event.tickets.settings': _('The ticket download settings have been changed.'),
'pretix.event.plugins.enabled': _('A plugin has been enabled.'),
'pretix.event.plugins.disabled': _('A plugin has been disabled.'),
'pretix.event.live.activated': _('The shop has been taken live.'),
'pretix.event.live.deactivated': _('The shop has been taken offline.'),
'pretix.event.testmode.activated': _('The shop has been taken into test mode.'),
'pretix.event.testmode.deactivated': _('The test mode has been disabled.'),
'pretix.event.added': _('The event has been created.'),
'pretix.event.changed': _('The event details have been changed.'),
'pretix.event.footerlinks.changed': _('The footer links have been changed.'),
'pretix.event.question.option.added': _('An answer option has been added to the question.'),
'pretix.event.question.option.deleted': _('An answer option has been removed from the question.'),
'pretix.event.question.option.changed': _('An answer option has been changed.'),
'pretix.event.permissions.added': _('A user has been added to the event team.'),
'pretix.event.permissions.invited': _('A user has been invited to the event team.'),
'pretix.event.permissions.changed': _('A user\'s permissions have been changed.'),
'pretix.event.permissions.deleted': _('A user has been removed from the event team.'),
'pretix.waitinglist.voucher': _('A voucher has been sent to a person on the waiting list.'), # legacy
'pretix.event.orders.waitinglist.voucher_assigned': _('A voucher has been sent to a person on the waiting list.'),
'pretix.event.orders.waitinglist.deleted': _('An entry has been removed from the waiting list.'),
'pretix.event.order.waitinglist.transferred': _('An entry has been transferred to another waiting list.'), # legacy
'pretix.event.orders.waitinglist.changed': _('An entry has been changed on the waiting list.'),
'pretix.event.orders.waitinglist.added': _('An entry has been added to the waiting list.'),
'pretix.team.created': _('The team has been created.'),
'pretix.team.changed': _('The team settings have been changed.'),
'pretix.team.deleted': _('The team has been deleted.'),
'pretix.gate.created': _('The gate has been created.'),
'pretix.gate.changed': _('The gate has been changed.'),
'pretix.gate.deleted': _('The gate has been deleted.'),
'pretix.subevent.deleted': pgettext_lazy('subevent', 'The event date has been deleted.'),
'pretix.subevent.canceled': pgettext_lazy('subevent', 'The event date has been canceled.'),
'pretix.subevent.changed': pgettext_lazy('subevent', 'The event date has been changed.'),
'pretix.subevent.added': pgettext_lazy('subevent', 'The event date has been created.'),
'pretix.subevent.quota.added': pgettext_lazy('subevent', 'A quota has been added to the event date.'),
'pretix.subevent.quota.changed': pgettext_lazy('subevent', 'A quota has been changed on the event date.'),
'pretix.subevent.quota.deleted': pgettext_lazy('subevent', 'A quota has been removed from the event date.'),
'pretix.device.created': _('The device has been created.'),
'pretix.device.changed': _('The device has been changed.'),
'pretix.device.revoked': _('Access of the device has been revoked.'),
'pretix.device.initialized': _('The device has been initialized.'),
'pretix.device.keyroll': _('The access token of the device has been regenerated.'),
'pretix.device.updated': _('The device has notified the server of an hardware or software update.'),
'pretix.giftcards.created': _('The gift card has been created.'),
'pretix.giftcards.modified': _('The gift card has been changed.'),
'pretix.giftcards.transaction.manual': _('A manual transaction has been performed.'),
}
data = json.loads(logentry.data)
if logentry.action_type.startswith('pretix.event.item.variation'):
if 'value' not in data:
# Backwards compatibility
var = ItemVariation.objects.filter(id=data['id']).first()
if var:
data['value'] = str(var.value)
else:
data['value'] = '?'
else:
data['value'] = LazyI18nString(data['value'])
if logentry.action_type == "pretix.voucher.redeemed":
data = defaultdict(lambda: '?', data)
url = reverse('control:event.order', kwargs={
'event': logentry.event.slug,
'organizer': logentry.event.organizer.slug,
'code': data['order_code']
})
return mark_safe(plains[logentry.action_type].format(
order_code='<a href="{}">{}</a>'.format(url, data['order_code']),
))
if logentry.action_type in plains:
data = defaultdict(lambda: '?', data)
return plains[logentry.action_type].format_map(data)
if logentry.action_type.startswith('pretix.event.order.changed'):
return _display_order_changed(sender, logentry)
@@ -624,16 +360,16 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
return _('The order has been canceled.')
if logentry.action_type in ('pretix.control.views.checkin.reverted', 'pretix.event.checkin.reverted'):
if 'list' in data:
if 'list' in logentry.parsed_data:
try:
checkin_list = sender.checkin_lists.get(pk=data.get('list')).name
checkin_list = sender.checkin_lists.get(pk=logentry.parsed_data.get('list')).name
except CheckinList.DoesNotExist:
checkin_list = _("(unknown)")
else:
checkin_list = _("(unknown)")
return _('The check-in of position #{posid} on list "{list}" has been reverted.').format(
posid=data.get('positionid'),
posid=logentry.parsed_data.get('positionid'),
list=checkin_list,
)
@@ -642,83 +378,14 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
if logentry.action_type == 'pretix.event.order.print':
return _('Position #{posid} has been printed at {datetime} with type "{type}".').format(
posid=data.get('positionid'),
posid=logentry.parsed_data.get('positionid'),
datetime=date_format(
dateutil.parser.parse(data["datetime"]).astimezone(sender.timezone),
dateutil.parser.parse(logentry.parsed_data["datetime"]).astimezone(sender.timezone),
"SHORT_DATETIME_FORMAT"
),
type=dict(PrintLog.PRINT_TYPES)[data["type"]],
type=dict(PrintLog.PRINT_TYPES)[logentry.parsed_data["type"]],
)
if logentry.action_type == 'pretix.control.views.checkin':
# deprecated
dt = dateutil.parser.parse(data.get('datetime'))
tz = sender.timezone
dt_formatted = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT")
if 'list' in data:
try:
checkin_list = sender.checkin_lists.get(pk=data.get('list')).name
except CheckinList.DoesNotExist:
checkin_list = _("(unknown)")
else:
checkin_list = _("(unknown)")
if data.get('first'):
return _('Position #{posid} has been checked in manually at {datetime} on list "{list}".').format(
posid=data.get('positionid'),
datetime=dt_formatted,
list=checkin_list,
)
return _('Position #{posid} has been checked in again at {datetime} on list "{list}".').format(
posid=data.get('positionid'),
datetime=dt_formatted,
list=checkin_list
)
if logentry.action_type == 'pretix.team.member.added':
return _('{user} has been added to the team.').format(user=data.get('email'))
if logentry.action_type == 'pretix.team.member.removed':
return _('{user} has been removed from the team.').format(user=data.get('email'))
if logentry.action_type == 'pretix.team.member.joined':
return _('{user} has joined the team using the invite sent to {email}.').format(
user=data.get('email'), email=data.get('invite_email')
)
if logentry.action_type == 'pretix.team.invite.created':
return _('{user} has been invited to the team.').format(user=data.get('email'))
if logentry.action_type == 'pretix.team.invite.resent':
return _('Invite for {user} has been resent.').format(user=data.get('email'))
if logentry.action_type == 'pretix.team.invite.deleted':
return _('The invite for {user} has been revoked.').format(user=data.get('email'))
if logentry.action_type == 'pretix.team.token.created':
return _('The token "{name}" has been created.').format(name=data.get('name'))
if logentry.action_type == 'pretix.team.token.deleted':
return _('The token "{name}" has been revoked.').format(name=data.get('name'))
if logentry.action_type == 'pretix.user.settings.changed':
text = str(_('Your account settings have been changed.'))
if 'email' in data:
text = text + ' ' + str(_('Your email address has been changed to {email}.').format(email=data['email']))
if 'new_pw' in data:
text = text + ' ' + str(_('Your password has been changed.'))
if data.get('is_active') is True:
text = text + ' ' + str(_('Your account has been enabled.'))
elif data.get('is_active') is False:
text = text + ' ' + str(_('Your account has been disabled.'))
return text
if logentry.action_type == 'pretix.control.auth.user.impersonated':
return str(_('You impersonated {}.')).format(data['other_email'])
if logentry.action_type == 'pretix.control.auth.user.impersonate_stopped':
return str(_('You stopped impersonating {}.')).format(data['other_email'])
@receiver(signal=orderposition_blocked_display, dispatch_uid="pretixcontrol_orderposition_blocked_display")
def pretixcontrol_orderposition_blocked_display(sender: Event, orderposition, block_name, **kwargs):
@@ -726,3 +393,479 @@ def pretixcontrol_orderposition_blocked_display(sender: Event, orderposition, bl
return _('Blocked manually')
elif block_name.startswith('api:'):
return _('Blocked because of an API integration')
@log_entry_types.new_from_dict({
'pretix.event.order.modified': _('The order details have been changed.'),
'pretix.event.order.unpaid': _('The order has been marked as unpaid.'),
'pretix.event.order.secret.changed': _('The order\'s secret has been changed.'),
'pretix.event.order.expirychanged': _('The order\'s expiry date has been changed.'),
'pretix.event.order.valid_if_pending.set': _('The order has been set to be usable before it is paid.'),
'pretix.event.order.valid_if_pending.unset': _('The order has been set to require payment before use.'),
'pretix.event.order.expired': _('The order has been marked as expired.'),
'pretix.event.order.cancellationrequest.deleted': _('The cancellation request has been deleted.'),
'pretix.event.order.refunded': _('The order has been refunded.'),
'pretix.event.order.reactivated': _('The order has been reactivated.'),
'pretix.event.order.deleted': _('The test mode order {code} has been deleted.'),
'pretix.event.order.placed': _('The order has been created.'),
'pretix.event.order.placed.require_approval': _(
'The order requires approval before it can continue to be processed.'),
'pretix.event.order.approved': _('The order has been approved.'),
'pretix.event.order.denied': _('The order has been denied (comment: "{comment}").'),
'pretix.event.order.contact.changed': _('The email address has been changed from "{old_email}" '
'to "{new_email}".'),
'pretix.event.order.contact.confirmed': _(
'The email address has been confirmed to be working (the user clicked on a link '
'in the email for the first time).'),
'pretix.event.order.phone.changed': _('The phone number has been changed from "{old_phone}" '
'to "{new_phone}".'),
'pretix.event.order.customer.changed': _('The customer account has been changed.'),
'pretix.event.order.locale.changed': _('The order locale has been changed.'),
'pretix.event.order.invoice.generated': _('The invoice has been generated.'),
'pretix.event.order.invoice.regenerated': _('The invoice has been regenerated.'),
'pretix.event.order.invoice.reissued': _('The invoice has been reissued.'),
'pretix.event.order.comment': _('The order\'s internal comment has been updated.'),
'pretix.event.order.custom_followup_at': _('The order\'s follow-up date has been updated.'),
'pretix.event.order.checkin_attention': _('The order\'s flag to require attention at check-in has been '
'toggled.'),
'pretix.event.order.checkin_text': _('The order\'s check-in text has been changed.'),
'pretix.event.order.pretix.event.order.valid_if_pending': _('The order\'s flag to be considered valid even if '
'unpaid has been toggled.'),
'pretix.event.order.payment.changed': _('A new payment {local_id} has been started instead of the previous one.'),
'pretix.event.order.email.sent': _('An unidentified type email has been sent.'),
'pretix.event.order.email.error': _('Sending of an email has failed.'),
'pretix.event.order.email.attachments.skipped': _('The email has been sent without attached tickets since they '
'would have been too large to be likely to arrive.'),
'pretix.event.order.email.custom_sent': _('A custom email has been sent.'),
'pretix.event.order.position.email.custom_sent': _('A custom email has been sent to an attendee.'),
'pretix.event.order.email.download_reminder_sent': _('An email has been sent with a reminder that the ticket '
'is available for download.'),
'pretix.event.order.email.expire_warning_sent': _('An email has been sent with a warning that the order is about '
'to expire.'),
'pretix.event.order.email.order_canceled': _(
'An email has been sent to notify the user that the order has been canceled.'),
'pretix.event.order.email.event_canceled': _('An email has been sent to notify the user that the event has '
'been canceled.'),
'pretix.event.order.email.order_changed': _(
'An email has been sent to notify the user that the order has been changed.'),
'pretix.event.order.email.order_free': _(
'An email has been sent to notify the user that the order has been received.'),
'pretix.event.order.email.order_paid': _(
'An email has been sent to notify the user that payment has been received.'),
'pretix.event.order.email.order_denied': _(
'An email has been sent to notify the user that the order has been denied.'),
'pretix.event.order.email.order_approved': _('An email has been sent to notify the user that the order has '
'been approved.'),
'pretix.event.order.email.order_placed': _(
'An email has been sent to notify the user that the order has been received and requires payment.'),
'pretix.event.order.email.order_placed_require_approval': _('An email has been sent to notify the user that '
'the order has been received and requires '
'approval.'),
'pretix.event.order.email.resend': _('An email with a link to the order detail page has been resent to the user.'),
'pretix.event.order.email.payment_failed': _('An email has been sent to notify the user that the payment failed.'),
})
class CoreOrderLogEntryType(OrderLogEntryType):
pass
@log_entry_types.new()
class OrderPaidLogEntryType(CoreOrderLogEntryType):
action_type = 'pretix.event.order.paid'
plain = _('The order has been marked as paid.')
data_schema = {
"type": "object",
"properties": {
"provider": {"type": "string", "shred": False, },
"info": {"type": ["null", "string", "object"], "shred": True, },
"date": {"type": "string", "shred": False, },
"force": {"type": "boolean", "shred": False, },
},
}
@log_entry_types.new_from_dict({
'pretix.voucher.added': _('The voucher has been created.'),
'pretix.voucher.sent': _('The voucher has been sent to {recipient}.'),
'pretix.voucher.added.waitinglist': _('The voucher has been created and sent to a person on the waiting list.'),
'pretix.voucher.expired.waitinglist': _(
'The voucher has been set to expire because the recipient removed themselves from the waiting list.'),
'pretix.voucher.changed': _('The voucher has been changed.'),
'pretix.voucher.deleted': _('The voucher has been deleted.'),
})
class CoreVoucherLogEntryType(VoucherLogEntryType):
pass
@log_entry_types.new()
class VoucherRedeemedLogEntryType(VoucherLogEntryType):
action_type = 'pretix.voucher.redeemed'
plain = _('The voucher has been redeemed in order {order_code}.')
data_schema = {
"type": "object",
"properties": {
"order_code": {"type": "string", "shred": False, },
},
}
def display(self, logentry):
data = json.loads(logentry.data)
data = defaultdict(lambda: '?', data)
url = reverse('control:event.order', kwargs={
'event': logentry.event.slug,
'organizer': logentry.event.organizer.slug,
'code': data['order_code']
})
return mark_safe(self.plain.format(
order_code='<a href="{}">{}</a>'.format(url, data['order_code']),
))
@log_entry_types.new_from_dict({
'pretix.event.category.added': _('The category has been added.'),
'pretix.event.category.deleted': _('The category has been deleted.'),
'pretix.event.category.changed': _('The category has been changed.'),
'pretix.event.category.reordered': _('The category has been reordered.'),
})
class CoreItemCategoryLogEntryType(ItemCategoryLogEntryType):
pass
@log_entry_types.new_from_dict({
'pretix.event.taxrule.added': _('The tax rule has been added.'),
'pretix.event.taxrule.deleted': _('The tax rule has been deleted.'),
'pretix.event.taxrule.changed': _('The tax rule has been changed.'),
})
class CoreTaxRuleLogEntryType(TaxRuleLogEntryType):
pass
class TeamMembershipLogEntryType(LogEntryType):
def display(self, logentry):
return self.plain.format(user=logentry.parsed_data.get('email'))
@log_entry_types.new_from_dict({
'pretix.team.member.added': _('{user} has been added to the team.'),
'pretix.team.member.removed': _('{user} has been removed from the team.'),
'pretix.team.invite.created': _('{user} has been invited to the team.'),
'pretix.team.invite.resent': _('Invite for {user} has been resent.'),
})
class CoreTeamMembershipLogEntryType(TeamMembershipLogEntryType):
pass
@log_entry_types.new()
class TeamMemberJoinedLogEntryType(LogEntryType):
action_type = 'pretix.team.member.joined'
def display(self, logentry):
return _('{user} has joined the team using the invite sent to {email}.').format(
user=logentry.parsed_data.get('email'), email=logentry.parsed_data.get('invite_email')
)
@log_entry_types.new()
class UserSettingsChangedLogEntryType(LogEntryType):
action_type = 'pretix.user.settings.changed'
def display(self, logentry):
text = str(_('Your account settings have been changed.'))
if 'email' in logentry.parsed_data:
text = text + ' ' + str(
_('Your email address has been changed to {email}.').format(email=logentry.parsed_data['email']))
if 'new_pw' in logentry.parsed_data:
text = text + ' ' + str(_('Your password has been changed.'))
if logentry.parsed_data.get('is_active') is True:
text = text + ' ' + str(_('Your account has been enabled.'))
elif logentry.parsed_data.get('is_active') is False:
text = text + ' ' + str(_('Your account has been disabled.'))
return text
class UserImpersonatedLogEntryType(LogEntryType):
def display(self, logentry):
return self.plain.format(logentry.parsed_data['other_email'])
@log_entry_types.new_from_dict({
'pretix.control.auth.user.impersonated': _('You impersonated {}.'),
'pretix.control.auth.user.impersonate_stopped': _('You stopped impersonating {}.'),
})
class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
pass
@log_entry_types.new_from_dict({
'pretix.object.cloned': _('This object has been created by cloning.'),
'pretix.organizer.changed': _('The organizer has been changed.'),
'pretix.organizer.settings': _('The organizer settings have been changed.'),
'pretix.organizer.footerlinks.changed': _('The footer links have been changed.'),
'pretix.organizer.export.schedule.added': _('A scheduled export has been added.'),
'pretix.organizer.export.schedule.changed': _('A scheduled export has been changed.'),
'pretix.organizer.export.schedule.deleted': _('A scheduled export has been deleted.'),
'pretix.organizer.export.schedule.executed': _('A scheduled export has been executed.'),
'pretix.organizer.export.schedule.failed': _('A scheduled export has failed: {reason}.'),
'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'),
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
'pretix.giftcards.acceptance.acceptor.invited': _('A new gift card acceptor has been invited.'),
'pretix.giftcards.acceptance.acceptor.removed': _('A gift card acceptor has been removed.'),
'pretix.giftcards.acceptance.issuer.removed': _('A gift card issuer has been removed or declined.'),
'pretix.giftcards.acceptance.issuer.accepted': _('A new gift card issuer has been accepted.'),
'pretix.webhook.created': _('The webhook has been created.'),
'pretix.webhook.changed': _('The webhook has been changed.'),
'pretix.webhook.retries.expedited': _('The webhook call retry jobs have been manually expedited.'),
'pretix.webhook.retries.dropped': _('The webhook call retry jobs have been dropped.'),
'pretix.ssoprovider.created': _('The SSO provider has been created.'),
'pretix.ssoprovider.changed': _('The SSO provider has been changed.'),
'pretix.ssoprovider.deleted': _('The SSO provider has been deleted.'),
'pretix.ssoclient.created': _('The SSO client has been created.'),
'pretix.ssoclient.changed': _('The SSO client has been changed.'),
'pretix.ssoclient.deleted': _('The SSO client has been deleted.'),
'pretix.membershiptype.created': _('The membership type has been created.'),
'pretix.membershiptype.changed': _('The membership type has been changed.'),
'pretix.membershiptype.deleted': _('The membership type has been deleted.'),
'pretix.saleschannel.created': _('The sales channel has been created.'),
'pretix.saleschannel.changed': _('The sales channel has been changed.'),
'pretix.saleschannel.deleted': _('The sales channel has been deleted.'),
'pretix.customer.created': _('The account has been created.'),
'pretix.customer.changed': _('The account has been changed.'),
'pretix.customer.membership.created': _('A membership for this account has been added.'),
'pretix.customer.membership.changed': _('A membership of this account has been changed.'),
'pretix.customer.membership.deleted': _('A membership of this account has been deleted.'),
'pretix.customer.anonymized': _('The account has been disabled and anonymized.'),
'pretix.customer.password.resetrequested': _('A new password has been requested.'),
'pretix.customer.password.set': _('A new password has been set.'),
'pretix.reusable_medium.created': _('The reusable medium has been created.'),
'pretix.reusable_medium.created.auto': _('The reusable medium has been created automatically.'),
'pretix.reusable_medium.changed': _('The reusable medium has been changed.'),
'pretix.reusable_medium.linked_orderposition.changed': _('The medium has been connected to a new ticket.'),
'pretix.reusable_medium.linked_giftcard.changed': _('The medium has been connected to a new gift card.'),
'pretix.email.error': _('Sending of an email has failed.'),
'pretix.event.comment': _('The event\'s internal comment has been updated.'),
'pretix.event.canceled': _('The event has been canceled.'),
'pretix.event.deleted': _('An event has been deleted.'),
'pretix.event.shredder.started': _('A removal process for personal data has been started.'),
'pretix.event.shredder.completed': _('A removal process for personal data has been completed.'),
'pretix.event.export.schedule.added': _('A scheduled export has been added.'),
'pretix.event.export.schedule.changed': _('A scheduled export has been changed.'),
'pretix.event.export.schedule.deleted': _('A scheduled export has been deleted.'),
'pretix.event.export.schedule.executed': _('A scheduled export has been executed.'),
'pretix.event.export.schedule.failed': _('A scheduled export has failed: {reason}.'),
'pretix.control.auth.user.created': _('The user has been created.'),
'pretix.control.auth.user.new_source': _('A first login using {agent_type} on {os_type} from {country} has '
'been detected.'),
'pretix.user.settings.2fa.enabled': _('Two-factor authentication has been enabled.'),
'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'),
'pretix.user.settings.2fa.regenemergency': _('Your two-factor emergency codes have been regenerated.'),
'pretix.user.settings.2fa.emergency': _('A two-factor emergency code has been generated.'),
'pretix.user.settings.2fa.device.added': _('A new two-factor authentication device "{name}" has been added to '
'your account.'),
'pretix.user.settings.2fa.device.deleted': _('The two-factor authentication device "{name}" has been removed '
'from your account.'),
'pretix.user.settings.notifications.enabled': _('Notifications have been enabled.'),
'pretix.user.settings.notifications.disabled': _('Notifications have been disabled.'),
'pretix.user.settings.notifications.changed': _('Your notification settings have been changed.'),
'pretix.user.anonymized': _('This user has been anonymized.'),
'pretix.user.oauth.authorized': _('The application "{application_name}" has been authorized to access your '
'account.'),
'pretix.control.auth.user.forgot_password.mail_sent': _('Password reset mail sent.'),
'pretix.control.auth.user.forgot_password.recovered': _('The password has been reset.'),
'pretix.control.auth.user.forgot_password.denied.repeated': _('A repeated password reset has been denied, as '
'the last request was less than 24 hours ago.'),
'pretix.organizer.deleted': _('The organizer "{name}" has been deleted.'),
'pretix.waitinglist.voucher': _('A voucher has been sent to a person on the waiting list.'), # legacy
'pretix.event.orders.waitinglist.voucher_assigned': _('A voucher has been sent to a person on the waiting list.'),
'pretix.event.orders.waitinglist.deleted': _('An entry has been removed from the waiting list.'),
'pretix.event.order.waitinglist.transferred': _('An entry has been transferred to another waiting list.'), # legacy
'pretix.event.orders.waitinglist.changed': _('An entry has been changed on the waiting list.'),
'pretix.event.orders.waitinglist.added': _('An entry has been added to the waiting list.'),
'pretix.team.created': _('The team has been created.'),
'pretix.team.changed': _('The team settings have been changed.'),
'pretix.team.deleted': _('The team has been deleted.'),
'pretix.gate.created': _('The gate has been created.'),
'pretix.gate.changed': _('The gate has been changed.'),
'pretix.gate.deleted': _('The gate has been deleted.'),
'pretix.subevent.deleted': pgettext_lazy('subevent', 'The event date has been deleted.'),
'pretix.subevent.canceled': pgettext_lazy('subevent', 'The event date has been canceled.'),
'pretix.subevent.changed': pgettext_lazy('subevent', 'The event date has been changed.'),
'pretix.subevent.added': pgettext_lazy('subevent', 'The event date has been created.'),
'pretix.subevent.quota.added': pgettext_lazy('subevent', 'A quota has been added to the event date.'),
'pretix.subevent.quota.changed': pgettext_lazy('subevent', 'A quota has been changed on the event date.'),
'pretix.subevent.quota.deleted': pgettext_lazy('subevent', 'A quota has been removed from the event date.'),
'pretix.device.created': _('The device has been created.'),
'pretix.device.changed': _('The device has been changed.'),
'pretix.device.revoked': _('Access of the device has been revoked.'),
'pretix.device.initialized': _('The device has been initialized.'),
'pretix.device.keyroll': _('The access token of the device has been regenerated.'),
'pretix.device.updated': _('The device has notified the server of an hardware or software update.'),
'pretix.giftcards.created': _('The gift card has been created.'),
'pretix.giftcards.modified': _('The gift card has been changed.'),
'pretix.giftcards.transaction.manual': _('A manual transaction has been performed.'),
'pretix.team.token.created': _('The token "{name}" has been created.'),
'pretix.team.token.deleted': _('The token "{name}" has been revoked.'),
})
class CoreLogEntryType(LogEntryType):
pass
@log_entry_types.new_from_dict({
'pretix.event.item_meta_property.added': _('A meta property has been added to this event.'),
'pretix.event.item_meta_property.deleted': _('A meta property has been removed from this event.'),
'pretix.event.item_meta_property.changed': _('A meta property has been changed on this event.'),
'pretix.event.checkinlist.added': _('The check-in list has been added.'),
'pretix.event.checkinlist.deleted': _('The check-in list has been deleted.'),
'pretix.event.checkinlists.deleted': _('The check-in list has been deleted.'), # backwards compatibility
'pretix.event.checkinlist.changed': _('The check-in list has been changed.'),
'pretix.event.settings': _('The event settings have been changed.'),
'pretix.event.tickets.settings': _('The ticket download settings have been changed.'),
'pretix.event.live.activated': _('The shop has been taken live.'),
'pretix.event.live.deactivated': _('The shop has been taken offline.'),
'pretix.event.testmode.activated': _('The shop has been taken into test mode.'),
'pretix.event.testmode.deactivated': _('The test mode has been disabled.'),
'pretix.event.added': _('The event has been created.'),
'pretix.event.changed': _('The event details have been changed.'),
'pretix.event.footerlinks.changed': _('The footer links have been changed.'),
'pretix.event.question.option.added': _('An answer option has been added to the question.'),
'pretix.event.question.option.deleted': _('An answer option has been removed from the question.'),
'pretix.event.question.option.changed': _('An answer option has been changed.'),
'pretix.event.permissions.added': _('A user has been added to the event team.'),
'pretix.event.permissions.invited': _('A user has been invited to the event team.'),
'pretix.event.permissions.changed': _('A user\'s permissions have been changed.'),
'pretix.event.permissions.deleted': _('A user has been removed from the event team.'),
})
class CoreEventLogEntryType(EventLogEntryType):
pass
@log_entry_types.new_from_dict({
'pretix.event.plugins.enabled': _('The plugin has been enabled.'),
'pretix.event.plugins.disabled': _('The plugin has been disabled.'),
})
class EventPluginStateLogEntryType(EventLogEntryType):
object_link_wrapper = _('Plugin {val}')
def get_object_link_info(self, logentry) -> dict:
if 'plugin' in logentry.parsed_data:
app = app_cache.get(logentry.parsed_data['plugin'])
if app and hasattr(app, 'PretixPluginMeta'):
return {
'href': reverse('control:event.settings.plugins', kwargs={
'organizer': logentry.event.organizer.slug,
'event': logentry.event.slug,
}) + '#plugin_' + logentry.parsed_data['plugin'],
'val': app.PretixPluginMeta.name
}
@log_entry_types.new_from_dict({
'pretix.event.item.added': _('The product has been created.'),
'pretix.event.item.changed': _('The product has been changed.'),
'pretix.event.item.reordered': _('The product has been reordered.'),
'pretix.event.item.deleted': _('The product has been deleted.'),
'pretix.event.item.addons.added': _('An add-on has been added to this product.'),
'pretix.event.item.addons.removed': _('An add-on has been removed from this product.'),
'pretix.event.item.addons.changed': _('An add-on has been changed on this product.'),
'pretix.event.item.bundles.added': _('A bundled item has been added to this product.'),
'pretix.event.item.bundles.removed': _('A bundled item has been removed from this product.'),
'pretix.event.item.bundles.changed': _('A bundled item has been changed on this product.'),
})
class CoreItemLogEntryType(ItemLogEntryType):
pass
@log_entry_types.new_from_dict({
'pretix.event.item.variation.added': _('The variation "{value}" has been created.'),
'pretix.event.item.variation.deleted': _('The variation "{value}" has been deleted.'),
'pretix.event.item.variation.changed': _('The variation "{value}" has been changed.'),
})
class VariationLogEntryType(ItemLogEntryType):
def display(self, logentry):
if 'value' not in logentry.parsed_data:
# Backwards compatibility
var = ItemVariation.objects.filter(id=logentry.parsed_data['id']).first()
if var:
logentry.parsed_data['value'] = str(var.value)
else:
logentry.parsed_data['value'] = '?'
else:
logentry.parsed_data['value'] = LazyI18nString(logentry.parsed_data['value'])
return super().display(logentry)
@log_entry_types.new_from_dict({
'pretix.event.order.payment.confirmed': _('Payment {local_id} has been confirmed.'),
'pretix.event.order.payment.canceled': _('Payment {local_id} has been canceled.'),
'pretix.event.order.payment.canceled.failed': _('Canceling payment {local_id} has failed.'),
'pretix.event.order.payment.started': _('Payment {local_id} has been started.'),
'pretix.event.order.payment.failed': _('Payment {local_id} has failed.'),
'pretix.event.order.quotaexceeded': _('The order could not be marked as paid: {message}'),
'pretix.event.order.overpaid': _('The order has been overpaid.'),
'pretix.event.order.refund.created': _('Refund {local_id} has been created.'),
'pretix.event.order.refund.created.externally': _('Refund {local_id} has been created by an external entity.'),
'pretix.event.order.refund.requested': _('The customer requested you to issue a refund.'),
'pretix.event.order.refund.done': _('Refund {local_id} has been completed.'),
'pretix.event.order.refund.canceled': _('Refund {local_id} has been canceled.'),
'pretix.event.order.refund.failed': _('Refund {local_id} has failed.'),
})
class CoreOrderPaymentLogEntryType(OrderLogEntryType):
pass
@log_entry_types.new_from_dict({
'pretix.event.quota.added': _('The quota has been added.'),
'pretix.event.quota.deleted': _('The quota has been deleted.'),
'pretix.event.quota.changed': _('The quota has been changed.'),
'pretix.event.quota.closed': _('The quota has closed.'),
'pretix.event.quota.opened': _('The quota has been re-opened.'),
})
class CoreQuotaLogEntryType(QuotaLogEntryType):
pass
@log_entry_types.new_from_dict({
'pretix.event.question.added': _('The question has been added.'),
'pretix.event.question.deleted': _('The question has been deleted.'),
'pretix.event.question.changed': _('The question has been changed.'),
'pretix.event.question.reordered': _('The question has been reordered.'),
})
class CoreQuestionLogEntryType(QuestionLogEntryType):
pass
@log_entry_types.new_from_dict({
'pretix.event.discount.added': _('The discount has been added.'),
'pretix.event.discount.deleted': _('The discount has been deleted.'),
'pretix.event.discount.changed': _('The discount has been changed.'),
})
class CoreDiscountLogEntryType(DiscountLogEntryType):
pass
@log_entry_types.new()
class LegacyCheckinLogEntryType(OrderLogEntryType):
action_type = 'pretix.control.views.checkin'
def display(self, logentry):
# deprecated
dt = dateutil.parser.parse(logentry.parsed_data.get('datetime'))
tz = logentry.event.timezone
dt_formatted = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT")
if 'list' in logentry.parsed_data:
try:
checkin_list = logentry.event.checkin_lists.get(pk=logentry.parsed_data.get('list')).name
except CheckinList.DoesNotExist:
checkin_list = _("(unknown)")
else:
checkin_list = _("(unknown)")
if logentry.parsed_data.get('first'):
return _('Position #{posid} has been checked in manually at {datetime} on list "{list}".').format(
posid=logentry.parsed_data.get('positionid'),
datetime=dt_formatted,
list=checkin_list,
)
return _('Position #{posid} has been checked in again at {datetime} on list "{list}".').format(
posid=logentry.parsed_data.get('positionid'),
datetime=dt_formatted,
list=checkin_list
)

View File

@@ -61,6 +61,7 @@
<script type="text/javascript" src="{% static "fileupload/jquery.fileupload.js" %}"></script>
<script type="text/javascript" src="{% static "lightbox/js/lightbox.js" %}"></script>
<script type="text/javascript" src="{% static "are-you-sure/jquery.are-you-sure.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script>
{% endcompress %}
{{ html_head|safe }}

View File

@@ -23,7 +23,7 @@
<legend>{{ catlabel }}</legend>
<div class="plugin-list">
{% for plugin in plist %}
<div class="plugin-container {% if plugin.featured %}featured-plugin{% endif %}">
<div class="plugin-container {% if plugin.featured %}featured-plugin{% endif %}" id="plugin_{{ plugin.module }}">
{% if plugin.featured %}
<div class="panel panel-default">
<div class="panel-body">

View File

@@ -22,7 +22,7 @@
{% bootstrap_field form.name layout="control" %}
{% bootstrap_field form.slug layout="control" %}
{% if form.domain %}
{% bootstrap_field form.domain layout="control" %}
{% bootstrap_field form.domain layout="horizontal" %}
{% endif %}
{% bootstrap_field form.date_from layout="control" %}
{% bootstrap_field form.date_to layout="control" %}

View File

@@ -220,7 +220,6 @@
{% endif %}
{% bootstrap_field formset.empty_form.available_from visibility_field=formset.empty_form.available_from_mode layout="control_with_visibility" %}
{% bootstrap_field formset.empty_form.available_until visibility_field=formset.empty_form.available_until_mode layout="control_with_visibility" %}
{% bootstrap_field formset.empty_form.available_until layout="control" %}
{% bootstrap_field formset.empty_form.all_sales_channels layout="control" %}
{% bootstrap_field formset.empty_form.limit_sales_channels layout="control" %}
{% bootstrap_field formset.empty_form.hide_without_voucher layout="control" %}

View File

@@ -46,11 +46,13 @@
<div id="cp{{ pos.id }}">
<div class="panel-body">
{% for form in forms %}
{% if form.pos.item != pos.item %}
{# Add-Ons #}
<legend>+ {{ form.pos.item }}</legend>
{% endif %}
{% bootstrap_form form layout="control" %}
<div class="profile-scope">
{% if form.pos.item != pos.item %}
{# Add-Ons #}
<legend>+ {{ form.pos.item }}</legend>
{% endif %}
{% bootstrap_form form layout="control" %}
</div>
{% endfor %}
</div>
</div>

View File

@@ -294,6 +294,71 @@
<legend>{% trans "Invoices" %}</legend>
{% bootstrap_field sform.invoice_regenerate_allowed layout="control" %}
</fieldset>
{% if domain_formset %}
<fieldset>
<legend>{% trans "Domains" %}</legend>
<div class="alert alert-warning">
{% trans "This dialog is intended for advanced users." %}
{% trans "The domain needs to be configured on your webserver before it can be used here." %}
</div>
<div class="formset" data-formset data-formset-prefix="{{ domain_formset.prefix }}">
{{ domain_formset.management_form }}
{% bootstrap_formset_errors domain_formset %}
<div data-formset-body>
{% for form in domain_formset %}
<div class="row formset-row" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-md-4">
{% bootstrap_field form.domainname layout='' form_group_class="" %}
{% bootstrap_form_errors form %}
</div>
<div class="col-md-3">
{% bootstrap_field form.mode layout='' form_group_class="" %}
</div>
<div class="col-md-3">
{% bootstrap_field form.event layout='' form_group_class="" %}
</div>
<div class="col-md-2 text-right flip">
<label aria-hidden="true">&nbsp;</label><br>
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="row formset-row" data-formset-form>
<div class="sr-only">
{{ domain_formset.empty_form.id }}
{% bootstrap_field domain_formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-md-4">
{% bootstrap_field domain_formset.empty_form.domainname layout='' form_group_class="" %}
</div>
<div class="col-md-3">
{% bootstrap_field domain_formset.empty_form.mode layout='' form_group_class="" %}
</div>
<div class="col-md-3">
{% bootstrap_field domain_formset.empty_form.event layout='' form_group_class="" %}
</div>
<div class="col-md-2 text-right flip">
<label aria-hidden="true">&nbsp;</label><br>
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add domain" %}</button>
</p>
</fieldset>
{% endif %}
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">

View File

@@ -38,6 +38,7 @@ from zoneinfo import ZoneInfo
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.contrib.humanize.templatetags.humanize import intcomma
from django.db.models import (
Count, IntegerField, Max, Min, OuterRef, Prefetch, Q, Subquery, Sum,
)
@@ -47,7 +48,6 @@ from django.http import JsonResponse
from django.shortcuts import render
from django.template.loader import get_template
from django.urls import reverse
from django.utils import formats
from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.timezone import now
@@ -67,6 +67,7 @@ from pretix.control.signals import (
from pretix.helpers.daterange import daterange
from ...base.models.orders import CancellationRequest
from ...base.templatetags.money import money_filter
from ..logdisplay import OVERVIEW_BANLIST
NUM_WIDGET = '<div class="numwidget"><span class="num">{num}</span><span class="text">{text}</span></div>'
@@ -111,7 +112,7 @@ def base_widgets(sender, subevent=None, lazy=False, **kwargs):
return [
{
'content': None if lazy else NUM_WIDGET.format(num=tickc, text=_('Attendees (ordered)')),
'content': None if lazy else NUM_WIDGET.format(num=intcomma(tickc), text=_('Attendees (ordered)')),
'lazy': 'attendees-ordered',
'display_size': 'small',
'priority': 100,
@@ -121,7 +122,7 @@ def base_widgets(sender, subevent=None, lazy=False, **kwargs):
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
},
{
'content': None if lazy else NUM_WIDGET.format(num=paidc, text=_('Attendees (paid)')),
'content': None if lazy else NUM_WIDGET.format(num=intcomma(paidc), text=_('Attendees (paid)')),
'lazy': 'attendees-paid',
'display_size': 'small',
'priority': 100,
@@ -132,7 +133,9 @@ def base_widgets(sender, subevent=None, lazy=False, **kwargs):
},
{
'content': None if lazy else NUM_WIDGET.format(
num=formats.localize(round_decimal(rev, sender.currency)), text=_('Total revenue ({currency})').format(currency=sender.currency)),
num=money_filter(round_decimal(rev, sender.currency), sender.currency, hide_currency=True),
text=_('Total revenue ({currency})').format(currency=sender.currency)
),
'lazy': 'total-revenue',
'display_size': 'small',
'priority': 100,
@@ -207,7 +210,7 @@ def waitinglist_widgets(sender, subevent=None, lazy=False, **kwargs):
widgets.append({
'content': None if lazy else NUM_WIDGET.format(
num=str(happy), text=_('available to give to people on waiting list')
num=intcomma(happy), text=_('available to give to people on waiting list')
),
'lazy': 'waitinglist-avail',
'priority': 50,
@@ -217,7 +220,7 @@ def waitinglist_widgets(sender, subevent=None, lazy=False, **kwargs):
})
})
widgets.append({
'content': None if lazy else NUM_WIDGET.format(num=str(wles.count()), text=_('total waiting list length')),
'content': None if lazy else NUM_WIDGET.format(num=intcomma(wles.count()), text=_('total waiting list length')),
'lazy': 'waitinglist-length',
'display_size': 'small',
'priority': 50,
@@ -245,7 +248,7 @@ def quota_widgets(sender, subevent=None, lazy=False, **kwargs):
status, left = qa.results[q] if q in qa.results else q.availability(allow_cache=True)
widgets.append({
'content': None if lazy else NUM_WIDGET.format(
num='{}/{}'.format(left, q.size) if q.size is not None else '\u221e',
num='{}/{}'.format(intcomma(left), intcomma(q.size)) if q.size is not None else '\u221e',
text=_('{quota} left').format(quota=escape(q.name))
),
'lazy': 'quota-{}'.format(q.pk),
@@ -297,7 +300,7 @@ def checkin_widget(sender, subevent=None, lazy=False, **kwargs):
for cl in qs:
widgets.append({
'content': None if lazy else NUM_WIDGET.format(
num='{}/{}'.format(cl.inside_count, cl.position_count),
num='{}/{}'.format(intcomma(cl.inside_count), intcomma(cl.position_count)),
text=_('Present {list}').format(list=escape(cl.name))
),
'lazy': 'checkin-{}'.format(cl.pk),

View File

@@ -62,7 +62,7 @@ from django.http import (
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.html import escape
from django.utils.html import conditional_escape
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy as _, gettext_noop
@@ -100,9 +100,12 @@ from ...base.models.items import (
Item, ItemCategory, ItemMetaProperty, Question, Quota,
)
from ...base.services.mail import prefix_subject
from ...base.services.placeholders import get_sample_context
from ...base.settings import LazyI18nStringList
from ...helpers.compat import CompatDeleteView
from ...helpers.format import format_map
from ...helpers.format import (
PlainHtmlAlternativeString, SafeFormatter, format_map,
)
from ..logdisplay import OVERVIEW_BANLIST
from . import CreateView, PaginationMixin, UpdateView
@@ -239,7 +242,6 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
kwargs = super().get_form_kwargs()
if self.request.user.has_active_staff_session(self.request.session.session_key):
kwargs['change_slug'] = True
kwargs['domain'] = True
return kwargs
def post(self, request, *args, **kwargs):
@@ -717,20 +719,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
# get all supported placeholders with dummy values
def placeholders(self, item):
ctx = {}
for p in get_available_placeholders(self.request.event, MailSettingsForm.base_context[item]).values():
s = str(p.render_sample(self.request.event))
if s.strip().startswith('* '):
ctx[p.identifier] = '<div class="placeholder" title="{}">{}</div>'.format(
_('This value will be replaced based on dynamic parameters.'),
markdown_compile_email(s)
)
else:
ctx[p.identifier] = '<span class="placeholder" title="{}">{}</span>'.format(
_('This value will be replaced based on dynamic parameters.'),
escape(s)
)
return ctx
return get_sample_context(self.request.event, MailSettingsForm.base_context[item])
def post(self, request, *args, **kwargs):
preview_item = request.POST.get('item', '')
@@ -752,9 +741,15 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
bleach.clean(v), self.placeholders(preview_item), raise_on_missing=True
), highlight=True)
else:
msgs[self.supported_locale[idx]] = markdown_compile_email(
format_map(v, self.placeholders(preview_item), raise_on_missing=True)
placeholders = self.placeholders(preview_item)
msgs[self.supported_locale[idx]] = format_map(
markdown_compile_email(
format_map(v, placeholders, raise_on_missing=True)
),
placeholders,
mode=SafeFormatter.MODE_RICH_TO_HTML,
)
except ValueError:
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
PlaceholderValidator.error_message)
@@ -777,13 +772,18 @@ class MailSettingsRendererPreview(MailSettingsPreview):
# get all supported placeholders with dummy values
def placeholders(self, item):
ctx = {}
for p in get_available_placeholders(self.request.event, MailSettingsForm.base_context[item]).values():
ctx[p.identifier] = escape(str(p.render_sample(self.request.event)))
for p in get_available_placeholders(self.request.event, MailSettingsForm.base_context[item], rich=True).values():
sample = p.render_sample(self.request.event)
if isinstance(sample, PlainHtmlAlternativeString):
ctx[p.identifier] = sample
else:
ctx[p.identifier] = conditional_escape(sample)
return ctx
def get(self, request, *args, **kwargs):
v = str(request.event.settings.mail_text_order_placed)
v = format_map(v, self.placeholders('mail_text_order_placed'))
context = self.placeholders('mail_text_order_placed')
v = format_map(v, context)
renderers = request.event.get_html_mail_renderers()
if request.GET.get('renderer') in renderers:
with rolledback_transaction():
@@ -801,7 +801,8 @@ class MailSettingsRendererPreview(MailSettingsPreview):
str(request.event.settings.mail_text_signature),
gettext('Your order: %(code)s') % {'code': order.code},
order,
position=None
position=None,
context=context,
)
r = HttpResponse(v, content_type='text/html')
r._csp_ignore = True

View File

@@ -134,7 +134,7 @@ from pretix.control.signals import order_search_forms
from pretix.control.views import PaginationMixin
from pretix.helpers import OF_SELF
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.format import format_map
from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.safedownload import check_token
from pretix.presale.signals import question_form_fields
@@ -2351,7 +2351,7 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
'subject': mark_safe(_('Subject: {subject}').format(
subject=prefix_subject(order.event, escape(email_subject), highlight=True)
)),
'html': markdown_compile_email(email_content)
'html': format_map(markdown_compile_email(email_content), email_context, mode=SafeFormatter.MODE_RICH_TO_HTML)
}
return self.get(self.request, *self.args, **self.kwargs)
else:

View File

@@ -104,11 +104,11 @@ from pretix.control.forms.organizer import (
CustomerCreateForm, CustomerUpdateForm, DeviceBulkEditForm, DeviceForm,
EventMetaPropertyAllowedValueFormSet, EventMetaPropertyForm, GateForm,
GiftCardAcceptanceInviteForm, GiftCardCreateForm, GiftCardUpdateForm,
MailSettingsForm, MembershipTypeForm, MembershipUpdateForm,
OrganizerDeleteForm, OrganizerFooterLinkFormset, OrganizerForm,
OrganizerSettingsForm, OrganizerUpdateForm, ReusableMediumCreateForm,
ReusableMediumUpdateForm, SalesChannelForm, SSOClientForm, SSOProviderForm,
TeamForm, WebHookForm,
KnownDomainFormset, MailSettingsForm, MembershipTypeForm,
MembershipUpdateForm, OrganizerDeleteForm, OrganizerFooterLinkFormset,
OrganizerForm, OrganizerSettingsForm, OrganizerUpdateForm,
ReusableMediumCreateForm, ReusableMediumUpdateForm, SalesChannelForm,
SSOClientForm, SSOProviderForm, TeamForm, WebHookForm,
)
from pretix.control.forms.rrule import RRuleForm
from pretix.control.logdisplay import OVERVIEW_BANLIST
@@ -122,7 +122,7 @@ from pretix.control.views.mailsetup import MailSettingsSetupView
from pretix.helpers import OF_SELF, GroupConcat
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.dicts import merge_dicts
from pretix.helpers.format import format_map
from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.urls import build_absolute_uri as build_global_uri
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.forms.customer import TokenGenerator
@@ -357,9 +357,10 @@ class MailSettingsPreview(OrganizerPermissionRequiredMixin, View):
highlight=True,
)
else:
msgs[self.supported_locale[idx]] = markdown_compile_email(
format_map(v, self.placeholders(preview_item))
)
placeholders = self.placeholders(preview_item)
msgs[self.supported_locale[idx]] = format_map(markdown_compile_email(
format_map(v, placeholders)
), placeholders, mode=SafeFormatter.MODE_RICH_TO_HTML)
return JsonResponse({
'item': preview_item,
@@ -447,6 +448,10 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
def get_object(self, queryset=None) -> Organizer:
return self.object
@cached_property
def domain_config(self):
return self.request.user.has_active_staff_session(self.request.session.session_key)
@cached_property
def sform(self):
return OrganizerSettingsForm(
@@ -461,6 +466,8 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
context = super().get_context_data(*args, **kwargs)
context['sform'] = self.sform
context['footer_links_formset'] = self.footer_links_formset
if self.domain_config:
context['domain_formset'] = self.domain_formset
return context
@transaction.atomic
@@ -483,6 +490,8 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
self.request.organizer.log_action('pretix.organizer.footerlinks.changed', user=self.request.user, data={
'data': self.footer_links_formset.cleaned_data
})
if self.domain_config and self.domain_formset.has_changed():
self._save_domain_config()
if form.has_changed():
self.request.organizer.log_action(
'pretix.organizer.changed',
@@ -493,10 +502,22 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form)
def _save_domain_config(self):
for form in self.domain_formset.initial_forms:
if form.instance.pk and form.has_changed():
self.object.domains.get(pk=form.instance.pk).log_delete(self.request.user)
self.domain_formset.save()
for new_obj in self.domain_formset.new_objects:
new_obj.log_create(self.request.user)
for ch_obj, form in self.domain_formset.changed_objects:
ch_obj.log_create(self.request.user)
self.request.organizer.cache.clear()
for ev in self.request.organizer.events.all():
ev.cache.clear()
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
if self.request.user.has_active_staff_session(self.request.session.session_key):
kwargs['domain'] = True
kwargs['change_slug'] = True
return kwargs
@@ -508,7 +529,7 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form = self.get_form()
if form.is_valid() and self.sform.is_valid() and self.footer_links_formset.is_valid():
if form.is_valid() and self.sform.is_valid() and self.footer_links_formset.is_valid() and (not self.domain_config or self.domain_formset.is_valid()):
return self.form_valid(form)
else:
return self.form_invalid(form)
@@ -519,6 +540,11 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
organizer=self.object,
prefix="footer-links", instance=self.object)
@cached_property
def domain_formset(self):
return KnownDomainFormset(self.request.POST if self.request.method == "POST" else None, prefix="domains",
instance=self.object, organizer=self.object)
def save_footer_links_formset(self, obj):
self.footer_links_formset.save()

View File

@@ -50,7 +50,7 @@ from django.http import (
from django.shortcuts import redirect, render
from django.urls import resolve, reverse
from django.utils.functional import cached_property
from django.utils.html import escape, format_html
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@@ -59,12 +59,12 @@ from django.views.generic import (
)
from django_scopes import scopes_disabled
from pretix.base.email import get_available_placeholders
from pretix.base.models import (
CartPosition, LogEntry, Voucher, WaitingListEntry,
)
from pretix.base.models.vouchers import generate_codes
from pretix.base.services.mail import prefix_subject
from pretix.base.services.placeholders import get_sample_context
from pretix.base.services.vouchers import vouchers_send
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.base.views.tasks import AsyncFormView
@@ -74,7 +74,7 @@ from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import voucher_form_class
from pretix.control.views import PaginationMixin
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.format import format_map
from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.models import modelcopy
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -549,22 +549,10 @@ class VoucherBulkMailPreview(EventPermissionRequiredMixin, View):
# get all supported placeholders with dummy values
def placeholders(self, item):
ctx = {}
base_ctx = ['event', 'name']
if item == 'send_message':
base_ctx += ['voucher_list']
for p in get_available_placeholders(self.request.event, base_ctx).values():
s = str(p.render_sample(self.request.event))
if s.strip().startswith('* ') or s.startswith(' '):
ctx[p.identifier] = '<div class="placeholder" title="{}">{}</div>'.format(
_('This value will be replaced based on dynamic parameters.'),
markdown_compile_email(s)
)
else:
ctx[p.identifier] = '<span class="placeholder" title="{}">{}</span>'.format(
_('This value will be replaced based on dynamic parameters.'),
escape(s)
)
ctx = get_sample_context(self.request.event, base_ctx)
return self.SafeDict(ctx)
def post(self, request, *args, **kwargs):
@@ -579,9 +567,10 @@ class VoucherBulkMailPreview(EventPermissionRequiredMixin, View):
highlight=True
)
else:
msgs["all"] = markdown_compile_email(
format_map(request.POST.get(preview_item), self.placeholders(preview_item))
)
placeholders = self.placeholders(preview_item)
msgs["all"] = format_map(markdown_compile_email(
format_map(request.POST.get(preview_item), placeholders)
), placeholders, mode=SafeFormatter.MODE_RICH_TO_HTML)
return JsonResponse({
'item': preview_item,

View File

@@ -25,14 +25,29 @@ from string import Formatter
logger = logging.getLogger(__name__)
class PlainHtmlAlternativeString:
def __init__(self, plain, html, is_block=False):
self.plain = plain
self.html = html
self.is_block = is_block
def __repr__(self):
return f"PlainHtmlAlternativeString('{self.plain}', '{self.html}')"
class SafeFormatter(Formatter):
"""
Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and
(b) does not allow any unwanted shenanigans like attribute access or format specifiers.
"""
def __init__(self, context, raise_on_missing=False):
MODE_IGNORE_RICH = 0
MODE_RICH_TO_PLAIN = 1
MODE_RICH_TO_HTML = 2
def __init__(self, context, raise_on_missing=False, mode=MODE_IGNORE_RICH):
self.context = context
self.raise_on_missing = raise_on_missing
self.mode = mode
def get_field(self, field_name, args, kwargs):
return self.get_value(field_name, args, kwargs), field_name
@@ -40,14 +55,22 @@ class SafeFormatter(Formatter):
def get_value(self, key, args, kwargs):
if not self.raise_on_missing and key not in self.context:
return '{' + str(key) + '}'
return self.context[key]
r = self.context[key]
if isinstance(r, PlainHtmlAlternativeString):
if self.mode == self.MODE_IGNORE_RICH:
return '{' + str(key) + '}'
elif self.mode == self.MODE_RICH_TO_PLAIN:
return r.plain
elif self.mode == self.MODE_RICH_TO_HTML:
return r.html
return r
def format_field(self, value, format_spec):
# Ignore format _spec
# Ignore format_spec
return super().format_field(value, '')
def format_map(template, context, raise_on_missing=False):
def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_IGNORE_RICH):
if not isinstance(template, str):
template = str(template)
return SafeFormatter(context, raise_on_missing).format(template)
return SafeFormatter(context, raise_on_missing, mode=mode).format(template)

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-19 15:34+0000\n"
"PO-Revision-Date: 2024-11-27 03:00+0000\n"
"PO-Revision-Date: 2024-12-03 20:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
"es/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.8.3\n"
"X-Generator: Weblate 5.8.4\n"
#: pretix/_base_settings.py:79
msgid "English"
@@ -11397,7 +11397,7 @@ msgstr "Color de fondo de la página"
#: pretix/base/settings.py:2845
msgid "Use round edges"
msgstr "Utilice bordes redondos"
msgstr "Utilizar bordes redondos"
#: pretix/base/settings.py:2854
msgid ""
@@ -11433,7 +11433,7 @@ msgstr ""
#: pretix/base/settings.py:2899 pretix/base/settings.py:2941
msgid "Use header image in its full size"
msgstr "Utilice la imagen del encabezado en su tamaño completo"
msgstr "Utilizar la imagen del encabezado en su tamaño completo"
#: pretix/base/settings.py:2900 pretix/base/settings.py:2942
msgid "We recommend to upload a picture at least 1170 pixels wide."
@@ -13067,13 +13067,12 @@ msgstr ""
#: pretix/control/forms/event.py:989 pretix/control/forms/organizer.py:534
msgid "Bcc address"
msgstr "Direcciones CCO"
msgstr "Direcciones en copia oculta"
#: pretix/control/forms/event.py:990 pretix/control/forms/organizer.py:535
msgid "All emails will be sent to this address as a Bcc copy"
msgstr ""
"Todos los correos electrónicos se enviarán a esta dirección como una copia "
"de CCO"
"Todos los correos electrónicos se enviarán a esta dirección en copia oculta"
#: pretix/control/forms/event.py:996 pretix/control/forms/organizer.py:541
msgid "Signature"
@@ -13650,7 +13649,7 @@ msgstr "Fecha de inicio"
#: pretix/control/forms/filter.py:1710 pretix/control/forms/filter.py:1713
#: pretix/control/forms/filter.py:2344
msgid "Date until"
msgstr "Fecha límite"
msgstr "Fecha final"
#: pretix/control/forms/filter.py:1218
msgid "Start time from"
@@ -16020,11 +16019,11 @@ msgstr ""
#: pretix/control/logdisplay.py:422
msgid "A custom email has been sent."
msgstr "Un e-mail personalizado ha sido enviado."
msgstr "Un email personalizado ha sido enviado."
#: pretix/control/logdisplay.py:423
msgid "A custom email has been sent to an attendee."
msgstr "Un e-mail personalizado ha sido enviado al participante."
msgstr "Un email personalizado ha sido enviado al participante."
#: pretix/control/logdisplay.py:424
msgid ""
@@ -16472,7 +16471,7 @@ msgstr "Un plugin ha sido desactivado."
#: pretix/control/logdisplay.py:533
msgid "The shop has been taken live."
msgstr "La tienda ha sido tomada en vivo."
msgstr "La tienda ha sido puesta en marcha."
#: pretix/control/logdisplay.py:534
msgid "The shop has been taken offline."
@@ -16951,11 +16950,11 @@ msgstr "Parametrizaciones globales"
#: pretix/control/navigation.py:440
msgid "Update check"
msgstr "Verificación de actualización"
msgstr "Comprobación de actualizaciones"
#: pretix/control/navigation.py:445
msgid "License check"
msgstr "Revisa de licencia"
msgstr "Verificación de la licencia"
#: pretix/control/navigation.py:450
msgid "System report"
@@ -17952,7 +17951,7 @@ msgstr "Regla de check-in personalizada"
#: pretix/control/templates/pretixcontrol/vouchers/bulk.html:117
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/send_form.html:84
msgid "Edit"
msgstr "Tratar"
msgstr "Editar"
#: pretix/control/templates/pretixcontrol/checkin/list_edit.html:89
msgid "Visualize"
@@ -20028,8 +20027,8 @@ msgid ""
"For more information or to obtain a paid pretix Enterprise license, contact "
"support@pretix.eu."
msgstr ""
"Para obtener más información u obtener una licencia pretix Enterprise paga, "
"comuníquese con support@pretix.eu."
"Para obtener más información u obtener una licencia pretix Enterprise de "
"pago, comuníquese con support@pretix.eu."
#: pretix/control/templates/pretixcontrol/global_license.html:26
msgid "License settings and check"
@@ -20071,10 +20070,9 @@ msgid ""
"pretix support when your license renews. It may also be requested by pretix "
"support to aid debugging of problems."
msgstr ""
"Si tienes una licencia de pretix de Enterprise, este informe se debe "
"entregar al equipo del servicio al cliente de pretix cuando tu licencia "
"renueve. También el servicio al cliente de pretix podría pedirlo para ayudar "
"durante depurar."
"Si dispone de una licencia de pretix Enterprise, deberá enviar este informe "
"al servicio de asistencia pretix cuando renueve su licencia. También puede "
"ser solicitado por el soporte pretix para ayudar a la solución de problemas."
#: pretix/control/templates/pretixcontrol/global_sysreport.html:8
msgid ""
@@ -21288,11 +21286,11 @@ msgstr "Revocar"
#: pretix/control/templates/pretixcontrol/oauth/authorized.html:6
#: pretix/control/templates/pretixcontrol/user/settings.html:61
msgid "Authorized applications"
msgstr "Solicitudes autorizadas"
msgstr "Aplicaciones autorizadas"
#: pretix/control/templates/pretixcontrol/oauth/authorized.html:9
msgid "Manage your own apps"
msgstr "Gestiona tus propias aplicaciones"
msgstr "Gestione sus propias aplicaciones"
#: pretix/control/templates/pretixcontrol/oauth/authorized.html:18
msgid "Permissions"
@@ -31500,7 +31498,7 @@ msgstr "Lo sentimos, se ha producido un error en el proceso de pago."
#: pretix/plugins/ticketoutputpdf/apps.py:44
#: pretix/plugins/ticketoutputpdf/apps.py:47
msgid "PDF ticket output"
msgstr "Salida de entradas de PDF"
msgstr "Salida de entradas en PDF"
#: pretix/plugins/ticketoutputpdf/apps.py:52
msgid ""
@@ -31603,7 +31601,7 @@ msgstr ""
#: pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/form.html:14
msgid "Open Layout Designer"
msgstr "Abrir diseñador de diseño"
msgstr "Abrir herramienta de diseño"
#: pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/form.html:18
msgid "Advanced mode (multiple layouts)"

View File

@@ -4,7 +4,7 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-19 15:34+0000\n"
"PO-Revision-Date: 2024-11-27 03:00+0000\n"
"PO-Revision-Date: 2024-12-03 20:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
">\n"
@@ -13,7 +13,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 5.8.3\n"
"X-Generator: Weblate 5.8.4\n"
#: pretix/_base_settings.py:79
msgid "English"
@@ -8954,7 +8954,7 @@ msgstr "Nombre maximum d'articles par commande"
#: pretix/base/settings.py:316
msgid "Add-on products will not be counted."
msgstr "Les Add-Ons ne seront pas pris en compte."
msgstr "Les Add-Ons e sont pas comptabilisés."
#: pretix/base/settings.py:325
msgid ""
@@ -9966,7 +9966,7 @@ msgstr ""
#: pretix/base/settings.py:1520
msgid "Allow users to download tickets"
msgstr "Autoriser les utilisateurs à télécharger des billets"
msgstr "Autoriser les utilisateurs à télécharger les billets"
#: pretix/base/settings.py:1521
msgid "If this is off, nobody can download a ticket."
@@ -18254,7 +18254,7 @@ msgstr "Voir tous les événements récents"
#: pretix/control/templates/pretixcontrol/dashboard.html:65
msgid "Your event series"
msgstr "Votre série d'événements"
msgstr "Vos séries d'événements"
#: pretix/control/templates/pretixcontrol/dashboard.html:81
msgid "View all event series"
@@ -20630,17 +20630,16 @@ msgid ""
"product. You can also specify the minimum and maximum number of add-ons of "
"the given category that can or need to be chosen."
msgstr ""
"Avec les modules complémentaires, vous pouvez spécifier des produits qui "
"peuvent être achetés en complément de ce produit. Par exemple, si vous "
"organisez une conférence avec un ticket de conférence de base et un certain "
"nombre dateliers, vous pouvez définir les ateliers comme des modules "
"complémentaires au ticket de conférence. Avec cette configuration, les "
"ateliers ne peuvent pas être achetés seuls, mais uniquement en combinaison "
"avec un billet de conférence. Vous pouvez spécifier ici des catégories de "
"produits qui peuvent être utilisés comme modules complémentaires à ce "
"produit. Vous pouvez également spécifier le nombre minimal et maximal de "
"modules complémentaires de la catégorie donnée qui peuvent ou doivent être "
"choisis."
"Avec les add-ons, vous pouvez spécifier des produits qui peuvent être "
"achetés en complément de ce produit. Par exemple, si vous organisez une "
"conférence avec un ticket de conférence de base et un certain nombre d"
"ateliers, vous pouvez définir les ateliers comme des modules complémentaires "
"au ticket de conférence. Avec cette configuration, les ateliers ne peuvent "
"pas être achetés seuls, mais uniquement en combinaison avec un billet de "
"conférence. Vous pouvez spécifier ici des catégories de produits qui peuvent "
"être utilisés comme modules complémentaires à ce produit. Vous pouvez "
"également spécifier le nombre minimal et maximal de modules complémentaires "
"de la catégorie donnée qui peuvent ou doivent être choisis."
#: pretix/control/templates/pretixcontrol/item/include_addons.html:28
#: pretix/control/templates/pretixcontrol/item/include_addons.html:62
@@ -31762,7 +31761,7 @@ msgstr "Désolé, une erreur sest produite dans le processus de paiement."
#: pretix/plugins/ticketoutputpdf/apps.py:44
#: pretix/plugins/ticketoutputpdf/apps.py:47
msgid "PDF ticket output"
msgstr "Sortie du ticket PDF"
msgstr "Génération de billets au format PDF"
#: pretix/plugins/ticketoutputpdf/apps.py:52
msgid ""

View File

@@ -7,7 +7,7 @@ msgstr ""
"Project-Id-Version: French\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-08 13:45+0000\n"
"PO-Revision-Date: 2024-11-16 05:00+0000\n"
"PO-Revision-Date: 2024-12-03 20:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
"fr/>\n"
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 5.8.3\n"
"X-Generator: Weblate 5.8.4\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -150,7 +150,7 @@ msgstr "Méthode de paiement non disponible"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Placed orders"
msgstr "Commandes placées"
msgstr "Commandes réalisées"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-19 15:34+0000\n"
"PO-Revision-Date: 2024-11-27 03:00+0000\n"
"PO-Revision-Date: 2024-12-02 06:00+0000\n"
"Last-Translator: Patrick Chilton <chpatrick@gmail.com>\n"
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix/"
"hu/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.8.3\n"
"X-Generator: Weblate 5.8.4\n"
#: pretix/_base_settings.py:79
msgid "English"
@@ -2796,12 +2796,9 @@ msgid "Reusable media"
msgstr ""
#: pretix/base/exporters/reusablemedia.py:35
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgctxt "export_category"
msgid "Reusable media"
msgstr "Nincs dátum kiválasztva."
msgstr ""
#: pretix/base/exporters/reusablemedia.py:36
msgid ""
@@ -3572,11 +3569,8 @@ msgid "Invalid option selected."
msgstr ""
#: pretix/base/modelimport_orders.py:658 pretix/base/modelimport_orders.py:666
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgid "Ambiguous option selected."
msgstr "Nincs dátum kiválasztva."
msgstr ""
#: pretix/base/modelimport_orders.py:697 pretix/base/models/orders.py:238
#: pretix/control/forms/orders.py:686 pretix/control/forms/organizer.py:795
@@ -8262,11 +8256,8 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:190
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgid "Activate re-usable media"
msgstr "Nincs dátum kiválasztva."
msgstr ""
#: pretix/base/settings.py:191
msgid ""
@@ -12857,11 +12848,8 @@ msgid "Device status"
msgstr ""
#: pretix/control/forms/filter.py:2621
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgid "Active devices"
msgstr "Nincs dátum kiválasztva."
msgstr ""
#: pretix/control/forms/filter.py:2622
msgid "Revoked devices"
@@ -13891,11 +13879,8 @@ msgid "Organizer short name"
msgstr "Szervező rövidített név"
#: pretix/control/forms/organizer.py:1092
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgid "Allow access to reusable media"
msgstr "Nincs dátum kiválasztva."
msgstr ""
#: pretix/control/forms/organizer.py:1093
msgid ""
@@ -20970,11 +20955,8 @@ msgid ""
msgstr ""
#: pretix/control/templates/pretixcontrol/orders/index.html:291
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgid "Select action"
msgstr "Nincs dátum kiválasztva."
msgstr ""
#: pretix/control/templates/pretixcontrol/orders/index.html:312
#: pretix/control/views/orders.py:335
@@ -21401,11 +21383,8 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html:188
#: pretix/control/templates/pretixcontrol/subevents/index.html:211
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgid "Edit selected"
msgstr "Nincs dátum kiválasztva."
msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/edit.html:12
msgid "Organizer settings"
@@ -22746,18 +22725,12 @@ msgid "Delete selected"
msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/index.html:214
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgid "Activate selected"
msgstr "Nincs dátum kiválasztva."
msgstr "Aktiválás"
#: pretix/control/templates/pretixcontrol/subevents/index.html:217
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgid "Deactivate selected"
msgstr "Nincs dátum kiválasztva."
msgstr "Deaktiválás"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:4
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:6
@@ -25706,11 +25679,8 @@ msgid "Badge layout: %(name)s"
msgstr ""
#: pretix/plugins/badges/templates/pretixplugins/badges/edit.html:26
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgid "Save & continue"
msgstr "Nincs dátum kiválasztva."
msgstr "Mentés és folytatás"
#: pretix/plugins/badges/templates/pretixplugins/badges/index.html:10
msgid "You haven't created any badge layouts yet."
@@ -28628,7 +28598,7 @@ msgstr ""
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_confirm.html:4
msgid "The total amount will be withdrawn from your credit card."
msgstr ""
msgstr "Az összeg a kártyádról kerül levonásra."
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_confirm.html:8
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_card.html:29
@@ -29906,12 +29876,9 @@ msgstr "%(item)s, %(var)s hozzáadása a kosárhoz"
#: pretix/presale/templates/pretixpresale/event/fragment_product_list.html:370
#: pretix/presale/templates/pretixpresale/event/voucher.html:231
#: pretix/presale/templates/pretixpresale/event/voucher.html:385
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgctxt "checkbox"
msgid "Select"
msgstr "Nincs dátum kiválasztva."
msgstr "Kiválaszt"
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:205
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:350
@@ -31427,11 +31394,8 @@ msgid "Social features"
msgstr ""
#: pretix/presale/templates/pretixpresale/fragment_modals.html:114
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgid "Save selection"
msgstr "Nincs dátum kiválasztva."
msgstr "Kijelölés mentése"
#: pretix/presale/templates/pretixpresale/fragment_week_calendar.html:82
#, python-format
@@ -31758,7 +31722,7 @@ msgstr "Ismeretlen eseménykód vagy nincs jogod az eseményhez."
#: pretix/presale/views/event.py:902
msgctxt "subevent"
msgid "No date selected."
msgstr "Nincs dátum kiválasztva."
msgstr "Nincs időpont kiválasztva."
#: pretix/presale/views/event.py:905
msgctxt "subevent"

View File

@@ -8,16 +8,16 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-08 13:45+0000\n"
"PO-Revision-Date: 2024-10-01 22:52+0000\n"
"PO-Revision-Date: 2024-11-28 06:00+0000\n"
"Last-Translator: Patrick Chilton <chpatrick@gmail.com>\n"
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix-"
"js/hu/>\n"
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/"
"pretix-js/hu/>\n"
"Language: hu\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.7.2\n"
"X-Generator: Weblate 5.8.3\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -779,7 +779,7 @@ msgstr "A kosár lejárt"
#: pretix/static/pretixpresale/js/ui/main.js:588
#: pretix/static/pretixpresale/js/ui/main.js:607
msgid "Time zone:"
msgstr ""
msgstr "Időzona:"
#: pretix/static/pretixpresale/js/ui/main.js:598
msgid "Your local time:"

View File

@@ -80,28 +80,32 @@ class MultiDomainMiddleware(MiddlewareMixin):
request.port = int(port) if port else None
request.host = domain
if domain == default_domain:
request.domain_mode = "system"
request.urlconf = "pretix.multidomain.maindomain_urlconf"
elif domain:
cached = cache.get('pretix_multidomain_instance_{}'.format(domain))
cached = cache.get('pretix_multidomain_instances_{}'.format(domain))
if cached is None:
try:
kd = KnownDomain.objects.select_related('organizer', 'event').get(domainname=domain) # noqa
orga = kd.organizer
event = kd.event
mode = kd.mode
except KnownDomain.DoesNotExist:
orga = False
event = False
mode = "system"
cache.set(
'pretix_multidomain_instance_{}'.format(domain),
(orga.pk if orga else None, event.pk if event else None),
'pretix_multidomain_instances_{}'.format(domain),
(orga.pk if orga else None, event.pk if event else None, mode),
3600
)
else:
orga, event = cached
orga, event, mode = cached
if event:
if mode == KnownDomain.MODE_EVENT_DOMAIN:
request.event_domain = True
request.domain_mode = KnownDomain.MODE_EVENT_DOMAIN
if isinstance(event, Event):
request.organizer = orga
request.event = event
@@ -110,11 +114,18 @@ class MultiDomainMiddleware(MiddlewareMixin):
request.event = Event.objects.select_related('organizer').get(pk=event)
request.organizer = request.event.organizer
request.urlconf = "pretix.multidomain.event_domain_urlconf"
elif orga:
elif mode == KnownDomain.MODE_ORG_ALT_DOMAIN:
request.organizer_domain = True
request.domain_mode = KnownDomain.MODE_ORG_ALT_DOMAIN
request.organizer = orga if isinstance(orga, Organizer) else Organizer.objects.get(pk=orga)
request.urlconf = "pretix.multidomain.organizer_alternative_domain_urlconf"
elif mode == KnownDomain.MODE_ORG_DOMAIN:
request.organizer_domain = True
request.domain_mode = KnownDomain.MODE_ORG_DOMAIN
request.organizer = orga if isinstance(orga, Organizer) else Organizer.objects.get(pk=orga)
request.urlconf = "pretix.multidomain.organizer_domain_urlconf"
elif settings.DEBUG or domain in LOCAL_HOST_NAMES:
request.domain_mode = "system"
request.urlconf = "pretix.multidomain.maindomain_urlconf"
else:
with scopes_disabled():

View File

@@ -0,0 +1,71 @@
# Generated by Django 4.2.16 on 2024-11-12 10:46
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0273_remove_checkinlist_auto_checkin_sales_channels"),
("pretixmultidomain", "0002_knowndomain_event"),
]
operations = [
migrations.CreateModel(
name="AlternativeDomainAssignment",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
],
),
migrations.AddField(
model_name="knowndomain",
name="mode",
field=models.CharField(default="organizer", max_length=255),
),
migrations.AlterField(
model_name="knowndomain",
name="event",
field=models.OneToOneField(
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="domain",
to="pretixbase.event",
),
),
migrations.RunSQL(
sql="UPDATE pretixmultidomain_knowndomain SET mode = 'event' WHERE event_id IS NOT NULL",
reverse_sql=migrations.RunSQL.noop,
),
migrations.AddConstraint(
model_name="knowndomain",
constraint=models.UniqueConstraint(
condition=models.Q(("mode", "organizer")),
fields=("organizer",),
name="unique_organizer_domain",
),
),
migrations.AddField(
model_name="alternativedomainassignment",
name="domain",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="event_assignments",
to="pretixmultidomain.knowndomain",
),
),
migrations.AddField(
model_name="alternativedomainassignment",
name="event",
field=models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="alternative_domain_assignment",
to="pretixbase.event",
),
),
]

View File

@@ -21,6 +21,7 @@
#
from django.core.cache import cache
from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from django_scopes import scopes_disabled
@@ -28,39 +29,134 @@ from pretix.base.models import Event, Organizer
class KnownDomain(models.Model):
domainname = models.CharField(max_length=255, primary_key=True)
organizer = models.ForeignKey(Organizer, blank=True, null=True, related_name='domains', on_delete=models.CASCADE)
event = models.ForeignKey(Event, blank=True, null=True, related_name='domains', on_delete=models.PROTECT)
MODE_ORG_DOMAIN = "organizer"
MODE_ORG_ALT_DOMAIN = "organizer_alternative"
MODE_EVENT_DOMAIN = "event"
MODES = (
(MODE_ORG_DOMAIN, _("Organizer domain")),
(MODE_ORG_ALT_DOMAIN, _("Alternative organizer domain for a set of events")),
(MODE_EVENT_DOMAIN, _("Event domain")),
)
domainname = models.CharField(
max_length=255,
primary_key=True,
verbose_name=_("Domain name"),
)
mode = models.CharField(
max_length=255,
choices=MODES,
default=MODE_ORG_DOMAIN,
verbose_name=_("Mode"),
)
organizer = models.ForeignKey(
Organizer,
blank=True,
null=True,
related_name='domains',
on_delete=models.CASCADE
)
event = models.OneToOneField(
Event,
blank=True,
null=True,
related_name='domain',
on_delete=models.PROTECT,
verbose_name=_("Event"),
)
class Meta:
verbose_name = _("Known domain")
verbose_name_plural = _("Known domains")
constraints = [
models.UniqueConstraint(
fields=("organizer",),
name="unique_organizer_domain",
condition=Q(mode="organizer"),
),
]
ordering = ("-mode", "domainname")
def __str__(self):
return self.domainname
@scopes_disabled()
def save(self, *args, **kwargs):
if self.event:
self.mode = KnownDomain.MODE_EVENT_DOMAIN
elif self.mode == KnownDomain.MODE_EVENT_DOMAIN:
raise ValueError("Event domain needs event")
super().save(*args, **kwargs)
if self.event:
self.event.get_cache().clear()
try:
self.event.alternative_domain_assignment.delete()
except AlternativeDomainAssignment.DoesNotExist:
pass
elif self.organizer:
self.organizer.get_cache().clear()
for event in self.organizer.events.all():
event.get_cache().clear()
cache.delete('pretix_multidomain_organizer_{}'.format(self.domainname))
cache.delete('pretix_multidomain_instance_{}'.format(self.domainname))
cache.delete('pretix_multidomain_instances_{}'.format(self.domainname))
cache.delete('pretix_multidomain_event_{}'.format(self.domainname))
@scopes_disabled()
def delete(self, *args, **kwargs):
if self.event:
self.event.get_cache().clear()
self.event.cache.clear()
elif self.organizer:
self.organizer.get_cache().clear()
self.organizer.cache.clear()
for event in self.organizer.events.all():
event.get_cache().clear()
event.cache.clear()
cache.delete('pretix_multidomain_organizer_{}'.format(self.domainname))
cache.delete('pretix_multidomain_instance_{}'.format(self.domainname))
cache.delete('pretix_multidomain_instances_{}'.format(self.domainname))
cache.delete('pretix_multidomain_event_{}'.format(self.domainname))
super().delete(*args, **kwargs)
def _log_domain_action(self, user, data):
if self.event:
self.event.log_action(
'pretix.event.settings',
user=user,
data=data
)
else:
self.organizer.log_action(
'pretix.organizer.settings',
user=user,
data=data
)
def log_create(self, user):
self._log_domain_action(user, {'add_alt_domain': self.domainname} if self.mode == KnownDomain.MODE_ORG_ALT_DOMAIN else {'domain': self.domainname})
def log_delete(self, user):
self._log_domain_action(user, {'remove_alt_domain': self.domainname} if self.mode == KnownDomain.MODE_ORG_ALT_DOMAIN else {'domain': None})
class AlternativeDomainAssignment(models.Model):
domain = models.ForeignKey(
KnownDomain,
on_delete=models.CASCADE,
related_name="event_assignments",
)
event = models.OneToOneField(
Event,
related_name="alternative_domain_assignment",
on_delete=models.CASCADE,
)
@scopes_disabled()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self.event.cache.clear()
cache.delete('pretix_multidomain_instances_{}'.format(self.domain_id))
cache.delete('pretix_multidomain_event_{}'.format(self.domain_id))
@scopes_disabled()
def delete(self, *args, **kwargs):
self.event.cache.clear()
cache.delete('pretix_multidomain_instances_{}'.format(self.domain_id))
cache.delete('pretix_multidomain_event_{}'.format(self.domain_id))
super().delete(*args, **kwargs)

View File

@@ -0,0 +1,63 @@
#
# 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 importlib.util
from django.apps import apps
from django.urls import include, re_path
from pretix.multidomain.plugin_handler import plugin_event_urls
from pretix.presale.urls import event_patterns, locale_patterns
from pretix.presale.views import organizer
from pretix.urls import common_patterns
presale_patterns = [
re_path(r'', include((locale_patterns + [
re_path(r'^$', organizer.RedirectToOrganizerIndex.as_view(), name='organizer.alt.index'),
re_path(r'^(?P<event>[^/]+)/', include(event_patterns)),
], 'presale')))
]
raw_plugin_patterns = []
for app in apps.get_app_configs():
if hasattr(app, 'PretixPluginMeta'):
if importlib.util.find_spec(app.name + '.urls'):
urlmod = importlib.import_module(app.name + '.urls')
if hasattr(urlmod, 'event_patterns'):
patterns = plugin_event_urls(urlmod.event_patterns, plugin=app.name)
raw_plugin_patterns.append(
re_path(r'^(?P<event>[^/]+)/', include((patterns, app.label)))
)
if hasattr(urlmod, 'organizer_patterns'):
patterns = plugin_event_urls(urlmod.organizer_patterns, plugin=app.name)
raw_plugin_patterns.append(
re_path(r'', include((patterns, app.label)))
)
plugin_patterns = [
re_path(r'', include((raw_plugin_patterns, 'plugins')))
]
# The presale namespace comes last, because it contains a wildcard catch
urlpatterns = common_patterns + plugin_patterns + presale_patterns
handler404 = 'pretix.base.views.errors.page_not_found'
handler500 = 'pretix.base.views.errors.server_error'

View File

@@ -43,28 +43,33 @@ from pretix.base.models import Event, Organizer
from .models import KnownDomain
def get_event_domain(event, fallback=False, return_info=False):
def get_event_domain(event, fallback=False, return_mode=False):
assert isinstance(event, Event)
if not event.pk:
# Can happen on the "event deleted" response
return (None, None) if return_info else None
suffix = ('_fallback' if fallback else '') + ('_info' if return_info else '')
return (None, None) if return_mode else None
suffix = ('_fallback' if fallback else '') + ('_mode' if return_mode else '')
domain = getattr(event, '_cached_domain' + suffix, None) or event.cache.get('domain' + suffix)
if domain is None:
domain = None, None
if fallback:
if hasattr(event, 'alternative_domain_assignment'):
domain = event.alternative_domain_assignment.domain_id, KnownDomain.MODE_ORG_ALT_DOMAIN
elif fallback:
domains = KnownDomain.objects.filter(
Q(event=event) | Q(organizer_id=event.organizer_id, event__isnull=True)
Q(event=event, mode=KnownDomain.MODE_EVENT_DOMAIN) |
Q(organizer_id=event.organizer_id, event__isnull=True, mode=KnownDomain.MODE_ORG_DOMAIN)
)
domains_event = [d for d in domains if d.event_id == event.pk]
domains_org = [d for d in domains if not d.event_id]
if domains_event:
domain = domains_event[0].domainname, "event"
domain = domains_event[0].domainname, KnownDomain.MODE_EVENT_DOMAIN
elif domains_org:
domain = domains_org[0].domainname, "organizer"
domain = domains_org[0].domainname, KnownDomain.MODE_ORG_DOMAIN
else:
domains = event.domains.all()
domain = domains[0].domainname if domains else None, "event"
try:
domain = event.domain.domainname, KnownDomain.MODE_EVENT_DOMAIN
except KnownDomain.DoesNotExist:
domain = None, None
event.cache.set('domain' + suffix, domain or 'none')
setattr(event, '_cached_domain' + suffix, domain or 'none')
elif domain == 'none':
@@ -72,7 +77,7 @@ def get_event_domain(event, fallback=False, return_info=False):
domain = None, None
else:
setattr(event, '_cached_domain' + suffix, domain)
return domain if return_info or not isinstance(domain, tuple) else domain[0]
return domain if return_mode else domain[0]
def get_organizer_domain(organizer):
@@ -81,7 +86,7 @@ def get_organizer_domain(organizer):
return None
domain = getattr(organizer, '_cached_domain', None) or organizer.cache.get('domain')
if domain is None:
domains = organizer.domains.filter(event__isnull=True)
domains = organizer.domains.filter(event__isnull=True, mode=KnownDomain.MODE_ORG_DOMAIN)
domain = domains[0].domainname if domains else None
organizer.cache.set('domain', domain or 'none')
organizer._cached_domain = domain or 'none'
@@ -131,7 +136,8 @@ def eventreverse(obj, name, kwargs=None):
:returns: An absolute or relative URL as a string
"""
from pretix.multidomain import (
event_domain_urlconf, maindomain_urlconf, organizer_domain_urlconf,
event_domain_urlconf, maindomain_urlconf,
organizer_alternative_domain_urlconf, organizer_domain_urlconf,
)
c = None
@@ -153,17 +159,24 @@ def eventreverse(obj, name, kwargs=None):
raise TypeError('obj should be Event or Organizer')
if event:
domain, domaintype = get_event_domain(obj, fallback=True, return_info=True)
domain, domaintype = get_event_domain(obj, fallback=True, return_mode=True)
else:
domain, domaintype = get_organizer_domain(organizer), "organizer"
domain, domaintype = get_organizer_domain(organizer), KnownDomain.MODE_ORG_DOMAIN
if domain:
if domaintype == "event" and 'event' in kwargs:
if domaintype == KnownDomain.MODE_EVENT_DOMAIN and 'event' in kwargs:
del kwargs['event']
if 'organizer' in kwargs:
del kwargs['organizer']
path = reverse(name, kwargs=kwargs, urlconf=event_domain_urlconf if domaintype == "event" else organizer_domain_urlconf)
if domaintype == KnownDomain.MODE_EVENT_DOMAIN:
urlconf = event_domain_urlconf
elif domaintype == KnownDomain.MODE_ORG_ALT_DOMAIN:
urlconf = organizer_alternative_domain_urlconf
else:
urlconf = organizer_domain_urlconf
path = reverse(name, kwargs=kwargs, urlconf=urlconf)
siteurlsplit = urlsplit(settings.SITE_URL)
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
domain = '%s:%d' % (domain, siteurlsplit.port)

View File

@@ -26,13 +26,12 @@ from collections import defaultdict
from django.dispatch import receiver
from django.template.loader import get_template
from django.urls import resolve, reverse
from django.utils.html import escape
from django.utils.translation import gettext_lazy as _
from pretix.base.logentrytypes import EventLogEntryType, log_entry_types
from pretix.base.models import Event, Order
from pretix.base.signals import (
event_copy_data, item_copy_data, logentry_display, logentry_object_link,
register_data_exporters,
event_copy_data, item_copy_data, register_data_exporters,
)
from pretix.control.signals import (
item_forms, nav_event, order_info, order_position_buttons,
@@ -173,35 +172,13 @@ def control_order_info(sender: Event, request, order: Order, **kwargs):
return template.render(ctx, request=request)
@receiver(signal=logentry_display, dispatch_uid="badges_logentry_display")
def badges_logentry_display(sender, logentry, **kwargs):
if not logentry.action_type.startswith('pretix.plugins.badges'):
return
plains = {
'pretix.plugins.badges.layout.added': _('Badge layout created.'),
'pretix.plugins.badges.layout.deleted': _('Badge layout deleted.'),
'pretix.plugins.badges.layout.changed': _('Badge layout changed.'),
}
if logentry.action_type in plains:
return plains[logentry.action_type]
@receiver(signal=logentry_object_link, dispatch_uid="badges_logentry_object_link")
def badges_logentry_object_link(sender, logentry, **kwargs):
if not logentry.action_type.startswith('pretix.plugins.badges.layout') or not isinstance(logentry.content_object,
BadgeLayout):
return
a_text = _('Badge layout {val}')
a_map = {
'href': reverse('plugins:badges:edit', kwargs={
'event': sender.slug,
'organizer': sender.organizer.slug,
'layout': logentry.content_object.id
}),
'val': escape(logentry.content_object.name),
}
a_map['val'] = '<a href="{href}">{val}</a>'.format_map(a_map)
return a_text.format_map(a_map)
@log_entry_types.new_from_dict({
'pretix.plugins.badges.layout.added': _('Badge layout created.'),
'pretix.plugins.badges.layout.deleted': _('Badge layout deleted.'),
'pretix.plugins.badges.layout.changed': _('Badge layout changed.'),
})
class BadgeLogEntryType(EventLogEntryType):
object_type = BadgeLayout
object_link_wrapper = _('Badge layout {val}')
object_link_viewname = 'plugins:badges:edit'
object_link_argname = 'layout'

View File

@@ -25,9 +25,12 @@ from django.urls import resolve, reverse
from django.utils.translation import gettext_lazy as _, gettext_noop
from i18nfield.strings import LazyI18nString
from pretix.base.signals import logentry_display, register_payment_providers
from pretix.base.signals import register_payment_providers
from pretix.control.signals import html_head, nav_event, nav_organizer
from ...base.logentrytypes import (
ClearDataShredderMixin, OrderLogEntryType, log_entry_types,
)
from ...base.settings import settings_hierarkey
from .payment import BankTransfer
@@ -117,13 +120,10 @@ def html_head_presale(sender, request=None, **kwargs):
return ""
@receiver(signal=logentry_display)
def pretixcontrol_logentry_display(sender, logentry, **kwargs):
plains = {
'pretix.plugins.banktransfer.order.email.invoice': _('The invoice was sent to the designated email address.'),
}
if logentry.action_type in plains:
return plains[logentry.action_type]
@log_entry_types.new()
class BanktransferOrderEmailInvoiceLogEntryType(OrderLogEntryType, ClearDataShredderMixin):
action_type = 'pretix.plugins.banktransfer.order.email.invoice'
plain = _('The invoice was sent to the designated email address.')
settings_hierarkey.add_default(

View File

@@ -22,17 +22,10 @@
from django.dispatch import receiver
from pretix.base.signals import logentry_display, register_payment_providers
from pretix.base.signals import register_payment_providers
@receiver(register_payment_providers, dispatch_uid="payment_paypal")
def register_payment_provider(sender, **kwargs):
from .payment import Paypal
return Paypal
@receiver(signal=logentry_display, dispatch_uid="paypal_logentry_display")
def pretixcontrol_logentry_display(sender, logentry, **kwargs):
from pretix.plugins.paypal2.signals import pretixcontrol_logentry_display
return pretixcontrol_logentry_display(sender, logentry, **kwargs)

View File

@@ -19,7 +19,6 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import json
from collections import OrderedDict
from django import forms
@@ -32,10 +31,11 @@ from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.forms import SecretKeySettingsField
from pretix.base.logentrytypes import EventLogEntryType, log_entry_types
from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
from pretix.base.settings import settings_hierarkey
from pretix.base.signals import (
logentry_display, register_global_settings, register_payment_providers,
register_global_settings, register_payment_providers,
)
from pretix.plugins.paypal2.payment import PaypalMethod
from pretix.presale.signals import html_head, process_response
@@ -47,33 +47,32 @@ def register_payment_provider(sender, **kwargs):
return [PaypalSettingsHolder, PaypalWallet, PaypalAPM]
@receiver(signal=logentry_display, dispatch_uid="paypal2_logentry_display")
def pretixcontrol_logentry_display(sender, logentry, **kwargs):
if logentry.action_type != 'pretix.plugins.paypal.event':
return
@log_entry_types.new()
class PaypalEventLogEntryType(EventLogEntryType):
action_type = 'pretix.plugins.paypal.event'
data = json.loads(logentry.data)
event_type = data.get('event_type')
text = None
plains = {
'PAYMENT.SALE.COMPLETED': _('Payment completed.'),
'PAYMENT.SALE.DENIED': _('Payment denied.'),
'PAYMENT.SALE.REFUNDED': _('Payment refunded.'),
'PAYMENT.SALE.REVERSED': _('Payment reversed.'),
'PAYMENT.SALE.PENDING': _('Payment pending.'),
'CHECKOUT.ORDER.APPROVED': pgettext_lazy('paypal', 'Order approved.'),
'CHECKOUT.ORDER.COMPLETED': pgettext_lazy('paypal', 'Order completed.'),
'PAYMENT.CAPTURE.COMPLETED': pgettext_lazy('paypal', 'Capture completed.'),
'PAYMENT.CAPTURE.PENDING': pgettext_lazy('paypal', 'Capture pending.'),
}
def display(self, logentry):
event_type = logentry.parsed_data.get('event_type')
text = None
plains = {
'PAYMENT.SALE.COMPLETED': _('Payment completed.'),
'PAYMENT.SALE.DENIED': _('Payment denied.'),
'PAYMENT.SALE.REFUNDED': _('Payment refunded.'),
'PAYMENT.SALE.REVERSED': _('Payment reversed.'),
'PAYMENT.SALE.PENDING': _('Payment pending.'),
'CHECKOUT.ORDER.APPROVED': pgettext_lazy('paypal', 'Order approved.'),
'CHECKOUT.ORDER.COMPLETED': pgettext_lazy('paypal', 'Order completed.'),
'PAYMENT.CAPTURE.COMPLETED': pgettext_lazy('paypal', 'Capture completed.'),
'PAYMENT.CAPTURE.PENDING': pgettext_lazy('paypal', 'Capture pending.'),
}
if event_type in plains:
text = plains[event_type]
else:
text = event_type
if event_type in plains:
text = plains[event_type]
else:
text = event_type
if text:
return _('PayPal reported an event: {}').format(text)
if text:
return _('PayPal reported an event: {}').format(text)
@receiver(register_global_settings, dispatch_uid='paypal2_global_settings')

View File

@@ -79,8 +79,8 @@ class BaseMailForm(FormPlaceholderMixin, forms.Form):
widget=I18nMarkdownTextarea, required=True,
locales=event.settings.get('locales'),
)
self._set_field_placeholders('subject', context_parameters)
self._set_field_placeholders('message', context_parameters)
self._set_field_placeholders('subject', context_parameters, rich=False)
self._set_field_placeholders('message', context_parameters, rich=True)
class WaitinglistMailForm(BaseMailForm):
@@ -382,7 +382,7 @@ class RuleForm(FormPlaceholderMixin, I18nModelForm):
)
self._set_field_placeholders('subject', ['event', 'order', 'event_or_subevent'])
self._set_field_placeholders('template', ['event', 'order', 'event_or_subevent'])
self._set_field_placeholders('template', ['event', 'order', 'event_or_subevent'], rich=True)
choices = [(e, l) for e, l in Order.STATUS_CHOICE if e != 'n']
choices.insert(0, ('n__valid_if_pending', _('payment pending but already confirmed')))

View File

@@ -46,13 +46,16 @@ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_scopes import scope, scopes_disabled
from pretix.base.logentrytypes import (
EventLogEntryType, OrderLogEntryType, log_entry_types,
)
from pretix.base.models import SubEvent
from pretix.base.signals import (
EventPluginSignal, event_copy_data, logentry_display, periodic_task,
EventPluginSignal, event_copy_data, periodic_task,
)
from pretix.control.signals import nav_event
from pretix.helpers import OF_SELF
from pretix.plugins.sendmail.models import ScheduledMail
from pretix.plugins.sendmail.models import Rule, ScheduledMail
from pretix.plugins.sendmail.views import OrderSendView, WaitinglistSendView
logger = logging.getLogger(__name__)
@@ -115,21 +118,28 @@ def control_nav_import(sender, request=None, **kwargs):
]
@receiver(signal=logentry_display)
def pretixcontrol_logentry_display(sender, logentry, **kwargs):
plains = {
'pretix.plugins.sendmail.sent': _('Mass email was sent to customers or attendees.'),
'pretix.plugins.sendmail.sent.waitinglist': _('Mass email was sent to waiting list entries.'),
'pretix.plugins.sendmail.order.email.sent': _('The order received a mass email.'),
'pretix.plugins.sendmail.order.email.sent.attendee': _('A ticket holder of this order received a mass email.'),
'pretix.plugins.sendmail.rule.added': _('An email rule was created'),
'pretix.plugins.sendmail.rule.changed': _('An email rule was updated'),
'pretix.plugins.sendmail.rule.order.email.sent': _('A scheduled email was sent to the order'),
'pretix.plugins.sendmail.rule.order.position.email.sent': _('A scheduled email was sent to a ticket holder'),
'pretix.plugins.sendmail.rule.deleted': _('An email rule was deleted'),
}
if logentry.action_type in plains:
return plains[logentry.action_type]
@log_entry_types.new('pretix.plugins.sendmail.sent', _('Mass email was sent to customers or attendees.'))
@log_entry_types.new('pretix.plugins.sendmail.sent.waitinglist', _('Mass email was sent to waiting list entries.'))
class SendmailPluginLogEntryType(EventLogEntryType):
pass
@log_entry_types.new('pretix.plugins.sendmail.order.email.sent', _('The order received a mass email.'))
@log_entry_types.new('pretix.plugins.sendmail.order.email.sent.attendee', _('A ticket holder of this order received a mass email.'))
class SendmailPluginOrderLogEntryType(OrderLogEntryType):
pass
@log_entry_types.new('pretix.plugins.sendmail.rule.added', _('An email rule was created'))
@log_entry_types.new('pretix.plugins.sendmail.rule.changed', _('An email rule was updated'))
@log_entry_types.new('pretix.plugins.sendmail.rule.order.email.sent', _('A scheduled email was sent to the order'))
@log_entry_types.new('pretix.plugins.sendmail.rule.order.position.email.sent', _('A scheduled email was sent to a ticket holder'))
@log_entry_types.new('pretix.plugins.sendmail.rule.deleted', _('An email rule was deleted'))
class SendmailPluginRuleLogEntryType(EventLogEntryType):
object_type = Rule
object_link_wrapper = _('Mail rule {val}')
object_link_viewname = 'plugins:sendmail:rule.update'
object_link_argname = 'rule'
@receiver(periodic_task)

View File

@@ -46,12 +46,10 @@ from django.shortcuts import get_object_or_404, redirect
from django.template.loader import get_template
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.html import escape
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, ngettext
from django.views.generic import DeleteView, FormView, ListView, TemplateView
from pretix.base.email import get_available_placeholders
from pretix.base.i18n import LazyI18nString, language
from pretix.base.models import Checkin, LogEntry, Order, OrderPosition
from pretix.base.models.event import SubEvent
@@ -63,7 +61,8 @@ from pretix.plugins.sendmail.tasks import (
)
from ...base.services.mail import prefix_subject
from ...helpers.format import format_map
from ...base.services.placeholders import get_sample_context
from ...helpers.format import SafeFormatter, format_map
from ...helpers.models import modelcopy
from . import forms
from .models import Rule, ScheduledMail
@@ -191,17 +190,15 @@ class BaseSenderView(EventPermissionRequiredMixin, FormView):
if self.request.POST.get("action") != "send":
for l in self.request.event.settings.locales:
with language(l, self.request.event.settings.region):
context_dict = {}
for k, v in get_available_placeholders(self.request.event, self.context_parameters).items():
context_dict[k] = '<span class="placeholder" title="{}">{}</span>'.format(
_('This value will be replaced based on dynamic parameters.'),
escape(v.render_sample(self.request.event))
)
context_dict = get_sample_context(self.request.event, self.context_parameters)
subject = bleach.clean(form.cleaned_data['subject'].localize(l), tags=set())
preview_subject = prefix_subject(self.request.event, format_map(subject, context_dict), highlight=True)
message = form.cleaned_data['message'].localize(l)
preview_text = markdown_compile_email(format_map(message, context_dict))
preview_text = format_map(
markdown_compile_email(format_map(message, context_dict)),
context_dict,
mode=SafeFormatter.MODE_RICH_TO_HTML,
)
self.output[l] = {
'subject': _('Subject: {subject}').format(subject=preview_subject),
@@ -603,31 +600,6 @@ class CreateRule(EventPermissionRequiredMixin, CreateView):
return super().form_invalid(form)
def form_valid(self, form):
self.output = {}
if self.request.POST.get("action") == "preview":
for l in self.request.event.settings.locales:
with language(l, self.request.event.settings.region):
context_dict = {}
for k, v in get_available_placeholders(self.request.event, ['event', 'order',
'position_or_address']).items():
context_dict[k] = '<span class="placeholder" title="{}">{}</span>'.format(
_('This value will be replaced based on dynamic parameters.'),
escape(v.render_sample(self.request.event))
)
subject = bleach.clean(form.cleaned_data['subject'].localize(l), tags=set())
preview_subject = prefix_subject(self.request.event, format_map(subject, context_dict), highlight=True)
template = form.cleaned_data['template'].localize(l)
preview_text = markdown_compile_email(format_map(template, context_dict))
self.output[l] = {
'subject': _('Subject: {subject}').format(subject=preview_subject),
'html': preview_text,
}
return self.get(self.request, *self.args, **self.kwargs)
messages.success(self.request, _('Your rule has been created.'))
form.instance.event = self.request.event
@@ -685,17 +657,15 @@ class UpdateRule(EventPermissionRequiredMixin, UpdateView):
for lang in self.request.event.settings.locales:
with language(lang, self.request.event.settings.region):
placeholders = {}
for k, v in get_available_placeholders(self.request.event, ['event', 'order', 'position_or_address']).items():
placeholders[k] = '<span class="placeholder" title="{}">{}</span>'.format(
_('This value will be replaced based on dynamic parameters.'),
escape(v.render_sample(self.request.event))
)
placeholders = get_sample_context(self.request.event, ['event', 'order', 'position_or_address'])
subject = bleach.clean(self.object.subject.localize(lang), tags=set())
preview_subject = prefix_subject(self.request.event, format_map(subject, placeholders), highlight=True)
template = self.object.template.localize(lang)
preview_text = markdown_compile_email(format_map(template, placeholders))
preview_text = format_map(
markdown_compile_email(format_map(template, placeholders)),
placeholders,
mode=SafeFormatter.MODE_RICH_TO_HTML,
)
o[lang] = {
'subject': _('Subject: {subject}'.format(subject=preview_subject)),

View File

@@ -388,21 +388,6 @@ class StripeSettingsHolder(BasePaymentProvider):
}
),
)),
('method_sofort',
forms.BooleanField(
label=_('SOFORT'),
disabled=self.event.currency != 'EUR',
help_text=(
_('Stripe is in the process of removing this payment method. If you created your Stripe '
'account after November 2023, you cannot use this payment method.') +
'<div class="alert alert-warning">%s</div>' % _(
'Despite the name, Sofort payments via Stripe are <strong>not</strong> processed '
'instantly but might take up to <strong>14 days</strong> to be confirmed in some cases. '
'Please only activate this payment method if your payment term allows for this lag.'
)
),
required=False,
)),
('method_eps',
forms.BooleanField(
label=_('EPS'),

View File

@@ -24,10 +24,9 @@ import json
from django.dispatch import receiver
from django.template.loader import get_template
from django.urls import reverse
from django.utils.html import escape
from django.utils.translation import gettext_lazy as _
from pretix.base.logentrytypes import EventLogEntryType, log_entry_types
from pretix.base.models import Event, SalesChannel
from pretix.base.signals import ( # NOQA: legacy import
EventPluginSignal, event_copy_data, item_copy_data, layout_text_variables,
@@ -134,38 +133,16 @@ def pdf_event_copy_data_receiver(sender, other, item_map, question_map, **kwargs
return layout_map
@receiver(signal=logentry_display, dispatch_uid="pretix_ticketoutputpdf_logentry_display")
def pdf_logentry_display(sender, logentry, **kwargs):
if not logentry.action_type.startswith('pretix.plugins.ticketoutputpdf'):
return
plains = {
'pretix.plugins.ticketoutputpdf.layout.added': _('Ticket layout created.'),
'pretix.plugins.ticketoutputpdf.layout.deleted': _('Ticket layout deleted.'),
'pretix.plugins.ticketoutputpdf.layout.changed': _('Ticket layout changed.'),
}
if logentry.action_type in plains:
return plains[logentry.action_type]
@receiver(signal=logentry_object_link, dispatch_uid="pretix_ticketoutputpdf_logentry_object_link")
def pdf_logentry_object_link(sender, logentry, **kwargs):
if not logentry.action_type.startswith('pretix.plugins.ticketoutputpdf.layout') or not isinstance(
logentry.content_object, TicketLayout):
return
a_text = _('Ticket layout {val}')
a_map = {
'href': reverse('plugins:ticketoutputpdf:edit', kwargs={
'event': sender.slug,
'organizer': sender.organizer.slug,
'layout': logentry.content_object.id
}),
'val': escape(logentry.content_object.name),
}
a_map['val'] = '<a href="{href}">{val}</a>'.format_map(a_map)
return a_text.format_map(a_map)
@log_entry_types.new_from_dict({
'pretix.plugins.ticketoutputpdf.layout.added': _('Ticket layout created.'),
'pretix.plugins.ticketoutputpdf.layout.deleted': _('Ticket layout deleted.'),
'pretix.plugins.ticketoutputpdf.layout.changed': _('Ticket layout changed.'),
})
class PdfTicketLayoutLogEntryType(EventLogEntryType):
object_type = TicketLayout
object_link_wrapper = _('Ticket layout {val}')
object_link_viewname = 'plugins:ticketoutputpdf:edit'
object_link_argname = 'layout'
def _ticket_layouts_for_item(request, item):

View File

@@ -1076,8 +1076,8 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
if warn:
messages.warning(request, _('Please fill in answers to all required questions.'))
return False
if cp.item.ask_attendee_data and self.request.event.settings.get('attendee_attendees_required', as_type=bool) \
and (cp.street is None or cp.city is None or cp.country is None):
if cp.item.ask_attendee_data and self.request.event.settings.get('attendee_addresses_required', as_type=bool) \
and (cp.street is None and cp.city is None and cp.country is None):
if warn:
messages.warning(request, _('Please fill in answers to all required questions.'))
return False

View File

@@ -133,6 +133,7 @@ class InvoiceAddressForm(BaseInvoiceAddressForm):
class InvoiceNameForm(InvoiceAddressForm):
address_validation = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@@ -13,7 +13,7 @@
</p>
{% elif incomplete %}
<div class="alert alert-danger">
{% trans "A product in your cart is only sold in combination with add-on products that are no longer available. Please contact the event organizer." %}
{% trans "A product in your cart is only sold in combination with add-on products that are not available. Please contact the event organizer." %}
</div>
{% endif %}
<form class="form-horizontal" method="post" data-asynctask

View File

@@ -6,13 +6,14 @@
{% load eventsignal %}
{% load rich_text %}
{% for c in form.categories %}
{% with category_idx=forloop.counter %}
<fieldset data-addon-max-count="{{ c.max_count }}"{% if c.multi_allowed %} data-addon-multi-allowed{% endif %}>
<legend>{{ c.category.name }}</legend>
{% if c.category.description %}
{{ c.category.description|rich_text }}
{% endif %}
{% if c.min_count == c.max_count %}
<p class="addon-count-desc">
<p class="addon-count-desc" id="c-{{ form.pos.pk }}-{{ category_idx }}-addon-count-desc">
{% blocktrans trimmed count min_count=c.min_count %}
You need to choose exactly one option from this category.
{% plural %}
@@ -21,7 +22,7 @@
</p>
{% elif c.min_count == 0 and c.max_count >= c.items|length and not c.multi_allowed %}
{% elif c.min_count == 0 %}
<p class="addon-count-desc">
<p class="addon-count-desc" id="c-{{ form.pos.pk }}-{{ category_idx }}-addon-count-desc">
{% blocktrans trimmed count max_count=c.max_count %}
You can choose {{ max_count }} option from this category.
{% plural %}
@@ -29,7 +30,7 @@
{% endblocktrans %}
</p>
{% else %}
<p class="addon-count-desc">
<p class="addon-count-desc" id="c-{{ form.pos.pk }}-{{ category_idx }}-addon-count-desc">
{% blocktrans trimmed with min_count=c.min_count max_count=c.max_count %}
You can choose between {{ min_count }} and {{ max_count }} options from
this category.
@@ -58,7 +59,7 @@
</div>
{% endif %}
{% if item.min_per_order and item.min_per_order > 1 %}
<p>
<p id="cp-{{ form.pos.pk }}-item-{{ item.pk }}-min-order">
<small>
{% blocktrans trimmed with num=item.min_per_order %}
minimum amount to order: {{ num }}
@@ -196,12 +197,14 @@
{% endif %}
id="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}"
name="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}"
aria-label="{% blocktrans with item=item.name var=var %}Add {{ item }}, {{ var }} to cart{% endblocktrans %}">
aria-label="{% blocktrans with item=item.name var=var %}Add {{ item }}, {{ var }} to cart{% endblocktrans %}"
aria-describedby="c-{{ form.pos.pk }}-{{ category_idx }}-addon-count-desc">
<i class="fa fa-shopping-cart" aria-hidden="true"></i>
{% trans "Select" context "checkbox" %}
</label>
{% else %}
<div class="input-item-count-group">
<fieldset class="input-item-count-group" aria-describedby="c-{{ form.pos.pk }}-{{ category_idx }}-addon-count-desc cp-{{ form.pos.pk }}-item-{{ item.pk }}-min-order">
<legend class="sr-only">{% blocktrans with item=item.name %}Add {{ item }}, {{ var }} to cart{% endblocktrans %}</legend>
<button type="button" data-step="-1" data-controls="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-dec" aria-label="{% trans "Decrease quantity" %}">-</button>
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
{% if var.initial %}value="{{ var.initial }}"{% endif %}
@@ -211,9 +214,9 @@
max="{{ c.max_count }}"
id="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}"
name="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}"
aria-label="{% blocktrans with item=item.name var=var %}Quantity of {{ item }}, {{ var }} to order{% endblocktrans %}">
aria-label="{% trans "Quantity" %}">
<button type="button" data-step="1" data-controls="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-inc" aria-label="{% trans "Increase quantity" %}">+</button>
</div>
</fieldset>
{% endif %}
</div>
{% else %}
@@ -250,7 +253,7 @@
{% include "pretixpresale/event/fragment_quota_left.html" with avail=item.cached_availability %}
{% endif %}
{% if item.min_per_order and item.min_per_order > 1 %}
<p>
<p id="cp-{{ form.pos.pk }}-item-{{ item.pk }}-min-order">
<small>
{% blocktrans trimmed with num=item.min_per_order %}
minimum amount to order: {{ num }}
@@ -341,12 +344,13 @@
name="cp_{{ form.pos.pk }}_item_{{ item.id }}"
id="cp_{{ form.pos.pk }}_item_{{ item.id }}"
aria-label="{% blocktrans with item=item.name %}Add {{ item }} to cart{% endblocktrans %}"
{% if item.description %} aria-describedby="cp-{{ form.pos.pk }}-item-{{ item.id }}-description"{% endif %}>
aria-describedby="c-{{ form.pos.pk }}-{{ category_idx }}-addon-count-desc">
<i class="fa fa-shopping-cart" aria-hidden="true"></i>
{% trans "Select" context "checkbox" %}
</label>
{% else %}
<div class="input-item-count-group">
<fieldset class="input-item-count-group" aria-describedby="c-{{ form.pos.pk }}-{{ category_idx }}-addon-count-desc cp-{{ form.pos.pk }}-item-{{ item.pk }}-min-order">
<legend class="sr-only">{% blocktrans with item=item.name %}Add {{ item }} to cart{% endblocktrans %}</legend>
<button type="button" data-step="-1" data-controls="cp_{{ form.pos.pk }}_item_{{ item.id }}" class="btn btn-default input-item-count-dec" aria-label="{% trans "Decrease quantity" %}">-</button>
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
{% if item.free_price %}
@@ -356,10 +360,9 @@
{% if item.initial %}value="{{ item.initial }}"{% endif %}
name="cp_{{ form.pos.pk }}_item_{{ item.id }}"
id="cp_{{ form.pos.pk }}_item_{{ item.id }}"
aria-label="{% blocktrans with item=item.name %}Quantity of {{ item }} to order{% endblocktrans %}"
{% if item.description %} aria-describedby="cp-{{ form.pos.pk }}-item-{{ item.id }}-description"{% endif %}>
aria-label="{% trans "Quantity" %}">
<button type="button" data-step="1" data-controls="cp_{{ form.pos.pk }}_item_{{ item.id }}" class="btn btn-default input-item-count-inc" aria-label="{% trans "Increase quantity" %}">+</button>
</div>
</fieldset>
{% endif %}
</div>
{% else %}
@@ -370,6 +373,7 @@
{% endif %}
{% endfor %}
</fieldset>
{% endwith %}
{% empty %}
<em>
{% trans "There are no add-ons available for this product." %}

View File

@@ -10,18 +10,41 @@
<i class="fa fa-shopping-cart" aria-hidden="true"></i>
<strong>{% trans "Your cart" %}</strong>
</span>
<strong id="cart-deadline-short" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}" aria-hidden="true">
{% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
{{ cart.minutes_left|stringformat:"02d" }}:{{ cart.seconds_left|stringformat:"02d" }}
{% else %}
{% trans "Cart expired" %}
{% endif %}
</strong>
{% if cart.positions %}
<strong id="cart-deadline-short" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}" aria-hidden="true">
{% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
{{ cart.minutes_left|stringformat:"02d" }}:{{ cart.seconds_left|stringformat:"02d" }}
{% else %}
{% trans "Cart expired" %}
{% endif %}
</strong>
{% endif %}
</h2>
</summary>
<div>
<div class="panel-body">
{% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event editable=True %}
{% if cart.positions %}
{% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event editable=True %}
{% endif %}
{% if cart.current_selected_payments %}
<p>{% trans "You already selected the following payment methods:" %}</p>
<div class="list-group">
{% for p in cart.current_selected_payments %}
<div class="list-group-item">
<div class="row">
<div class="col-xs-9">
{{ p.provider_name }}
</div>
<div class="col-xs-3 text-right">
{% if p.payment_amount %}
{{ p.payment_amount|money:request.event.currency }}
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<div class="checkout-button-row">
<form class="checkout-button-primary" method="get" action="{% eventurl request.event "presale:event.checkout.start" cart_namespace=cart_namespace %}">
<p><button class="btn btn-primary btn-lg" type="submit"{% if has_addon_choices or cart.total == 0 %} aria-label="{% trans "Continue with order process" %}"{% endif %}>

View File

@@ -1,31 +1,29 @@
{% load i18n %}
{% load bootstrap3 %}
{% load textbubble %}
{# Changes should be replicated in pretixcontrol/orders/fragment_order_status.html and in pretix/base/models/orders.py #}
{% if order.status == "n" %}
{% if order.require_approval %}
{% trans "Approval pending" %}
{% textbubble "warning" icon="exclamation-triangle" %}{% trans "Approval pending" %}{% endtextbubble %}
{% elif order.total == 0 %}
{% trans "Confirmation pending" context "order state" %}
{% textbubble "warning" icon="exclamation-triangle" %}{% trans "Confirmation pending" context "order state" %}{% endtextbubble %}
{% elif event.settings.payment_pending_hidden %}
{# intentionally left blank #}
{% elif order.valid_if_pending %}
{% trans "Confirmed" context "order state" %}
{% textbubble "info" icon="info-circle" %}{% trans "Confirmed" context "order state" %}{% endtextbubble %}
{% else %}
{% trans "Payment pending" %}
{% endif %}
{% if not event.settings.payment_pending_hidden %}
<i class="status-dot fa fa-circle {% if order.valid_if_pending %}text-info{% else %}text-warning{% endif %}" aria-hidden="true"></i>
{% textbubble "warning" icon="exclamation-triangle" %}{% trans "Payment pending" %}{% endtextbubble %}
{% endif %}
{% elif order.status == "p" %}
{% if order.count_positions == 0 %}
{% trans "Canceled (paid fee)" %} <i class="status-dot fa fa-info-circle text-info" aria-hidden="true"></i>
{% textbubble "info" icon="info-circle" %}{% trans "Canceled (paid fee)" %}{% endtextbubble %}
{% elif order.total == 0 %}
{% trans "Confirmed" context "order state" %} <i class="status-dot fa fa-check-circle text-success" aria-hidden="true"></i>
{% textbubble "success" icon="check" %}{% trans "Confirmed" context "order state" %}{% endtextbubble %}
{% else %}
{% trans "Paid" %} <i class="status-dot fa fa-check-circle text-success" aria-hidden="true"></i>
{% textbubble "success" icon="check" %}{% trans "Paid" %}{% endtextbubble %}
{% endif %}
{% elif order.status == "e" %}
{% trans "Expired" %} <i class="status-dot fa fa-minus-circle text-danger" aria-hidden="true"></i>
{% textbubble "danger" icon="minus" %}{% trans "Expired" %}{% endtextbubble %}
{% elif order.status == "c" %}
{% trans "Canceled" %} <i class="status-dot fa fa-times-circle text-danger" aria-hidden="true"></i>
{% textbubble "danger" icon="times" %}{% trans "Canceled" %}{% endtextbubble %}
{% endif %}

View File

@@ -10,12 +10,12 @@
<section aria-labelledby="{{ form_prefix }}category-{{ category.id }}"{% if category.description %} aria-describedby="{{ form_prefix }}category-info-{{ category.id }}"{% endif %}>
<h{{ headline_level|default:3 }} class="h3" id="{{ form_prefix }}category-{{ category.id }}">{{ category.name }}
{% if category.subevent_name %}
<small class="text-muted"><i class="fa fa-calendar"></i> {{ category.subevent_name }}</small>
<small class="text-muted"><i class="fa fa-calendar" aria-hidden="true"></i> {{ category.subevent_name }}</small>
{% endif %}
{% if category.category_has_discount %}
<small class="text-success">
<i class="fa fa-star" aria-hidden="true"></i>
<span class="sr-only">Congratulations!</span>
<span class="sr-only">{% trans "Congratulations!" %}</span>
{% trans "Your order qualifies for a discount" %}
</small>
{% endif %}
@@ -219,7 +219,8 @@
{% trans "Select" context "checkbox" %}
</label>
{% else %}
<div class="input-item-count-group">
<fieldset class="input-item-count-group">
<legend class="sr-only">{% blocktrans with item=item.name %}Add {{ item }}, {{ var }} to cart{% endblocktrans %}</legend>
<button type="button" data-step="-1" data-controls="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-dec" aria-label="{% trans "Decrease quantity" %}"
{% if not ev.presale_is_running %}disabled{% endif %}>-</button>
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
@@ -230,10 +231,10 @@
max="{{ var.order_max }}"
id="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}"
name="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}"
aria-label="{% blocktrans with item=item.name var=var.name %}Quantity of {{ item }}, {{ var }} to order{% endblocktrans %}">
aria-label="{% trans "Quantity" %}">
<button type="button" data-step="1" data-controls="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-inc" aria-label="{% trans "Increase quantity" %}"
{% if not ev.presale_is_running %}disabled{% endif %}>+</button>
</div>
</fieldset>
{% endif %}
</div>
{% else %}
@@ -370,7 +371,8 @@
{% trans "Select" context "checkbox" %}
</label>
{% else %}
<div class="input-item-count-group">
<fieldset class="input-item-count-group">
<legend class="sr-only">{% blocktrans with item=item.name %}Add {{ item }} to cart{% endblocktrans %}</legend>
<button type="button" data-step="-1" data-controls="{{ form_prefix }}item_{{ item.id }}" class="btn btn-default input-item-count-dec" aria-label="{% trans "Decrease quantity" %}"
{% if not ev.presale_is_running %}disabled{% endif %}>-</button>
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
@@ -382,11 +384,10 @@
max="{{ item.order_max }}"
name="{{ form_prefix }}item_{{ item.id }}"
id="{{ form_prefix }}item_{{ item.id }}"
aria-label="{% blocktrans with item=item.name %}Quantity of {{ item }} to order{% endblocktrans %}"
{% if item.description %} aria-describedby="{{ form_prefix }}item-{{ item.id }}-description"{% endif %}>
aria-label="{% trans "Quantity" %}">
<button type="button" data-step="1" data-controls="{{ form_prefix }}item_{{ item.id }}" class="btn btn-default input-item-count-inc" aria-label="{% trans "Increase quantity" %}"
{% if not ev.presale_is_running %}disabled{% endif %}>+</button>
</div>
</fieldset>
{% endif %}
</div>
{% else %}

View File

@@ -231,16 +231,17 @@
{% trans "Select" context "checkbox" %}
</label>
{% else %}
<div class="input-item-count-group">
<fieldset class="input-item-count-group">
<legend class="sr-only">{% blocktrans with item=item.name %}Add {{ item }}, {{ var }} to cart{% endblocktrans %}</legend>
<button type="button" data-step="-1" data-controls="variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-dec" aria-label="{% trans "Decrease quantity" %}">-</button>
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
max="{{ var.order_max }}"
id="variation_{{ item.id }}_{{ var.id }}"
name="variation_{{ item.id }}_{{ var.id }}"
{% if options == 1 %}value="1"{% endif %}
aria-label="{% blocktrans with item=item.name var=var.name %}Quantity of {{ item }}, {{ var }} to order{% endblocktrans %}">
aria-label="{% trans "Quantity" %}">
<button type="button" data-step="1" data-controls="variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-inc" aria-label="{% trans "Increase quantity" %}">+</button>
</div>
</fieldset>
{% endif %}
{% else %}
<label>
@@ -385,7 +386,8 @@
{% trans "Select" context "checkbox" %}
</label>
{% else %}
<div class="input-item-count-group">
<fieldset class="input-item-count-group">
<legend class="sr-only">{% blocktrans with item=item.name %}Add {{ item }} to cart{% endblocktrans %}</legend>
<button type="button" data-step="-1" data-controls="item_{{ item.id }}" class="btn btn-default input-item-count-dec" aria-label="{% trans "Decrease quantity" %}">-</button>
<input type="number" class="form-control input-item-count"
placeholder="0" min="0"
@@ -393,10 +395,9 @@
id="item_{{ item.id }}"
name="item_{{ item.id }}"
{% if options == 1 %}value="1"{% endif %}
aria-label="{% blocktrans with item=item.name %}Quantity of {{ item }} to order{% endblocktrans %}"
{% if item.description %} aria-describedby="item-{{ item.id }}-description"{% endif %}>
aria-label="{% trans "Quantity" %}">
<button type="button" data-step="1" data-controls="item_{{ item.id }}" class="btn btn-default input-item-count-inc" aria-label="{% trans "Increase quantity" %}">+</button>
</div>
</fieldset>
{% endif %}
{% else %}
<label>

View File

@@ -2,7 +2,7 @@
{% load date_fast %}
{% load calendarhead %}
<div class="table-responsive">
<table class="table table-calendar">
<table class="table table-calendar" role="grid">
<caption class="sr-only">{% trans "Calendar" %}</caption>
<thead>
<tr>
@@ -18,7 +18,26 @@
{% if day %}
<td class="day {% if day.events %}has-events{% else %}no-events{% endif %}"
data-date="{{ day.date|date_fast:"SHORT_DATE_FORMAT" }}">
<p><time datetime="{{ day.date|date_fast:"Y-m-d" }}">{{ day.day }}</time></p>
<p>
{% if day.events %}
<a href="#selected-day" class="day-label event hidden-sm hidden-md hidden-lg">
<b aria-hidden="true">{{ day.day }}</b>
<time datetime="{{ day.date|date_fast:"Y-m-d" }}" class="sr-only">
{{ day.date|date_fast:"SHORT_DATE_FORMAT" }}
</time>
<span class="sr-only">
({% blocktrans trimmed count count=day.events|length %}
{{ count }} event
{% plural %}
{{ count }} events
{% endblocktrans %})
</span>
</a>
<time datetime="{{ day.date|date_fast:"Y-m-d" }}" class="hidden-xs">{{ day.day }}</time>
{% else %}
<time datetime="{{ day.date|date_fast:"Y-m-d" }}" class="day-label">{{ day.day }}</time>
{% endif %}
</p>
<ul class="events">
{% for event in day.events %}
<li><a class="event {% if event.continued %}continued{% endif %} {% spaceless %}
@@ -111,9 +130,7 @@
{% endfor %}
</tr>
{% endfor %}
<tr class="selected-day hidden">
<td colspan="7"></td>
</tr>
</tbody>
</table>
<div id="selected-day" aria-live="polite" class="table-calendar hidden-sm hidden-md hidden-lg"></div>
</div>

View File

@@ -20,4 +20,5 @@
<script type="text/javascript" src="{% static "pretixpresale/js/ui/cart.js" %}"></script>
<script type="text/javascript" src="{% static "lightbox/js/lightbox.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/iframe.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script>
{% endcompress %}

View File

@@ -4,9 +4,9 @@
{% if request.organizer.settings.customer_accounts %}
<nav class="loginstatus" aria-label="{% trans "customer account" %}">
{% if request.customer %}
<a href="{% abseventurl request.organizer "presale:organizer.customer.profile" %}"
<a href="{% abseventurl request.organizer "presale:organizer.customer.index" %}"
aria-label="{% trans "View customer account" %}" data-placement="bottom"
title="{% trans "View user profile" %}" data-toggle="tooltip">
title="{% trans "View customer account" %}" data-toggle="tooltip">
<span class="fa fa-user" aria-hidden="true"></span>
{{ request.customer.name|default:request.customer.email }}</a>
<a href="{% if request.event_domain %}{% abseventurl request.event "presale:organizer.customer.logout" %}{% else %}{% abseventurl request.organizer "presale:organizer.customer.logout" %}{% endif %}?next={{ request.path|urlencode }}%3F{{ request.META.QUERY_STRING|urlencode }}"

View File

@@ -1,33 +1,39 @@
{% extends "pretixpresale/organizers/base.html" %}
{% extends "pretixpresale/organizers/customer_base.html" %}
{% load i18n %}
{% load icon %}
{% load eventurl %}
{% block title %}{% trans "Delete address" %}{% endblock %}
{% block content %}
<h2>
{% trans "Delete address" %}
</h2>
{% block inner %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% icon "address-card-o" %} <b>{% trans "Delete address" %}</b>
</h3>
</div>
<div class="panel-body account-addresses">
<p>
{% trans "Do you really want to delete the following address from your account?" %}
</p>
<address>
{{ address.describe|linebreaksbr }}
</address>
</div>
</div>
<form method="post">
{% csrf_token %}
<p>
{% trans "Do you really want to delete the following address from your account?" %}
</p>
<address>
{{ address.describe|linebreaksbr }}
</address>
<div class="row">
<div class="col-md-4 col-sm-6">
<a class="btn btn-block btn-default btn-lg"
href="{% abseventurl request.organizer "presale:organizer.customer.profile" %}">
href="{% abseventurl request.organizer "presale:organizer.customer.addresses" %}">
{% trans "Go back" %}
</a>
</div>
<div class="col-md-4 col-md-offset-4 col-sm-6">
<button class="btn btn-block btn-danger btn-lg" type="submit">
{% icon "trash" %}
{% trans "Delete" %}
</button>
</div>
<div class="clearfix"></div>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,42 @@
{% extends "pretixpresale/organizers/customer_base.html" %}
{% load i18n %}
{% load icon %}
{% load eventurl %}
{% block title %}{% trans "Addresses" %}{% endblock %}
{% block inner %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% icon "address-card-o" %}
<b>{% trans "Addresses" %}</b> ({{ page_obj.paginator.count }})
</h3>
</div>
<div class="panel-body">
{% if invoice_addresses %}
<ul class="full-width-list alternating-rows account-addresses">
<li class="row">
{% for ia in invoice_addresses %}
{% if forloop.counter0 and forloop.counter0|divisibleby:4 %}
</li>
<li class="row">
{% endif %}
<div class="col-md-3 col-xs-12">
<address>{{ ia.describe|linebreaksbr }}</address>
<p class="blank-after">
<a href="{% abseventurl request.organizer "presale:organizer.customer.address.delete" id=ia.id %}"
class="btn btn-danger btn-sm">
{% icon "trash" %}
{% trans "Delete" %}
</a>
</p>
</div>
{% endfor %}
</li>
</ul>
{% else %}
<p class="text-center">{% trans "You dont have any addresses in your account yet." %}</p>
{% endif %}
</div>
</div>
{% include "pretixcontrol/pagination.html" %}
{% endblock %}

View File

@@ -0,0 +1,55 @@
{% extends "pretixpresale/organizers/base.html" %}
{% load i18n %}
{% load icon %}
{% load eventurl %}
{% block content %}
<h2>
{% trans "Your account" %}
</h2>
<div class="blank-after">
<dl class="row">
<div class="col-sm-6">
<dt>{{ customer.name }}</dt>
<dd>{{ customer.email }}</dd>
{% if customer.phone %}
<dd>{% icon "phone" %} {{ customer.phone }}</dd>
{% endif %}
<dd>
<ul class="list-inline">
<li>
<a href="{% eventurl request.organizer "presale:organizer.customer.change" %}">
{% icon "edit" %}
{% trans "Change account information" %}
</a>
</li>
<li>
<a href="{% eventurl request.organizer "presale:organizer.customer.password" %}">
{% icon "key" %}
{% trans "Change password" %}
</a>
</li>
</ul>
</dd>
<dd>
</dd>
</div>
<div class="col-sm-6 text-right">
<dt>{% trans "Customer ID" %}</dt>
<dd>#{{ customer.identifier }}</dd>
</div>
</dl>
<nav class="subnav row" aria-label="{% trans "customer account information" %}">
<ul class="list-inline blank-after col-xs-12">
{% for nav in sub_nav %}
<li>
<a href="{{ nav.url }}"{% if nav.active %} class="active"{% endif %}>
{% icon nav.icon %}{{ nav.label }}
</a>
</li>
{% endfor %}
</ul>
</nav>
</div>
{% block inner %}
{% endblock %}
{% endblock %}

View File

@@ -1,7 +1,7 @@
{% extends "pretixpresale/organizers/base.html" %}
{% load i18n %}
{% load icon %}
{% load eventurl %}
{% load urlreplace %}
{% load bootstrap3 %}
{% block title %}{% trans "Log in" %}{% endblock %}
{% block content %}
@@ -18,6 +18,7 @@
{% bootstrap_form form %}
<div class="form-group buttons">
<button type="submit" class="btn btn-primary btn-lg btn-block">
{% icon "sign-in" %}
{% trans "Log in" %}
</button>
</div>
@@ -35,6 +36,7 @@
<div class="col-md-6">
<a class="btn btn-link btn-block"
href="{% eventurl request.organizer "presale:organizer.customer.register" %}">
{% icon "address-book-o" %}
{% trans "Create account" %}
</a>
</div>

View File

@@ -1,96 +1,127 @@
{% extends "pretixpresale/organizers/base.html" %}
{% extends "pretixpresale/organizers/customer_base.html" %}
{% load i18n %}
{% load icon %}
{% load eventurl %}
{% load urlreplace %}
{% load money %}
{% load bootstrap3 %}
{% load textbubble %}
{% block title %}{% trans "Your membership" %}{% endblock %}
{% block content %}
<h2>
{% trans "Your membership" %}
</h2>
<div class="panel panel-primary items">
{% block inner %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Details" %}
{% if membership.membership_type.transferable %}
{% icon "users" %}
{% else %}
{% icon "id-badge" %}
{% endif %}
<b>{% trans "Your membership" %}</b>
{% if membership.testmode %}
<span class="h6">
{% textbubble "warning" %}
{% trans "TEST MODE" %}
{% endtextbubble %}
</span>
{% endif %}
</h3>
</div>
<div class="panel-body">
<dl class="dl-horizontal">
<dt>{% trans "Membership type" %}</dt>
<dd>{{ membership.membership_type.name }}</dd>
<dd>{% if membership.canceled %}<del>{% endif %}
{{ membership.membership_type.name }}
{% if membership.canceled %}</del>
<small>
{% textbubble "danger" icon="times" %}
{% trans "Canceled" %}
{% endtextbubble %}
</small>
{% endif %}
<br><small class="text-muted">
{% if membership.membership_type.transferable %}
({% trans "transferable" %})
{% else %}
({% trans "not transferable" %})
{% endif %}
</small>
</dd>
<dt>{% trans "Valid from" %}</dt>
<dd>{{ membership.date_start|date:"SHORT_DATETIME_FORMAT" }}
<dt>{% trans "Valid until" %}</dt>
<dd>{{ membership.date_end|date:"SHORT_DATETIME_FORMAT" }}
<dt>{% trans "Attendee name" %}</dt>
<dd>{{ membership.attendee_name }}
<dd>{{ membership.attendee_name|default_if_none:"" }}
<dt>{% trans "Maximum usages" %}</dt>
<dd>{{ membership.membership_type.max_usages|default_if_none:"" }}</dd>
</dl>
</div>
</div>
<div class="panel panel-default items">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Usages" %}
</h3>
</div>
<table class="panel-body table table-hover">
<caption class="sr-only">{% trans "Usages" %}</caption>
<thead>
<tr>
<th>{% trans "Order code" %}</th>
<th>{% trans "Event" %}</th>
<th>{% trans "Product" %}</th>
<th>{% trans "Order date" %}</th>
<th class="text-right">{% trans "Status" %}</th>
<th class="text-right"></th>
</tr>
</thead>
<tbody>
<div class="panel-body">
{% if usages %}
<ul class="full-width-list alternating-rows">
{% for op in usages %}
<tr>
<td>
<strong>
{{ op.order.code }}-{{ op.positionid }}
</strong>
{% if op.order.testmode %}
<span class="label label-warning">{% trans "TEST MODE" %}</span>
{% endif %}
</td>
<td>
{{ op.order.event }}
{% if op.subevent %}
<br>
{{ op.subevent|default:"" }}
{% endif %}
</td>
<td>
{{ op.item.name }}
{% if op.variation %} {{ op.variation }}{% endif %}
</td>
<td>
{{ op.order.datetime|date:"SHORT_DATETIME_FORMAT" }}
</td>
<td class="text-right flip">
{% if op.canceled %}
{% trans "Canceled" %} <i class="{{ class }} fa fa-times-circle text-danger" aria-hidden="true"></i>
{% else %}
{% include "pretixcontrol/orders/fragment_order_status.html" with order=op.order %}
{% endif %}
</td>
<td class="text-right flip">
<a href="{% abseventurl op.order.event "presale:event.order" order=op.order.code secret=op.order.secret %}"
target="_blank"
class="btn btn-default">
{% trans "Details" %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% include "pretixcontrol/pagination.html" %}
<li class="row">
<dl>
<div class="col-md-4 col-sm-5 col-xs-12">
<dt class="sr-only">{% trans "Order" %}</dt>
<dd><strong>
<a href="{% abseventurl op.order.event "presale:event.order" order=op.order.code secret=op.order.secret %}" target="_blank">
{% icon "shopping-cart" %}
{{ op.order.code }}-{{ op.positionid }}
</a>
</strong>
<small>{% include "pretixpresale/event/fragment_order_status.html" with order=op.order event=op.order.event %}</small>
</dd>
<dd><time datetime="{{ op.order.datetime|date:"Y-m-d H:i" }}" class="text-muted small">{{ op.order.datetime|date:"SHORT_DATETIME_FORMAT" }}</time></dd>
{% if op.order.testmode %}
<dd>
<small>
{% textbubble "warning" %}
{% trans "TEST MODE" %}
{% endtextbubble %}
</small>
</dd>
{% endif %}
</div>
<div class="col-md-6 col-sm-5 col-xs-8">
<dt class="sr-only">{% trans "Product" %}</dt>
<dd>{{ op.item.name }}
{% if op.variation %} - {{ op.variation }}{% endif %}
</dd>
<dt class="sr-only">{% trans "Event" %}</dt>
<dd>
<small class="text-muted">
{{ op.order.event }}
{% if op.subevent %}
<br>{{ op.subevent }}
{% endif %}
{% if not op.order.event.has_subevents and op.order.event.settings.show_dates_on_frontpage %}
<br>{{ op.order.event.get_date_range_display }}
{% endif %}
</small>
</dd>
</div>
<div class="col-sm-2 col-xs-4">
<dt class="sr-only">{% trans "Actions" %}</dt>
<dd class="text-right">
<a href="{% abseventurl op.order.event "presale:event.order" order=op.order.code secret=op.order.secret %}"
target="_blank">
{% icon "list-ul" %}
{% trans "Details" %}
</a></dd>
</div>
</dl>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-center">{% trans "You havent used this membership yet." %}</p>
{% endif %}
</div>
</div>
{% include "pretixcontrol/pagination.html" %}
{% endblock %}

View File

@@ -0,0 +1,111 @@
{% extends "pretixpresale/organizers/customer_base.html" %}
{% load i18n %}
{% load icon %}
{% load eventurl %}
{% load textbubble %}
{% block title %}{% trans "Memberships" %}{% endblock %}
{% block inner %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% icon "id-badge" %}
<b>{% trans "Memberships" %}</b> ({{ page_obj.paginator.count }})
</h3>
</div>
<div class="panel-body">
{% if memberships %}
<ul class="full-width-list alternating-rows">
{% for m in memberships %}
<li class="row">
<dl>
<div class="col-xs-5">
<dt>
{% if m.canceled %}<del>{% endif %}
<a href="{% abseventurl request.organizer "presale:organizer.customer.membership" id=m.id %}">
{{ m.membership_type.name }}
</a>
{% if m.canceled %}</del>{% endif %}
{% if m.membership_type.transferable %}
<span class="text-muted" data-toggle="tooltip" title="{% trans "Membership is transferable" %}">
{% icon "users" %}
</span>
{% endif %}
</dt>
{% if m.attendee_name %}
<dd class="text-muted">
{% icon "id-badge" %}
<span class="sr-only">{% trans "Attendee name" %}:</span>
{{ m.attendee_name }}
</dd>
{% endif %}
<dd class="text-muted">
<small>
{% if m.canceled %}
{% textbubble "danger" icon="times" %}
{% trans "Canceled" %}
{% endtextbubble %}
{% elif m.expired %}
{% icon "minus-square-o" %}
{% trans "Expired since" %}
<time datetime="{{ m.date_end|date:"Y-m-d H:i" }}">
{{ m.date_end|date:"SHORT_DATETIME_FORMAT" }}
</time>
{% elif m.not_yet_valid %}
{% icon "clock-o" %}
{% trans "Valid from" %}
<time datetime="{{ m.date_start|date:"Y-m-d H:i" }}">
{{ m.date_start|date:"SHORT_DATETIME_FORMAT" }}
</time>
{% else %}
{% icon "check" %}
{% trans "Valid until" %}
<time datetime="{{ m.date_end|date:"Y-m-d H:i" }}">
{{ m.date_end|date:"SHORT_DATETIME_FORMAT" }}
</time>
{% endif %}
</small>
</dd>
{% if m.testmode %}
<dd>
<small>
{% textbubble "warning" %}
{% trans "TEST MODE" %}
{% endtextbubble %}
</small>
</dd>
{% endif %}
</div>
<div class="col-xs-5">
<dd>
<div class="quotabox full-width">
<div class="progress">
<div class="progress-bar progress-bar-success progress-bar-{{ m.percentage_used }}">
</div>
</div>
<div class="numbers">
{{ m.usages }} /
{{ m.membership_type.max_usages|default_if_none:"∞" }}
</div>
</div>
</dd>
</div>
<div class="col-xs-2">
<dt class="sr-only">{% trans "Actions" %}</dt>
<dd class="text-right">
<a href="{% abseventurl request.organizer "presale:organizer.customer.membership" id=m.id %}">
{% icon "list-ul" %}
{% trans "Details" %}
</a>
</dd>
</div>
</dl>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-center">{% trans "You dont have any memberships in your account yet." %}</p>
{% endif %}
</div>
</div>
{% include "pretixcontrol/pagination.html" %}
{% endblock %}

View File

@@ -0,0 +1,83 @@
{% extends "pretixpresale/organizers/customer_base.html" %}
{% load i18n %}
{% load icon %}
{% load eventurl %}
{% load money %}
{% load textbubble %}
{% block title %}{% trans "Your account" %}{% endblock %}
{% block inner %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% icon "shopping-cart" %}
<b>{% trans "Orders" %}</b> ({{ page_obj.paginator.count }})
</h3>
</div>
<div class="panel-body">
{% if orders %}
<ul class="full-width-list alternating-rows">
{% for o in orders %}
<li class="row">
<dl>
<div class="col-md-4 col-sm-5 col-xs-8">
<dt class="sr-only">{% trans "Order" %}</dt>
<dd><strong>
<a href="{% abseventurl o.event "presale:event.order" order=o.code secret=o.secret %}" target="_blank">
{% icon "shopping-cart" %}
{{ o.code }}</a>
</strong>
{% if o.customer_id != customer.pk %}
<span class="text-muted" data-toggle="tooltip"
title="{% trans "Matched to the account based on the email address." %}">
{% icon "compress" %}
</span>
{% endif %}
<small>{% include "pretixpresale/event/fragment_order_status.html" with order=o event=o.event %}</small>
</dd>
<dd><time datetime="{{ o.datetime|date:"Y-m-d H:i" }}" class="text-muted small">{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}</time></dd>
{% if o.testmode %}
<dd>
<small>
{% textbubble "warning" %}
{% trans "TEST MODE" %}
{% endtextbubble %}
</small>
</dd>
{% endif %}
</div>
<div class="col-md-2 col-sm-2 col-xs-4 text-right">
<dt class="sr-only">{% trans "Order total" %}</dt>
<dd>{{ o.total|money:o.event.currency }}</dd>
<dt class="sr-only">{% trans "Positions" %}</dt>
<dd class="text-muted"><small>{% blocktranslate count counter=o.count_positions|default_if_none:0 %}{{ counter }} item{% plural %}{{ counter }} items{% endblocktranslate %}</small>
</dd>
</div>
<div class="col-md-4 col-sm-3 col-xs-8">
<dt class="sr-only">{% trans "Event" %}</dt>
<dd>
{{ o.event }}
{% if not o.event.has_subevents and o.event.settings.show_dates_on_frontpage %}
<br><small class="text-muted">{{ o.event.get_date_range_display }}</small>
{% endif %}
</dd>
</div>
<div class="col-sm-2 col-xs-4">
<dt class="sr-only">{% trans "Actions" %}</dt>
<dd class="text-right">
<a href="{% abseventurl o.event "presale:event.order" order=o.code secret=o.secret %}"
target="_blank">
{% icon "list-ul" %}
{% trans "Details" %}
</a></dd>
</div>
</dl>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-center">{% trans "You dont have any orders in your account yet." %}</p>
{% endif %}
</div>
</div>
{% include "pretixcontrol/pagination.html" %}
{% endblock %}

View File

@@ -1,253 +0,0 @@
{% extends "pretixpresale/organizers/base.html" %}
{% load i18n %}
{% load eventurl %}
{% load urlreplace %}
{% load money %}
{% load bootstrap3 %}
{% block title %}{% trans "Your account" %}{% endblock %}
{% block content %}
<h2>
{% trans "Your account" %}
</h2>
<div class="panel panel-primary items">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Account information" %}
</h3>
</div>
<div class="panel-body">
<dl class="dl-horizontal">
<dt>{% trans "Customer ID" %}</dt>
<dd>#{{ customer.identifier }}</dd>
{% if customer.provider %}
<dt>{% trans "Login method" %}</dt>
<dd>{{ customer.provider.name }}</dd>
{% endif %}
<dt>{% trans "Email" %}</dt>
<dd>{{ customer.email }}
</dd>
<dt>{% trans "Name" %}</dt>
<dd>{{ customer.name }}</dd>
{% if customer.phone %}
<dt>{% trans "Phone" %}</dt>
<dd>{{ customer.phone }}</dd>
{% endif %}
</dl>
<div class="text-right">
<a href="{% eventurl request.organizer "presale:organizer.customer.change" %}"
class="btn btn-default">
{% trans "Change account information" %}
</a>
{% if not customer.provider %}
<a href="{% eventurl request.organizer "presale:organizer.customer.password" %}"
class="btn btn-default">
{% trans "Change password" %}
</a>
{% endif %}
</div>
</div>
</div>
<div>
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active">
<a href="#orders" aria-controls="orders" role="tab" data-toggle="tab">{% trans "Orders" %}</a>
</li>
<li role="presentation">
<a href="#memberships" aria-controls="memberships" role="tab" data-toggle="tab">{% trans "Memberships" %}</a>
</li>
<li role="presentation">
<a href="#addresses" aria-controls="addresses" role="tab" data-toggle="tab">{% trans "Addresses" %}</a>
</li>
<li role="presentation">
<a href="#profiles" aria-controls="profiles" role="tab" data-toggle="tab">{% trans "Attendee profiles" %}</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="orders">
<table class="panel-body table table-hover">
<thead>
<tr>
<th>{% trans "Order code" %}</th>
<th>{% trans "Event" %}</th>
<th>{% trans "Order date" %}</th>
<th class="text-right">{% trans "Order total" %}</th>
<th class="text-right">{% trans "Positions" %}</th>
<th class="text-right">{% trans "Status" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for o in orders %}
<tr>
<td>
<strong>
<a href="{% abseventurl o.event "presale:event.order" order=o.code secret=o.secret %}" target="_blank">
{{ o.code }}
</a>
</strong>
{% if o.testmode %}
<span class="label label-warning">{% trans "TEST MODE" %}</span>
{% endif %}
</td>
<td>
{{ o.event }}
{% if not o.event.has_subevents and o.event.settings.show_dates_on_frontpage %}
<br><small class="text-muted">{{ o.event.get_date_range_display }}</small>
{% endif %}
</td>
<td>
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
{% if o.customer_id != customer.pk %}
<span class="fa fa-link text-muted"
data-toggle="tooltip"
title="{% trans "Matched to the account based on the email address." %}"
></span>
{% endif %}
</td>
<td class="text-right flip">
{{ o.total|money:o.event.currency }}
</td>
<td class="text-right flip">{{ o.count_positions|default_if_none:"0" }}</td>
<td class="text-right flip">{% include "pretixpresale/event/fragment_order_status.html" with order=o event=o.event %}</td>
<td class="text-right flip">
<a href="{% abseventurl o.event "presale:event.order" order=o.code secret=o.secret %}"
target="_blank"
class="btn btn-default">
{% trans "Details" %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% include "pretixcontrol/pagination.html" %}
</div>
<div role="tabpanel" class="tab-pane" id="memberships">
<table class="panel-body table table-hover">
<caption class="sr-only">{% trans "Memberships" %}</caption>
<thead>
<tr>
<th>{% trans "Membership type" %}</th>
<th>{% trans "Valid from" %}</th>
<th>{% trans "Valid until" %}</th>
<th>{% trans "Attendee name" %}</th>
<th>{% trans "Usages" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for m in memberships %}
<tr>
<td>
{% if m.canceled %}<del>{% endif %}
{{ m.membership_type.name }}
{% if m.canceled %}</del>{% endif %}
{% if m.testmode %}<span class="label label-warning">{% trans "TEST MODE" %}</span>{% endif %}
</td>
<td>
{{ m.date_start|date:"SHORT_DATETIME_FORMAT" }}
</td>
<td>
{{ m.date_end|date:"SHORT_DATETIME_FORMAT" }}
</td>
<td>
{{ m.attendee_name }}
</td>
<td>
<div class="quotabox">
<div class="progress">
<div class="progress-bar progress-bar-success progress-bar-{{ m.percent }}">
</div>
</div>
<div class="numbers">
{{ m.usages }} /
{{ m.membership_type.max_usages|default_if_none:"∞" }}
</div>
</div>
</td>
<td class="text-right flip">
<a href="{% abseventurl request.organizer "presale:organizer.customer.membership" id=m.id %}"
data-toggle="tooltip"
title="{% trans "Details" %}"
class="btn btn-default">
<i class="fa fa-list"></i>
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center">{% trans "No memberships are stored in your account." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div role="tabpanel" class="tab-pane" id="addresses">
<table class="panel-body table table-hover">
<thead>
<tr>
<th>{% trans "Address" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for ia in invoice_addresses %}
<tr>
<td>
{{ ia.describe|linebreaksbr }}
</td>
<td class="text-right flip">
<a href="{% abseventurl request.organizer "presale:organizer.customer.address.delete" id=ia.id %}"
data-toggle="tooltip"
title="{% trans "Delete" %}"
class="btn btn-danger">
<i class="fa fa-trash"></i>
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="2" class="text-center">
{% trans "No addresses are stored in your account." %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div role="tabpanel" class="tab-pane" id="profiles">
<table class="panel-body table table-hover">
<thead>
<tr>
<th>{% trans "Profile" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for ap in customer.attendee_profiles.all %}
<tr>
<td>
{{ ap.describe|linebreaksbr }}
</td>
<td class="text-right flip">
<a href="{% abseventurl request.organizer "presale:organizer.customer.profile.delete" id=ap.id %}"
data-toggle="tooltip"
title="{% trans "Delete" %}"
class="btn btn-danger">
<i class="fa fa-trash"></i>
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="2" class="text-center">
{% trans "No attendee profiles are stored in your account." %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,33 +1,39 @@
{% extends "pretixpresale/organizers/base.html" %}
{% extends "pretixpresale/organizers/customer_base.html" %}
{% load i18n %}
{% load icon %}
{% load eventurl %}
{% block title %}{% trans "Delete profile" %}{% endblock %}
{% block content %}
<h2>
{% trans "Delete profile" %}
</h2>
{% block inner %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% icon "user" %} <b>{% trans "Delete profile" %}</b>
</h3>
</div>
<div class="panel-body">
<p>
{% trans "Do you really want to delete the following profile from your account?" %}
</p>
<p>
{{ profile.describe|linebreaksbr }}
</p>
</div>
</div>
<form method="post">
{% csrf_token %}
<p>
{% trans "Do you really want to delete the following profile from your account?" %}
</p>
<address>
{{ profile.describe|linebreaksbr }}
</address>
<div class="row">
<div class="col-md-4 col-sm-6">
<a class="btn btn-block btn-default btn-lg"
href="{% abseventurl request.organizer "presale:organizer.customer.profile" %}">
href="{% abseventurl request.organizer "presale:organizer.customer.profiles" %}">
{% trans "Go back" %}
</a>
</div>
<div class="col-md-4 col-md-offset-4 col-sm-6">
<button class="btn btn-block btn-danger btn-lg" type="submit">
{% icon "trash" %}
{% trans "Delete" %}
</button>
</div>
<div class="clearfix"></div>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,42 @@
{% extends "pretixpresale/organizers/customer_base.html" %}
{% load i18n %}
{% load icon %}
{% load eventurl %}
{% block title %}{% trans "Attendee profiles" %}{% endblock %}
{% block inner %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% icon "users" %}
<b>{% trans "Attendee profiles" %}</b> ({{ page_obj.paginator.count }})
</h3>
</div>
<div class="panel-body">
{% if attendee_profiles %}
<ol class="full-width-list alternating-rows">
<li class="row">
{% for ap in attendee_profiles %}
{% if forloop.counter0 and forloop.counter0|divisibleby:4 %}
</li>
<li class="row">
{% endif %}
<div class="col-md-3 col-xs-12">
<p>{{ ap.describe|linebreaksbr }}</p>
<p class="blank-after">
<a href="{% abseventurl request.organizer "presale:organizer.customer.profile.delete" id=ap.id %}"
class="btn btn-danger btn-sm">
{% icon "trash" %}
{% trans "Delete" %}
</a>
</p>
</div>
{% endfor %}
</li>
</ol>
{% else %}
<p class="text-center">{% trans "You dont have any attendee profiles in your account yet." %}</p>
{% endif %}
</div>
</div>
{% include "pretixcontrol/pagination.html" %}
{% endblock %}

View File

@@ -1,9 +1,11 @@
{% extends "pretixpresale/organizers/base.html" %}
{% load i18n %}
{% load icon %}
{% load rich_text %}
{% load tz %}
{% load eventurl %}
{% load urlreplace %}
{% load textbubble %}
{% load thumb %}
{% block title %}{% trans "Event list" %}{% endblock %}
{% block custom_header %}
@@ -86,51 +88,73 @@
<td>
</div>
<div class="col-md-2 col-xs-6">
<small>
{% if e.has_subevents %}
<span class="label label-default">{% trans "Event series" %}</span>
{% textbubble "info" icon="bars" %}
{% trans "Event series" %}
{% endtextbubble %}
{% elif e.presale_is_running and request.organizer.settings.event_list_availability %}
{% if e.best_availability_state == 100 %}
{% if e.best_availability_is_low %}
<span class="label label-success-warning">{% trans "Few tickets left" %}</span>
{% textbubble "success-warning" icon="exclamation-triangle" %}
{% trans "Few tickets left" %}
{% endtextbubble %}
{% else %}
{% if e.has_paid_item %}
<span class="label label-success">{% trans "Buy now" context "available_event_in_list" %}</span>
{% else %}
<span class="label label-success">{% trans "Book now" %}</span>
{% endif %}
{% textbubble "success" icon="check" %}
{% if e.has_paid_item %}
{% trans "Buy now" context "available_event_in_list" %}
{% else %}
{% trans "Book now" %}
{% endif %}
{% endtextbubble %}
{% endif %}
{% elif e.waiting_list_active and e.best_availability_state >= 0 %}
<span class="label label-warning">{% trans "Waiting list" %}</span>
{% textbubble "warning" icon="ellipsis-h" %}
{% trans "Waiting list" %}
{% endtextbubble %}
{% elif e.best_availability_state == 20 %}
<span class="label label-danger">{% trans "Reserved" %}</span>
{% textbubble "danger" icon="minus" %}
{% trans "Reserved" %}
{% endtextbubble %}
{% elif e.best_availability_state < 20 %}
{% if e.has_paid_item %}
<span class="label label-danger">{% trans "Sold out" %}</span>
{% else %}
<span class="label label-danger">{% trans "Fully booked" %}</span>
{% endif %}
{% textbubble "danger" icon="times" %}
{% if e.has_paid_item %}
{% trans "Sold out" %}
{% else %}
{% trans "Fully booked" %}
{% endif %}
{% endtextbubble %}
{% endif %}
{% elif e.presale_is_running %}
<span class="label label-success">{% trans "Book now" %}</span>
{% textbubble "success" icon="check" %}
{% trans "Book now" %}
{% endtextbubble %}
{% elif e.presale_has_ended %}
<span class="label label-danger">{% trans "Sale over" %}</span>
{% textbubble "danger" icon="times" %}
{% trans "Sale over" %}
{% endtextbubble %}
{% elif e.settings.presale_start_show_date %}
<span class="label label-warning">
{% blocktrans trimmed with date=e.effective_presale_start|date:"SHORT_DATE_FORMAT" %}
{% textbubble "warning" icon="clock-o" %}
{% with date_iso=e.effective_presale_start.isoformat date_human=e.effective_presale_start|date:"SHORT_DATE_FORMAT" %}
{% blocktrans trimmed with date='<time datetime="'|add:date_iso|add:'">'|add:date_human|add:"</time>"|safe %}
Sale starts {{ date }}
{% endblocktrans %}
</span>
{% endwith %}
{% endtextbubble %}
{% else %}
<span class="label label-warning">{% trans "Not yet on sale" %}</span>
{% textbubble "warning" icon="clock-o" %}
{% trans "Not yet on sale" %}
{% endtextbubble %}
{% endif %}
</small>
</div>
<div class="col-md-2 col-xs-6 text-right flip">
<a class="btn btn-primary btn-block" href="{{ url }}{% if e.has_subevents and e.match_by_subevents %}{{ filterquery }}{% endif %}">
{% if e.has_subevents %}<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Tickets" %}
{% if e.has_subevents %}{% icon "ticket" %} {% trans "Tickets" %}
{% elif e.presale_is_running and e.best_availability_state == 100 %}
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Tickets" %}
{% icon "ticket" %} {% trans "Tickets" %}
{% else %}
<span class="fa fa-info" aria-hidden="true"></span> {% trans "More info" %}
{% icon "info" %} {% trans "More info" %}
{% endif %}
</a>
</div>

View File

@@ -210,10 +210,13 @@ organizer_patterns = [
re_path(r'^account/password$', pretix.presale.views.customer.ChangePasswordView.as_view(), name='organizer.customer.password'),
re_path(r'^account/change$', pretix.presale.views.customer.ChangeInformationView.as_view(), name='organizer.customer.change'),
re_path(r'^account/confirmchange$', pretix.presale.views.customer.ConfirmChangeView.as_view(), name='organizer.customer.change.confirm'),
re_path(r'^account/membership/(?P<id>\d+)/$', pretix.presale.views.customer.MembershipUsageView.as_view(), name='organizer.customer.membership'),
re_path(r'^account/memberships$', pretix.presale.views.customer.MembershipView.as_view(), name='organizer.customer.memberships'),
re_path(r'^account/memberships/(?P<id>\d+)/$', pretix.presale.views.customer.MembershipUsageView.as_view(), name='organizer.customer.membership'),
re_path(r'^account/addresses$', pretix.presale.views.customer.AddressView.as_view(), name='organizer.customer.addresses'),
re_path(r'^account/addresses/(?P<id>\d+)/delete$', pretix.presale.views.customer.AddressDeleteView.as_view(), name='organizer.customer.address.delete'),
re_path(r'^account/profiles$', pretix.presale.views.customer.ProfileView.as_view(), name='organizer.customer.profiles'),
re_path(r'^account/profiles/(?P<id>\d+)/delete$', pretix.presale.views.customer.ProfileDeleteView.as_view(), name='organizer.customer.profile.delete'),
re_path(r'^account/$', pretix.presale.views.customer.ProfileView.as_view(), name='organizer.customer.profile'),
re_path(r'^account/$', pretix.presale.views.customer.OrderView.as_view(), name='organizer.customer.index'),
re_path(r'^oauth2/v1/authorize$', pretix.presale.views.oidc_op.AuthorizeView.as_view(),
name='organizer.oauth2.v1.authorize'),

View File

@@ -57,8 +57,9 @@ from pretix.base.middleware import LocaleMiddleware
from pretix.base.models import Customer, Event, Organizer
from pretix.base.timemachine import time_machine_now_assigned_from_request
from pretix.helpers.http import redirect_to_url
from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import (
get_event_domain, get_organizer_domain,
build_absolute_uri, get_event_domain, get_organizer_domain,
)
from pretix.presale.signals import process_request, process_response
@@ -134,7 +135,7 @@ def update_customer_session_auth_hash(request, customer):
def add_customer_to_request(request):
if 'cross_domain_customer_auth' in request.GET and request.event_domain:
if 'cross_domain_customer_auth' in request.GET and request.domain_mode in (KnownDomain.MODE_EVENT_DOMAIN, KnownDomain.MODE_ORG_ALT_DOMAIN):
# The user is logged in on the main domain and now wants to take their session
# to a event-specific domain. We validate the one time token received via a
# query parameter and make sure we invalidate it right away. Then, we look up
@@ -258,11 +259,12 @@ def _detect_event(request, require_live=True, require_plugin=None):
url = resolve(request.path_info)
request_domain_mode = getattr(request, 'domain_mode', 'system')
try:
if hasattr(request, 'event_domain'):
if request_domain_mode == KnownDomain.MODE_EVENT_DOMAIN:
# We are on an event's custom domain
pass
elif hasattr(request, 'organizer_domain'):
elif request_domain_mode in (KnownDomain.MODE_ORG_DOMAIN, KnownDomain.MODE_ORG_ALT_DOMAIN):
# We are on an organizer's custom domain
if 'organizer' in url.kwargs and url.kwargs['organizer']:
if url.kwargs['organizer'] != request.organizer.slug:
@@ -277,12 +279,20 @@ def _detect_event(request, require_live=True, require_plugin=None):
organizer=request.organizer,
)
# If this event has a custom domain, send the user there
domain = get_event_domain(request.event)
if domain:
# If this event has a custom domain or is not available on this alt domain, send the user there
domain, domainmode = get_event_domain(request.event, fallback=False, return_mode=True)
if not domain and request_domain_mode == KnownDomain.MODE_ORG_ALT_DOMAIN:
path = request.get_full_path().split("/", 2)[-1]
r = redirect_to_url(build_absolute_uri(request.event, "presale:event.index") + path)
r['Access-Control-Allow-Origin'] = '*'
return r
elif domain and domain != request.host:
if request.port and request.port not in (80, 443):
domain = '%s:%d' % (domain, request.port)
path = request.get_full_path().split("/", 2)[-1]
if domainmode == KnownDomain.MODE_EVENT_DOMAIN:
path = request.get_full_path().split("/", 2)[-1]
else:
path = request.get_full_path()
r = redirect_to_url(urljoin('%s://%s' % (request.scheme, domain), path))
r['Access-Control-Allow-Origin'] = '*'
return r
@@ -299,11 +309,14 @@ def _detect_event(request, require_live=True, require_plugin=None):
request.organizer = request.event.organizer
# If this event has a custom domain, send the user there
domain = get_event_domain(request.event)
domain, domainmode = get_event_domain(request.event, fallback=False, return_mode=True)
if domain:
if request.port and request.port not in (80, 443):
domain = '%s:%d' % (domain, request.port)
path = request.get_full_path().split("/", 3)[-1]
if domainmode == KnownDomain.MODE_EVENT_DOMAIN:
path = request.get_full_path().split("/", 3)[-1]
else:
path = request.get_full_path().split("/", 2)[-1]
r = redirect_to_url(urljoin('%s://%s' % (request.scheme, domain), path))
r['Access-Control-Allow-Origin'] = '*'
return r
@@ -377,6 +390,7 @@ def _detect_event(request, require_live=True, require_plugin=None):
except Event.DoesNotExist:
try:
if hasattr(request, 'organizer_domain'):
# Redirect for case-insensitive event slug
event = request.organizer.events.get(
slug__iexact=url.kwargs['event'],
organizer=request.organizer,
@@ -388,6 +402,7 @@ def _detect_event(request, require_live=True, require_plugin=None):
return r
else:
if 'event' in url.kwargs and 'organizer' in url.kwargs:
# Redirect for case-insensitive event or organizer slug
event = Event.objects.select_related('organizer').get(
slug__iexact=url.kwargs['event'],
organizer__slug__iexact=url.kwargs['organizer']
@@ -403,6 +418,7 @@ def _detect_event(request, require_live=True, require_plugin=None):
raise Http404(_('The selected event was not found.'))
except Organizer.DoesNotExist:
if 'organizer' in url.kwargs:
# Redirect for case-insensitive organizer slug
try:
organizer = Organizer.objects.get(
slug__iexact=url.kwargs['organizer']

View File

@@ -251,7 +251,8 @@ class CartMixin:
'seconds_left': seconds_left,
'first_expiry': first_expiry,
'is_ordered': bool(order),
'itemcount': sum(c.count for c in positions if not c.addon_to)
'itemcount': sum(c.count for c in positions if not c.addon_to),
'current_selected_payments': [p for p in self.current_selected_payments(total) if p.get('multi_use_supported')]
}
def current_selected_payments(self, total, warn=False, total_includes_payment_fees=False):

View File

@@ -42,6 +42,7 @@ from urllib.parse import quote
from django.conf import settings
from django.contrib import messages
from django.core.cache import caches
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.http import FileResponse, Http404, JsonResponse
from django.shortcuts import get_object_or_404, render
@@ -57,7 +58,7 @@ from django.views.generic import TemplateView, View
from django_scopes import scopes_disabled
from pretix.base.models import (
CartPosition, InvoiceAddress, QuestionAnswer, SubEvent, Voucher,
CartPosition, GiftCard, InvoiceAddress, QuestionAnswer, SubEvent, Voucher,
)
from pretix.base.services.cart import (
CartError, add_items_to_cart, apply_voucher, clear_cart, error_messages,
@@ -438,8 +439,48 @@ class CartApplyVoucher(EventViewMixin, CartActionMixin, AsyncAction, View):
return _('We applied the voucher to as many products in your cart as we could.')
def post(self, request, *args, **kwargs):
from pretix.base.payment import GiftCardPayment
if 'voucher' in request.POST:
return self.do(self.request.event.id, request.POST.get('voucher'), get_or_create_cart_id(self.request),
code = request.POST.get('voucher').strip()
if not self.request.event.vouchers.filter(code__iexact=code):
try:
gc = self.request.event.organizer.accepted_gift_cards.get(secret=code)
gcp = GiftCardPayment(self.request.event)
if not gcp.is_enabled or not gcp.is_allowed(self.request, Decimal("1.00")):
raise ValidationError(error_messages['voucher_invalid'])
else:
cs = cart_session(request)
gcp._add_giftcard_to_cart(cs, gc)
messages.success(
request,
_("The gift card has been saved to your cart. Please continue your checkout.")
)
if "ajax" in self.request.POST or "ajax" in self.request.GET:
return JsonResponse({
'ready': True,
'success': True,
'redirect': self.get_success_url(),
'message': str(
_("The gift card has been saved to your cart. Please continue your checkout.")
)
})
return redirect_to_url(self.get_success_url())
except GiftCard.DoesNotExist:
pass
except ValidationError as e:
messages.error(self.request, str(e.message))
if "ajax" in self.request.POST or "ajax" in self.request.GET:
return JsonResponse({
'ready': True,
'success': False,
'redirect': self.get_success_url(),
'message': str(e.message)
})
return redirect_to_url(self.get_error_url())
return self.do(self.request.event.id, code, get_or_create_cart_id(self.request),
translation.get_language(), request.sales_channel.identifier,
time_machine_now(default=None))
else:
@@ -631,6 +672,8 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView
return context
def dispatch(self, request, *args, **kwargs):
from pretix.base.payment import GiftCardPayment
err = None
v = request.GET.get('voucher')
@@ -653,10 +696,24 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView
if v_avail < 1 and not err:
err = error_messages['voucher_redeemed_cart'] % self.request.event.settings.reservation_time
except Voucher.DoesNotExist:
if self.request.event.organizer.accepted_gift_cards.filter(secret__iexact=request.GET.get("voucher")).exists():
err = error_messages['gift_card']
else:
try:
gc = self.request.event.organizer.accepted_gift_cards.get(secret=v.strip())
gcp = GiftCardPayment(self.request.event)
if not gcp.is_enabled or not gcp.is_allowed(self.request, Decimal("1.00")):
err = error_messages['voucher_invalid']
else:
cs = cart_session(request)
gcp._add_giftcard_to_cart(cs, gc)
messages.success(
request,
_("The gift card has been saved to your cart. Please now select the products "
"you want to purchase.")
)
return redirect_to_url(self.get_next_url())
except GiftCard.DoesNotExist:
err = error_messages['voucher_invalid']
except ValidationError as e:
err = str(e.message)
else:
context = {}
context['cart'] = self.get_cart()

View File

@@ -77,7 +77,7 @@ class RedirectBackMixin:
self.redirect_field_name,
self.request.GET.get(self.redirect_field_name, '')
)
hosts = list(KnownDomain.objects.filter(event__organizer=self.request.organizer).values_list('domainname', flat=True))
hosts = list(KnownDomain.objects.filter(organizer=self.request.organizer).values_list('domainname', flat=True))
siteurlsplit = urlsplit(settings.SITE_URL)
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
hosts = ['%s:%d' % (h, siteurlsplit.port) for h in hosts]
@@ -134,7 +134,7 @@ class LoginView(RedirectBackMixin, FormView):
url = self.get_redirect_url()
if not url:
return eventreverse(self.request.organizer, 'presale:organizer.customer.profile', kwargs={})
return eventreverse(self.request.organizer, 'presale:organizer.customer.index', kwargs={})
if self.request.GET.get("request_cross_domain_customer_auth") == "true":
otpstore = SessionStore()
@@ -168,7 +168,7 @@ class LogoutView(View):
return HttpResponseRedirect(next_page)
def get_next_page(self):
if getattr(self.request, 'event_domain', False):
if getattr(self.request, 'domain_mode', 'system') in (KnownDomain.MODE_ORG_ALT_DOMAIN, KnownDomain.MODE_EVENT_DOMAIN):
# After we cleared the cookies on this domain, redirect to the parent domain to clear cookies as well
next_page = eventreverse(self.request.organizer, 'presale:organizer.customer.logout', kwargs={})
if self.redirect_field_name in self.request.POST or self.redirect_field_name in self.request.GET:
@@ -188,7 +188,7 @@ class LogoutView(View):
self.redirect_field_name,
self.request.GET.get(self.redirect_field_name)
)
hosts = list(KnownDomain.objects.filter(event__organizer=self.request.organizer).values_list('domainname', flat=True))
hosts = list(KnownDomain.objects.filter(organizer=self.request.organizer).values_list('domainname', flat=True))
siteurlsplit = urlsplit(settings.SITE_URL)
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
hosts = ['%s:%d' % (h, siteurlsplit.port) for h in hosts]
@@ -350,8 +350,42 @@ class CustomerRequiredMixin:
return super().dispatch(request, *args, **kwargs)
class ProfileView(CustomerRequiredMixin, ListView):
template_name = 'pretixpresale/organizers/customer_profile.html'
class CustomerAccountBaseMixin(CustomerRequiredMixin):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['customer'] = self.request.customer
url_name = self.request.resolver_match.url_name
ctx['sub_nav'] = [
{
'label': _('Orders'),
'url': eventreverse(self.request.organizer, 'presale:organizer.customer.index', kwargs={}),
'active': url_name == 'organizer.customer.index',
'icon': 'shopping-cart',
},
{
'label': _('Memberships'),
'url': eventreverse(self.request.organizer, 'presale:organizer.customer.memberships', kwargs={}),
'active': url_name.startswith('organizer.customer.membership'),
'icon': 'id-badge',
},
{
'label': _('Addresses'),
'url': eventreverse(self.request.organizer, 'presale:organizer.customer.addresses', kwargs={}),
'active': url_name.startswith('organizer.customer.address'),
'icon': 'address-card-o',
},
{
'label': _('Attendee profiles'),
'url': eventreverse(self.request.organizer, 'presale:organizer.customer.profiles', kwargs={}),
'active': url_name.startswith('organizer.customer.profile'),
'icon': 'user',
},
]
return ctx
class OrderView(CustomerAccountBaseMixin, ListView):
template_name = 'pretixpresale/organizers/customer_orders.html'
context_object_name = 'orders'
paginate_by = 20
@@ -369,18 +403,6 @@ class ProfileView(CustomerRequiredMixin, ListView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['customer'] = self.request.customer
ctx['memberships'] = self.request.customer.memberships.with_usages().select_related(
'membership_type', 'granted_in', 'granted_in__order', 'granted_in__order__event'
)
ctx['invoice_addresses'] = InvoiceAddress.profiles.filter(customer=self.request.customer)
ctx['is_paginated'] = True
for m in ctx['memberships']:
if m.membership_type.max_usages:
m.percent = int(m.usages / m.membership_type.max_usages * 100)
else:
m.percent = 0
s = OrderPosition.objects.filter(
order=OuterRef('pk')
@@ -404,7 +426,18 @@ class ProfileView(CustomerRequiredMixin, ListView):
return ctx
class MembershipUsageView(CustomerRequiredMixin, ListView):
class MembershipView(CustomerAccountBaseMixin, ListView):
template_name = 'pretixpresale/organizers/customer_memberships.html'
context_object_name = 'memberships'
paginate_by = 20
def get_queryset(self):
return self.request.customer.memberships.with_usages().select_related(
'membership_type', 'granted_in', 'granted_in__order', 'granted_in__order__event'
)
class MembershipUsageView(CustomerAccountBaseMixin, ListView):
template_name = 'pretixpresale/organizers/customer_membership.html'
context_object_name = 'usages'
paginate_by = 20
@@ -428,7 +461,16 @@ class MembershipUsageView(CustomerRequiredMixin, ListView):
return ctx
class AddressDeleteView(CustomerRequiredMixin, CompatDeleteView):
class AddressView(CustomerAccountBaseMixin, ListView):
template_name = 'pretixpresale/organizers/customer_addresses.html'
context_object_name = 'invoice_addresses'
paginate_by = 20
def get_queryset(self):
return InvoiceAddress.profiles.filter(customer=self.request.customer)
class AddressDeleteView(CustomerAccountBaseMixin, CompatDeleteView):
template_name = 'pretixpresale/organizers/customer_address_delete.html'
context_object_name = 'address'
@@ -436,10 +478,19 @@ class AddressDeleteView(CustomerRequiredMixin, CompatDeleteView):
return get_object_or_404(InvoiceAddress.profiles, customer=self.request.customer, pk=self.kwargs.get('id'))
def get_success_url(self):
return eventreverse(self.request.organizer, 'presale:organizer.customer.profile', kwargs={})
return eventreverse(self.request.organizer, 'presale:organizer.customer.addresses', kwargs={})
class ProfileDeleteView(CustomerRequiredMixin, CompatDeleteView):
class ProfileView(CustomerAccountBaseMixin, ListView):
template_name = 'pretixpresale/organizers/customer_profiles.html'
context_object_name = 'attendee_profiles'
paginate_by = 20
def get_queryset(self):
return self.request.customer.attendee_profiles.all()
class ProfileDeleteView(CustomerAccountBaseMixin, CompatDeleteView):
template_name = 'pretixpresale/organizers/customer_profile_delete.html'
context_object_name = 'profile'
@@ -447,10 +498,10 @@ class ProfileDeleteView(CustomerRequiredMixin, CompatDeleteView):
return get_object_or_404(self.request.customer.attendee_profiles, pk=self.kwargs.get('id'))
def get_success_url(self):
return eventreverse(self.request.organizer, 'presale:organizer.customer.profile', kwargs={})
return eventreverse(self.request.organizer, 'presale:organizer.customer.profiles', kwargs={})
class ChangePasswordView(CustomerRequiredMixin, FormView):
class ChangePasswordView(CustomerAccountBaseMixin, FormView):
template_name = 'pretixpresale/organizers/customer_password.html'
form_class = ChangePasswordForm
@@ -465,7 +516,7 @@ class ChangePasswordView(CustomerRequiredMixin, FormView):
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return eventreverse(self.request.organizer, 'presale:organizer.customer.profile', kwargs={})
return eventreverse(self.request.organizer, 'presale:organizer.customer.index', kwargs={})
@transaction.atomic()
def form_valid(self, form):
@@ -483,7 +534,7 @@ class ChangePasswordView(CustomerRequiredMixin, FormView):
return kwargs
class ChangeInformationView(CustomerRequiredMixin, FormView):
class ChangeInformationView(CustomerAccountBaseMixin, FormView):
template_name = 'pretixpresale/organizers/customer_info.html'
form_class = ChangeInfoForm
@@ -498,7 +549,7 @@ class ChangeInformationView(CustomerRequiredMixin, FormView):
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return eventreverse(self.request.organizer, 'presale:organizer.customer.profile', kwargs={})
return eventreverse(self.request.organizer, 'presale:organizer.customer.index', kwargs={})
def form_valid(self, form):
if form.cleaned_data['email'] != self.initial_email and not self.request.customer.provider:
@@ -581,7 +632,7 @@ class ConfirmChangeView(View):
return HttpResponseRedirect(self.get_success_url())
def get_success_url(self):
return eventreverse(self.request.organizer, 'presale:organizer.customer.profile', kwargs={})
return eventreverse(self.request.organizer, 'presale:organizer.customer.index', kwargs={})
class SSOLoginView(RedirectBackMixin, View):
@@ -641,7 +692,7 @@ class SSOLoginView(RedirectBackMixin, View):
url = self.get_redirect_url()
if not url:
return eventreverse(self.request.organizer, 'presale:organizer.customer.profile', kwargs={})
return eventreverse(self.request.organizer, 'presale:organizer.customer.index', kwargs={})
return url
@@ -864,7 +915,7 @@ class SSOLoginReturnView(RedirectBackMixin, View):
url = self.get_redirect_url(redirect_to)
if not url:
return eventreverse(self.request.organizer, 'presale:organizer.customer.profile', kwargs={})
return eventreverse(self.request.organizer, 'presale:organizer.customer.index', kwargs={})
else:
if self.request.session.get(f'pretix_customerauth_{self.provider.pk}_cross_domain_requested'):
otpstore = SessionStore()

View File

@@ -632,7 +632,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
context['subevent_list_cache_key'] = self._subevent_list_cachekey()
context['show_cart'] = (
context['cart']['positions'] and (
(context['cart']['positions'] or context['cart'].get('current_selected_payments')) and (
self.request.event.has_subevents or self.request.event.presale_is_running
)
)

View File

@@ -71,7 +71,7 @@ from pretix.helpers.formats.en.formats import (
)
from pretix.helpers.http import redirect_to_url
from pretix.helpers.thumb import get_thumbnail
from pretix.multidomain.urlreverse import eventreverse
from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse
from pretix.presale.forms.organizer import EventListFilterForm
from pretix.presale.ical import get_public_ical
from pretix.presale.views import OrganizerViewMixin
@@ -1305,3 +1305,8 @@ class OrganizerFavicon(View):
return redirect_to_url(get_thumbnail(icon_file, '32x32^', formats=settings.PILLOW_FORMATS_QUESTIONS_FAVICON).thumb.url)
else:
return redirect_to_url(static("pretixbase/img/favicon.ico"))
class RedirectToOrganizerIndex(View):
def get(self, *args, **kwargs):
return redirect_to_url(build_absolute_uri(self.request.organizer, "presale:organizer.index"))

View File

@@ -435,14 +435,6 @@ REST_FRAMEWORK = {
}
CORE_MODULES = {
"pretix.base",
"pretix.presale",
"pretix.control",
"pretix.plugins.checkinlists",
"pretix.plugins.reports",
}
MIDDLEWARE = [
'pretix.helpers.logs.RequestIdMiddleware',
'pretix.api.middleware.IdempotencyMiddleware',

View File

@@ -275,12 +275,12 @@
}
// Apply the mixin to the panel headings only
.panel-default > .panel-heading { @include panel-heading-styles($panel-default-heading-bg); }
.panel-primary > .panel-heading { @include panel-heading-styles($panel-primary-heading-bg); }
.panel-success > .panel-heading { @include panel-heading-styles($panel-success-heading-bg); }
.panel-info > .panel-heading { @include panel-heading-styles($panel-info-heading-bg); }
.panel-warning > .panel-heading { @include panel-heading-styles($panel-warning-heading-bg); }
.panel-danger > .panel-heading { @include panel-heading-styles($panel-danger-heading-bg); }
.panel-default > .panel-heading, .panel-default > legend > .panel-heading { @include panel-heading-styles($panel-default-heading-bg); }
.panel-primary > .panel-heading, .panel-primary > legend > .panel-heading { @include panel-heading-styles($panel-primary-heading-bg); }
.panel-success > .panel-heading, .panel-success > legend > .panel-heading { @include panel-heading-styles($panel-success-heading-bg); }
.panel-info > .panel-heading, .panel-info > legend > .panel-heading { @include panel-heading-styles($panel-info-heading-bg); }
.panel-warning > .panel-heading, .panel-warning > legend > .panel-heading { @include panel-heading-styles($panel-warning-heading-bg); }
.panel-danger > .panel-heading, .panel-danger > legend > .panel-heading { @include panel-heading-styles($panel-danger-heading-bg); }
//

View File

@@ -3,7 +3,7 @@
@mixin panel-variant($border, $heading-text-color, $heading-bg-color, $heading-border) {
border-color: $border;
& > .panel-heading {
& > .panel-heading, & > legend > .panel-heading {
color: $heading-text-color;
background-color: $heading-bg-color;
border-color: $heading-border;

View File

@@ -0,0 +1,64 @@
$(function () {
"use strict";
$("select[data-country-information-url]").each(function () {
let xhr;
const dependency = $(this),
loader = $("<span class='fa fa-cog fa-spin'></span>").hide().prependTo(dependency.closest(".form-group").find("label")),
url = this.getAttribute('data-country-information-url'),
form = dependency.closest(".panel-body, form, .profile-scope"),
isRequired = dependency.closest(".form-group").is(".required"),
dependents = {
'city': form.find("input[name$=city]"),
'zipcode': form.find("input[name$=zipcode]"),
'street': form.find("textarea[name$=street]"),
'state': form.find("select[name$=state]"),
'vat_id': form.find("input[name$=vat_id]"),
},
update = function (ev) {
if (xhr) {
xhr.abort();
}
for (var k in dependents) dependents[k].prop("disabled", true);
loader.show();
xhr = $.getJSON(url + '?country=' + dependency.val(), function (data) {
var selected_value = dependents.state.prop("data-selected-value");
if (selected_value) dependents.state.prop("data-selected-value", "");
dependents.state.find("option:not([value=''])").remove();
if (data.data.length > 0) {
$.each(data.data, function (k, s) {
var o = $("<option>").attr("value", s.code).text(s.name);
if (selected_value == s.code) o.prop("selected", true);
dependents.state.append(o);
});
}
for(var k in dependents) {
const options = data[k],
dependent = dependents[k],
visible = 'visible' in options ? options.visible : true,
required = 'required' in options && options.required && isRequired && visible;
dependent.closest(".form-group").toggle(visible).toggleClass('required', required);
dependent.prop("required", required);
}
for (var k in dependents) dependents[k].prop("disabled", false);
}).always(function() {
loader.hide();
}).fail(function(){
// In case of errors, show everything and require nothing, we can still handle errors in backend
for(var k in dependents) {
const dependent = dependents[k],
visible = true,
required = false;
dependent.closest(".form-group").toggle(visible).toggleClass('required', required);
dependent.prop("required", required);
}
});
};
dependents.state.prop("data-selected-value", dependents.state.val());
update();
dependency.on("change", update);
});
});

View File

@@ -8,7 +8,7 @@ $gray-lightest: lighten(#000, 97.25%);
$font-family-sans-serif: var(--pretix-font-family-sans-serif);
$text-color: #222222 !default;
$text-muted: #767676 !default;
$text-muted: #737373 !default;
$input-color-placeholder: lighten(#000, 70%) !default;
$border-radius-base: var(--pretix-border-radius-base);

View File

@@ -53,7 +53,9 @@ $in-border-radius-small: 2px !default;
--pretix-brand-primary-darken-17: #{darken($in-brand-primary, 17%)};
--pretix-brand-primary-darken-20: #{darken($in-brand-primary, 20%)};
--pretix-brand-primary-darken-30: #{darken($in-brand-primary, 30%)};
--pretix-brand-primary-tint-90: #{tint($in-brand-primary, 90%)};
--pretix-brand-primary-shade-25: #{shade($in-brand-primary, 25%)};
--pretix-brand-primary-shade-42: #{shade($in-brand-primary, 42%)};
--pretix-brand-primary-lighten-28-saturate-20: #{saturate(lighten($in-brand-primary, 28%), 20%)};
--pretix-brand-primary-lighten-23-saturate-2: #{saturate(lighten($in-brand-primary, 23%), 2%)};

View File

@@ -434,60 +434,6 @@ var form_handlers = function (el) {
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("dp.change", update);
});
$("input[name$=vat_id][data-countries-with-vat-id]").each(function () {
var dependent = $(this),
dependency_country = $(this).closest(".panel-body, form").find('select[name$=country]'),
dependency_id_is_business_1 = $(this).closest(".panel-body, form").find('input[id$=id_is_business_1]'),
update = function (ev) {
if (dependency_id_is_business_1.length && !dependency_id_is_business_1.prop("checked")) {
dependent.closest(".form-group").hide();
} else if (dependent.attr('data-countries-with-vat-id').split(',').includes(dependency_country.val())) {
dependent.closest(".form-group").show();
} else {
dependent.closest(".form-group").hide();
}
};
update();
dependency_country.on("change", update);
dependency_id_is_business_1.on("change", update);
});
$("select[name$=state]:not([data-static])").each(function () {
var dependent = $(this),
counter = 0,
dependency = $(this).closest(".panel-body, form").find('select[name$=country]'),
update = function (ev) {
counter++;
var curCounter = counter;
dependent.prop("disabled", true);
dependency.closest(".form-group").find("label").prepend("<span class='fa fa-cog fa-spin'></span> ");
$.getJSON('/js_helpers/states/?country=' + dependency.val(), function (data) {
if (counter > curCounter) {
return; // Lost race
}
dependent.find("option").filter(function (t) {return !!$(this).attr("value")}).remove();
if (data.data.length > 0) {
$.each(data.data, function (k, s) {
dependent.append($("<option>").attr("value", s.code).text(s.name));
});
dependent.closest(".form-group").show();
dependent.prop('required', dependency.prop("required"));
} else {
dependent.closest(".form-group").hide();
dependent.prop("required", false);
}
dependent.prop("disabled", false);
dependency.closest(".form-group").find("label .fa-spin").remove();
});
};
if (dependent.find("option").length === 1) {
dependent.closest(".form-group").hide();
} else {
dependent.prop('required', dependency.prop("required"));
}
dependency.on("change", update);
});
el.find("div.scrolling-choice:not(.no-search)").each(function () {
if ($(this).find("input[type=text]").length > 0) {
return;

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