Improved and documented i18n and background tasks

This commit is contained in:
Raphael Michel
2016-05-29 20:02:31 +02:00
parent 8369ad291e
commit ead7d8ed78
12 changed files with 273 additions and 101 deletions
@@ -0,0 +1,96 @@
Background tasks
================
pretix provides the ability to run all longer-running tasks like generating ticket files or sending emails
in a background thread instead of the webserver process. We use the well-established `Celery`_ project to
implement this. However, as celery requires running a task queue like RabbitMQ and a result storage such as
Redis to work efficiently, we don't like to *depend* on celery being available to make small-scale installations
of pretix more straightforward. For this reason, the "background" in "background task" is always optional.
The Django settings variable ``settings.HAS_CELERY`` provides information on whether celery is configured
in the current installation.
Implementing a task
-------------------
A common pattern for implementing "optionally-asynchronous" tasks that can be seen a lot in ``pretix.base.services``
looks like this::
def my_task(argument1, argument2):
# Important: All arguments and return values need to be serializable into JSON.
# Do not use model instances, use their primary keys instead!
pass # do your work here
if settings.HAS_CELERY:
# Transform this into a background task
from pretix.celery import app # Important: Do not import this unconditionally!
my_task_async = app.task(export)
def my_task(*args, **kwargs):
my_task_async.apply_async(args=args, kwargs=kwargs)
This explicit declaration method also allows you to place some custom retry logic etc. in the asynchronous version.
Tasks in the request-response flow
----------------------------------
If your user needs to wait for the response of the asynchronous task, there are helpers available in ``pretix.presale``
that will probably move to ``pretix.base`` at some point. They consist of the view mixin ``AsyncAction`` that allows
you to easily write a view that kicks off and waits for an asynchronous task. ``AsyncAction`` will determine whether
to run the task asynchronously or not and will do some magic to look nice for users with and without JavaScript support.
A usage example taken directly from the code is::
class OrderCancelDo(EventViewMixin, OrderDetailMixin, AsyncAction, View):
"""
A view that executes a task asynchronously. A POST request will kick of the
task into the background or run it in the foreground, if celery is not installed.
In the former case, subsequent GET calls can be used to determinine the current
status of the task.
"""
task = cancel_order # The task to be used, defined like above
def get_success_url(self, value):
"""
Returns the URL the user will be redirected to if the task succeeded.
"""
return self.get_order_url()
def get_error_url(self):
"""
Returns the URL the user will be redirected to if the task failed.
"""
return self.get_order_url()
def post(self, request, *args, **kwargs):
"""
Will be called while handling a POST request. This should process the
request arguments in some way and call ``self.do`` with the task arguments
to kick of the task.
"""
if not self.order:
raise Http404(_('Unknown order code or not authorized to access this order.'))
return self.do(self.order.pk)
def get_error_message(self, exception):
"""
Returns the message that will be shown to the user if the task has failed.
"""
if isinstance(exception, dict) and exception['exc_type'] == 'OrderError':
return gettext(exception['exc_message'])
elif isinstance(exception, OrderError):
return str(exception)
return super().get_error_message(exception)
On the client side, this can be used by simply adding a ``data-asynctask`` attribute to an HTML form. This will enable
AJAX sending of the form and display a loading indicator::
<form method="post" data-asynctask
action="{% eventurl request.event "presale:event.order.cancel.do" … %}">
{% csrf_token %}
...
</form>
.. _Celery: http://www.celeryproject.org/
+72
View File
@@ -0,0 +1,72 @@
Internationalization
====================
One of pretix' major selling points is it's multi-language capability. We make heavy use of Django's
`translation features`_ that are built upon `GNU gettext`_. However, Django does not provide a standard
way to translate *user-generated content*. In our case, we need to translate strings like product names
or event descriptions, so we need event organizers to be able to fill in all fields in multiple languages
at the same time.
.. note:: Implementing object-level translation in a relational database is a task that requires some difficult
trade-off. We decided for a design that is not elegant on the database level (as it violates the `1NF`_) and
makes searching in the respective database fields very hard, but allows for a simple design on the ORM level
and adds only minimal performance overhead.
All classes and functions introduced in this document are located in ``pretix.base.i18n`` if not stated otherwise.
Database storage
----------------
pretix provides two custom model field types that allow you to work with localized strings: ``I18nCharField`` and
``I18nTextField``. Both of them are stored in the database as a ``TextField`` internally, they only differ in the
default form widget that is used by ``ModelForm``.
Yes, we know that this has negative impact on performance when indexing or searching them, but as mentioned above,
within pretix this is not used in places that need to be searched. Lookups are currently not even implemented on these
fields. In the database, the strings will be stored as a JSON-encoded mapping of language codes to strings.
Whenever you interact with those fields, you will either provide or receive an instance of the following class:
.. autoclass:: pretix.base.i18n.LazyI18nString
:members: __init__, localize, __str__
Forms
-----
We provide i18n-aware versions of the respective form fields and widgets: ``I18nFormField`` with the ``I18nTextInput``
and ``I18nTextarea`` widgets. They transparently allow you to use ``LazyI18nString`` values in forms and render text
inputs for multiple languages.
.. autoclass:: pretix.base.i18n.I18nFormField
To easily limit the displayed languages to the languages relevant to an event, there is a custom ``ModelForm`` subclass
that deals with this for you:
.. autoclass:: pretix.base.forms.I18nModelForm
There are equivalents for ``BaseModelFormSet`` and ``BaseInlineFormSet``:
.. autoclass:: pretix.base.forms.I18nFormSet
.. autoclass:: pretix.base.forms.I18nInlineFormSet
Useful utilities
----------------
The ``i18n`` module contains a few more useful utilities, starting with simple lazy-evaluation wrappers for formatted
numbers and dates, ``LazyDate`` and ``LazyNumber``. There also is a ``LazyLocaleException`` base class that provides
exceptions with gettext-localized exception messages.
Last, but definitely not least, we have the ``language`` context manager that allows you to execute a piece of code with
a different locale::
with language('de'):
render_mail_template()
This is very useful e.g. when sending an email to a user that has a different language than the user performing the
action that causes the mail to be sent.
.. _translation features: https://docs.djangoproject.com/en/1.9/topics/i18n/translation/
.. _GNU gettext: https://www.gnu.org/software/gettext/
.. _1NF: https://en.wikipedia.org/wiki/First_normal_form
+16
View File
@@ -0,0 +1,16 @@
Implementation and Utilities
============================
This chapter describes the various inner workings that power pretix, most of them living in ``pretix.base``.
If you want to develop around pretix' core or advanced plugins, this aims to describe everything you absolutely
need to know.
Contents:
.. toctree::
:maxdepth: 2
models
background
urlconfig
i18n
+90
View File
@@ -0,0 +1,90 @@
.. highlight:: python
:linenothreshold: 5
Data model
==========
pretix provides the following data(base) models. Every model and every model method or field that is not
documented here is considered private and should not be used by third-party plugins, as it may change
without advance notice.
User model
----------
.. autoclass:: pretix.base.models.User
:members:
Organizers and events
---------------------
.. autoclass:: pretix.base.models.Organizer
:members:
.. autoclass:: pretix.base.models.OrganizerPermission
:members:
.. autoclass:: pretix.base.models.Event
:members:
.. autoclass:: pretix.base.models.EventPermission
:members:
Items
-----
.. autoclass:: pretix.base.models.Item
:members:
.. autoclass:: pretix.base.models.ItemCategory
:members:
.. autoclass:: pretix.base.models.ItemVariation
:members:
.. autoclass:: pretix.base.models.Question
:members:
.. autoclass:: pretix.base.models.Quota
:members:
Carts and Orders
----------------
.. autoclass:: pretix.base.models.Order
:members:
.. autoclass:: pretix.base.models.AbstractPosition
:members:
.. autoclass:: pretix.base.models.OrderPosition
:members:
.. autoclass:: pretix.base.models.CartPosition
:members:
.. autoclass:: pretix.base.models.QuestionAnswer
:members:
Logging
-------
.. autoclass:: pretix.base.models.LogEntry
:members:
Invoicing
---------
.. autoclass:: pretix.base.models.Invoice
:members:
.. autoclass:: pretix.base.models.InvoiceLine
:members:
Vouchers
--------
.. autoclass:: pretix.base.models.Voucher
:members:
.. _cleanerversion: https://github.com/swisscom/cleanerversion
@@ -0,0 +1,90 @@
.. _`urlconf`:
Working with URLs
=================
As soon as you write a plugin that provides a new view to the user (or if you want to
contribute to pretix itself), you need to understand how URLs work in pretix as it slightly
differs from the standard Django system.
The reason for the complicated URL handling is that pretix supports custom subdomains for
single organizers. In this example we will use an event organizer with the slug ``bigorg``
that manages an awesome conference with the slug ``awesomecon``. If pretix is installed
on pretix.eu, this event is available by default at ``https://pretix.eu/bigorg/awesomecon/``
and the admin panel is available at ``https://pretix.eu/control/event/bigorg/awesomecon/``.
If the organizer now configures a custom domain like ``tickets.bigorg.com``, his event will
from now on be available on ``https://tickets.bigorg.com/awesomecon/``. The former URL at
``pretix.eu`` will redirect there. However, the admin panel will still only be available
on ``pretix.eu`` for convenience and security reasons.
URL routing
-----------
The hard part about implementing this URL routing in Django is that
``https://pretix.eu/bigorg/awesomecon/`` contains two parameters of nearly arbitrary content
and ``https://tickets.bigorg.com/awesomecon/`` contains only one. The only robust way to do
this is by having *seperate* URL configuration for those two cases. In pretix, we call the
former our ``maindomain`` config and the latter our ``subdomain`` config. For pretix' core
modules we do some magic to avoid duplicate configuration, but for a fairly simple plugin with
only a handful of routes, we recommend just configuring the two URL sets seperately.
The file ``maindomain_urls.py`` inside your plugin package will be loaded and scanned for
URL configuration automatically and should be provided by any plugin that provides any view.
A very basic example that provides one view in the admin panel and one view in the frontend
could look like this::
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/mypluginname/',
views.AdminView.as_view(), name='backend'),
url(r'^(?P<organizer>[^/]+)/(?P<event>[^/]+)/mypluginname/',
views.FrontendView.as_view(), name='frontend'),
]
A matching configuration for custom domains will be expected in the ``subdomain_urls.py`` file
of your package and would look like this::
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^(?P<event>[^/]+)/mypluginname/',
views.FrontendView.as_view(), name='frontend'),
]
If you only provide URLs in the admin area, you do not need to provide a ``subdomain_urls`` module.
URL reversal
------------
pretix uses Django's URL namespacing feature. The URLs of pretix' core are available in the ``control``
and ``presale`` namespaces, there are only very few URLs in the root namespace. Your plugin's URLs will
be available in the ``plugins:<applabel>`` namespace, e.g. the form of the email sending plugin is
available as ``plugins:sendmail:send``.
Generating an URL for the frontend is a complicated task, because you need to know whether the event's
organizer uses a custom URL or not and then generate the URL with a different domain and different
arguments based on this information. pretix provides some helpers to make this easier. The first helper
is a python method that emulates a behaviour similar to ``reverse``:
.. autofunction:: pretix.multidomain.urlreverse.eventreverse
In addition, there is a template tag that works similar to ``url`` but takes an event or organizer object
as its first argument and can be used like this::
{% load eventurl %}
<a href="{% eventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
Implementation details
----------------------
There are some other caveats when using a design like this, e.g. you have to care about cookie domains
and referer verification yourself. If you want to see how we built this, look into the ``pretix/multidomain/``
sub-tree.