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.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 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.logentrytypes.log_entry_types registry allows you to do so. A simple implementation could look like:

from django.utils.translation import gettext as _
from pretix.base.logentrytypes import log_entry_types


@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 cannot be automatically detected as belonging to your plugin, leading to confusing user interface situations.

Customizing log entry display

The base LogEntryType classes allow 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 define 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 additional arguments provided by object_link_args. The default implementation of object_link_args will return an argument named by ``object_link_argname, with a value of content_object.pk (the primary key of the model object). 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.

class ItemLogEntryType(EventLogEntryType):
    object_link_wrapper = _('Product {val}')

    # link will be generated as reverse('control:event.item', {'organizer': ..., 'event': ..., 'item': item.pk})
    object_link_viewname = 'control:event.item'
    object_link_argname = 'item'
class OrderLogEntryType(EventLogEntryType):
    object_link_wrapper = _('Order {val}')

    # link will be generated as reverse('control:event.order', {'organizer': ..., 'event': ..., 'code': order.code})
    object_link_viewname = 'control:event.order'

    def object_link_args(self, order):
        return {'code': 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, override the display method:

@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)
LogEntryType.display(logentry)

Returns the message to be displayed for a given logentry of this type.

Returns:

str or LazyI18nString

If your new model object does not belong to an Event, you need to inherit directly from LogEntryType instead of EventLogEntryType, providing your own implementation of get_object_link_info if object links should be displayed.

class pretix.base.logentrytypes.LogEntryType(action_type=None, plain=None)

Base class for a type of LogEntry, identified by its action_type.

Return information to generate a link to the content_object of a given log entry.

Not implemented in the base class, causing the object link to be omitted.

Returns:

Dictionary with the keys href (containing a URL to view/edit the object) and val (containing the escaped text for the anchor element)

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.'))