Logging and notifications

As pretix is handling monetary transactions, we are very careful to make it possible to review all changes in the system that lead to the current state.

Logging changes

We log data changes to the database in a format that makes it possible to display those logs to a human, if required. pretix stores all those logs centrally in a model called pretix.base.models.LogEntry. We recommend all relevant models to inherit from LoggedModel as it simplifies creating new log entries:

class pretix.base.models.LoggedModel(*args, **kwargs)
all_logentries()

Returns all log entries that are attached to this object.

Returns:

A QuerySet of LogEntry objects

log_action(action, data=None, user=None, api_token=None, auth=None, save=True)

Create a LogEntry object that is related to this object. See the LogEntry documentation for details.

Parameters:
  • action – The namespaced action code

  • data – Any JSON-serializable object

  • user – The user performing the action (optional)

To actually log an action, you can just call the log_action method on your object:

order.log_action('pretix.event.order.canceled', user=user, data={})

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 optional and may contain the user who performed the action. The optional data argument can contain additional information about this action.

Logging form actions

A very common use case is to log the changes to a model that have been done in a ModelForm. In this case, we generally use a custom form_valid method on our FormView that looks like this:

@transaction.atomic
def form_valid(self, form):
    if form.has_changed():
        self.request.event.log_action('pretix.event.changed', user=self.request.user, data={
            k: getattr(self.request.event, k) for k in form.changed_data
        })
    messages.success(self.request, _('Your changes have been saved.'))
    return super().form_valid(form)

It gets a little bit more complicated if your form allows file uploads:

@transaction.atomic
def form_valid(self, form):
    if form.has_changed():
        self.request.event.log_action(
            'pretix.event.changed', user=self.request.user, data={
                k: (form.cleaned_data.get(k).name
                    if isinstance(form.cleaned_data.get(k), File)
                    else form.cleaned_data.get(k))
                for k in form.changed_data
            }
        )
    messages.success(self.request, _('Your changes have been saved.'))
    return super().form_valid(form)

Displaying logs

If you want to display the logs of a particular object to a user in the backend, you can use the 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 pretix.base.signals.logentry_display signals allows you to do so. A simple implementation could look like:

from django.utils.translation import gettext as _
from pretix.base.signals import logentry_display

@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]

Sending notifications

If you think that the logged information might be important or urgent enough to send out a notification to interested organizers. In this case, you should listen for the pretix.base.signals.register_notification_types signal to register a notification type:

@receiver(register_notification_types)
def register_my_notification_types(sender, **kwargs):
    return [MyNotificationType(sender)]

Note that this event is different than other events send out by pretix: sender may be an event or None. The latter case is required to let the user define global notification preferences for all events.

You also need to implement a custom class that specifies how notifications should be handled for your notification type. You should subclass the base NotificationType class and implement all its members:

class pretix.base.notifications.NotificationType(event: Event = None)
property action_type: str

The action_type string that this notification handles, for example "pretix.event.order.paid". Only one notification type should be registered per action type.

build_notification(logentry: LogEntry) Notification

This is the main function that you should override. It is supposed to turn a log entry object into a notification object that can then be rendered e.g. into an email.

property required_permission: str

The permission a user needs to hold for the related event to receive this notification.

property verbose_name: str

A human-readable name of this notification type.

A simple implementation could look like this:

class MyNotificationType(NotificationType):
    required_permission = "can_view_orders"
    action_type = "pretix.event.order.paid"
    verbose_name = _("Order has been paid")

    def build_notification(self, logentry: LogEntry):
        order = logentry.content_object

        order_url = build_absolute_uri(
            'control:event.order',
            kwargs={
                'organizer': logentry.event.organizer.slug,
                'event': logentry.event.slug,
                'code': order.code
            }
        )

        n = Notification(
            event=logentry.event,
            title=_('Order {code} has been marked as paid').format(code=order.code),
            url=order_url
        )
        n.add_attribute(_('Order code'), order.code)
        n.add_action(_('View order details'), order_url)
        return n

As you can see, the relevant code is in the build_notification method that is supposed to create a Notification method that has a title, description, URL, attributes, and actions. The full definition of Notification is the following:

class pretix.base.notifications.Notification(event: Event, title: str, detail: str = None, url: str = None)

Represents a notification that is sent/shown to a user. A notification consists of:

  • one event reference

  • one title text that is shown e.g. in the email subject or in a headline

  • optionally one detail text that may or may not be shown depending on the notification method

  • optionally one url that should be absolute and point to the context of an notification (e.g. an order)

  • optionally a number of attributes consisting of a title and a value that can be used to add additional details to the notification (e.g. “Customer: ABC”)

  • optionally a number of actions that may or may not be shown as buttons depending on the notification method, each consisting of a button label and an absolute URL to point to.

add_action(label, url)

Add an action to the notification, defined by a label and an url. An example could be a label of “View order” and an url linking to the order detail page.

add_attribute(title, value)

Add an attribute to the notification, defined by a title and a value. An example could be a title of “Date” and a value of “2017-12-14”.

Logging technical information

If you just want to log technical information to a log file on disk that does not need to be parsed and displayed later, you can just use Python’s logging module:

import logging

logger = logging.getLogger(__name__)

logger.info('Startup complete.')

This is also very useful to provide debugging information when an exception occurs:

try:
   foo()
except:
   logger.exception('Error when calling foo()')  # Traceback will automatically be appended
   messages.error(request, _('An error occured.'))