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.
- get_object_link_info(logentry) dict ¶
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) andval
(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
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.'))