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
+3
View File
@@ -29,6 +29,9 @@ on the type of navigation. You should also return an ``active`` key with a boole
set to ``True``, when this item should be marked as active. The ``request`` object
will have an attribute ``event``.
If you use this, you should read the documentation on :ref:`how to deal with URLs <urlconf>`
in pretix.
``pretix.control.signals.nav_event``
The sidebar navigation when the admin has selected an event.
+2 -2
View File
@@ -1,5 +1,5 @@
Plugin API
==========
Plugin hooks
============
Contents:
@@ -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
@@ -1,10 +1,10 @@
.. highlight:: python
:linenothreshold: 5
Data models
===========
Data model
==========
Pretix provides the following data(base) models. Every model and every model method or field that is not
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.
+1 -2
View File
@@ -10,9 +10,8 @@ Contents:
setup
structure
contribution/index
models
implementation/index
api/index
urlconfig
.. TODO::
Document settings objects, ItemVariation objects, form fields.