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
referenceone
title
text that is shown e.g. in the email subject or in a headlineoptionally one
detail
text that may or may not be shown depending on the notification methodoptionally 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.'))