From e4b1cf8d6fcd4fa18a8a13a928e8d3b676bdcb44 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Sun, 29 May 2016 20:34:03 +0200 Subject: [PATCH] Document setting storage and mail sending --- doc/development/implementation/email.rst | 7 +++ doc/development/implementation/index.rst | 4 +- doc/development/implementation/settings.rst | 52 +++++++++++++++++++++ src/pretix/base/forms/__init__.py | 10 +++- src/pretix/base/services/mail.py | 33 +++++++------ src/pretix/base/settings.py | 44 ++++++++++++----- 6 files changed, 122 insertions(+), 28 deletions(-) create mode 100644 doc/development/implementation/email.rst create mode 100644 doc/development/implementation/settings.rst diff --git a/doc/development/implementation/email.rst b/doc/development/implementation/email.rst new file mode 100644 index 0000000000..46447b2386 --- /dev/null +++ b/doc/development/implementation/email.rst @@ -0,0 +1,7 @@ +Sending Email +============= + +As pretix allows event organizers to configure how they want to sent emails to their users in multiple ways. +Therefore, all emails should be sent throught the following function: + +.. autofunction:: pretix.base.services.mail.mail \ No newline at end of file diff --git a/doc/development/implementation/index.rst b/doc/development/implementation/index.rst index 8b41ec040a..8220205cde 100644 --- a/doc/development/implementation/index.rst +++ b/doc/development/implementation/index.rst @@ -11,6 +11,8 @@ Contents: :maxdepth: 2 models - background urlconfig i18n + settings + background + email diff --git a/doc/development/implementation/settings.rst b/doc/development/implementation/settings.rst new file mode 100644 index 0000000000..7ce8d42776 --- /dev/null +++ b/doc/development/implementation/settings.rst @@ -0,0 +1,52 @@ +Settings storage +================ + +pretix is highly configurable and therefore needs to store a lot of per-event and per-organizer settings. +Those settings are stored in the database and accessed through a ``SettingsProxy`` instance. You can obtain +such an instance from any event or organizer model instance by just accessing ``event.settings``. + +Any setting consists of a key and a value. By default, all settings are strings, but the settings system +includes serializers for serializing the following types: + +* Built-in types: ``int``, ``float``, ``decimal.Decimal``, ``dict``, ``list``, ``bool`` +* ``datetime.date``, ``datetime.datetime``, ``datetime.time`` +* ``LazyI18nString`` +* References to Django ``File`` objects that are already stored in a storage backend +* References to model instances + +In code, we recommend to always use the ``.get()`` method on the settings object to access a value, but for +convenience in templates you can also access settings values at ``settings[name]`` and ``settings.name``. + +.. autoclass:: pretix.base.settings.SettingsProxy + :members: get, set, delete, freeze + +To avoid naming conflicts, plugins are requested to prefix all settings they use with the name of the plugin +or something unique, e.g. ``payment.paypal.api_key``. To reduce redundant typing of this prefix, we provide +another helper class: + +.. autoclass:: pretix.base.settings.SettingsSandbox + +When implementing e.g. a payment or export provider, you do not event need to create this sandbox yourself, +you will just be passed a sandbox object with a prefix generated from your provider name. + +Forms +----- + +We also provide a base class for forms that allow the modification of settings: + +.. autoclass:: pretix.base.forms.SettingsForm + :members: save + +You can simply use it like this:: + + class EventSettingsForm(SettingsForm): + show_date_to = forms.BooleanField( + label=_("Show event end date"), + help_text=_("If disabled, only event's start date will be displayed to the public."), + required=False + ) + payment_term_days = forms.IntegerField( + label=_('Payment term in days'), + help_text=_("The number of days after placing an order the user has to pay to " + "preserve his reservation."), + ) diff --git a/src/pretix/base/forms/__init__.py b/src/pretix/base/forms/__init__.py index 8efc3dbffe..b0a61be99f 100644 --- a/src/pretix/base/forms/__init__.py +++ b/src/pretix/base/forms/__init__.py @@ -83,7 +83,12 @@ class I18nInlineFormSet(BaseInlineFormSet): class SettingsForm(forms.Form): """ - This form is meant to be used for modifying Event- or OrganizerSettings + This form is meant to be used for modifying Event- or OrganizerSettings. It takes + care of loading the current values of the fields and saving the field inputs to the + settings storage. It also deals with setting the available languages for internationalized + fields. + + :param obj: The event or organizer object which should be used for the settings storage """ BOOL_CHOICES = ( ('False', _('disabled')), @@ -99,6 +104,9 @@ class SettingsForm(forms.Form): field.widget.enabled_langcodes = self.obj.settings.get('locales') def save(self): + """ + Performs the save operation + """ for name, field in self.fields.items(): value = self.cleaned_data[name] if isinstance(value, UploadedFile): diff --git a/src/pretix/base/services/mail.py b/src/pretix/base/services/mail.py index ed8bcd26bf..9befef4ac7 100644 --- a/src/pretix/base/services/mail.py +++ b/src/pretix/base/services/mail.py @@ -3,7 +3,6 @@ import logging from django.conf import settings from django.core.mail import EmailMessage, get_connection from django.template.loader import get_template -from django.utils import translation from django.utils.translation import ugettext as _ from typing import Any, Dict @@ -22,22 +21,26 @@ class TolerantDict(dict): def mail(email: str, subject: str, template: str, context: Dict[str, Any]=None, event: Event=None, locale: str=None): """ - Sends out an email to a user. + Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation. - :param email: The e-mail this should be sent to. - :param subject: The e-mail subject. Should be localized. - :param template: The filename of a template to be used. It will - be rendered with the recipient's locale. Alternatively, you - can pass a LazyI18nString and ``context`` will be used - for a Python .format() call. - :param context: The context for rendering the template. - :param event: The event, used for determining the sender of the e-mail - :param locale: The locale used while rendering the template + :param email: The e-mail address of the recipient. - :return: ``False`` on obvious failures, like the user having to e-mail - address, ``True`` otherwise. ``True`` does not necessarily mean that - the email has been sent, just that it has been queued by the e-mail - backend. + :param subject: The e-mail subject. Should be localized to the recipients's locale or a lazy object that will be + localized by being casted to a string. + + :param template: The filename of a template to be used. It will be rendered with the locale given in the locale + argument and the context given in the next argument. Alternatively, you can pass a LazyI18nString and + ``context`` will be used as the argument to a Python ``.format()`` call on the template. + + :param context: The context for rendering the template (see ``template`` parameter). + + :param event: The event this email is related to (optional). If set, this will be used to determine the sender, + a possible prefix for the subject and the SMTP server that should be used to send this email. + + :param locale: The locale to be used while evaluating the subject and the template. + + :return: ``False`` on obvious, immediate failures, ``True`` otherwise. ``True`` does not necessarily mean that + the email has been sent, just that it has been queued by the e-mail backend. """ with language(locale): if isinstance(template, LazyI18nString): diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 7bdeda5261..d205bcda8e 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -213,7 +213,7 @@ class SettingsProxy: This objects allows convenient access to settings stored in the EventSettings/OrganizerSettings database model. It exposes all settings as properties and it will do all the nasty inheritance and defaults stuff for - you. It will return None for non-existing properties. + you. """ def __init__(self, obj: Model, parent: Optional[Model]=None, type=None): @@ -232,7 +232,11 @@ class SettingsProxy: def _flush(self) -> None: self._cached_obj = None - def freeze(self): + def freeze(self) -> dict: + """ + Returns a dictionary of all settings set for this object, including + any default values of its parents or hardcoded in pretix. + """ settings = {} for key, v in DEFAULTS.items(): settings[key] = self._unserialize(v['default'], v['type']) @@ -297,11 +301,17 @@ class SettingsProxy: raise TypeError('Unable to serialize %s into a setting.' % str(type(value))) - def get(self, key: str, default: Any=None, as_type: type=None): + def get(self, key: str, default=None, as_type: type=None): """ - Get a setting specified by key 'key'. Normally, settings are strings, but + Get a setting specified by key ``key``. Normally, settings are strings, but if you put non-strings into the settings object, you can request unserialization - by specifying 'as_type' + by specifying ``as_type``. If the key does not have a harcdoded type in the pretix source, + omitting ``as_type`` always will get you a string. + + If the setting with the specified name does not exist on this object, any parent object + will be queried (e.g. the organizer of an event). If still no value is found, a default + value hardcoded will be returned if one exists. If not, the value of the ``default`` argument + will be returned instead. """ if as_type is None and key in DEFAULTS: as_type = DEFAULTS[key]['type'] @@ -336,6 +346,9 @@ class SettingsProxy: self.set(key, value) def set(self, key: str, value: Any) -> None: + """ + Stores a setting to the database of this object. + """ if key in self._cache(): s = self._cache()[key] else: @@ -347,9 +360,15 @@ class SettingsProxy: def __delattr__(self, key: str) -> None: if key.startswith('_'): return super().__delattr__(key) - return self.__delitem__(key) + self.delete(key) def __delitem__(self, key: str) -> None: + self.delete(key) + + def delete(self, key: str) -> None: + """ + Deletes a setting from this object's storage. + """ if key in self._cache(): self._cache()[key].delete() del self._cache()[key] @@ -357,13 +376,16 @@ class SettingsProxy: class SettingsSandbox: """ - Transparently proxied access to event settings, handling your domain- - prefixes for you. + Transparently proxied access to event settings, handling your prefixes for you. + + :param typestr: The first part of the pretix, e.g. ``plugin`` + :param key: The prefix, e.g. the name of your plugin + :param obj: The event or organizer that should be queried """ - def __init__(self, type: str, key: str, event: Model): - self._event = event - self._type = type + def __init__(self, typestr: str, key: str, obj: Model): + self._event = obj + self._type = typestr self._key = key def _convert_key(self, key: str) -> str: