From 505d3624b322dda879d588a992c62e596a4e28fa Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Sun, 31 Jul 2016 13:31:57 +0200 Subject: [PATCH] Added documentation on logging --- doc/development/implementation/index.rst | 1 + doc/development/implementation/logging.rst | 102 +++++++++++++++++++++ src/pretix/base/models/__init__.py | 2 +- src/pretix/base/models/base.py | 5 + 4 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 doc/development/implementation/logging.rst diff --git a/doc/development/implementation/index.rst b/doc/development/implementation/index.rst index 416163068..7faf4f235 100644 --- a/doc/development/implementation/index.rst +++ b/doc/development/implementation/index.rst @@ -16,3 +16,4 @@ Contents: settings background email + logging diff --git a/doc/development/implementation/logging.rst b/doc/development/implementation/logging.rst new file mode 100644 index 000000000..d6416c92d --- /dev/null +++ b/doc/development/implementation/logging.rst @@ -0,0 +1,102 @@ +Logging +======= + +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 mossible to display those logs to a human, if +required. pretix stores all those logs centrally in a model called :py:class:`pretix.base.models.LogEntry`. +We recommend all relevant models to inherit from ``LoggedModel`` as it simplifies creating new log entries: + +.. autoclass:: pretix.base.models.LoggedModel + :members: log_action, all_logentries + +To actually log an action, you can just call the ``log_action`` method on your object:: + + order.log_action('pretix.event.order.cancelled', user=user, data={}) + +The positional ``action`` argument should represent the type of action and should be globally unique, we +recomment do prefix it with your packagename, 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 :py:attr:`pretix.base.signals.logentry_display` signals allows you to do so. A simple +implementation could look like:: + + 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.cancelled': _('The order has been cancelled.'), + ... + } + if logentry.action_type in plains: + return plains[logentry.action_type] + + +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.')) diff --git a/src/pretix/base/models/__init__.py b/src/pretix/base/models/__init__.py index 7ae792690..ec9b5c7da 100644 --- a/src/pretix/base/models/__init__.py +++ b/src/pretix/base/models/__init__.py @@ -1,5 +1,5 @@ from .auth import User -from .base import CachedFile, cachedfile_name +from .base import CachedFile, LoggedModel, cachedfile_name from .event import Event, EventLock, EventPermission, EventSetting from .invoices import Invoice, InvoiceLine, invoice_filename from .items import ( diff --git a/src/pretix/base/models/base.py b/src/pretix/base/models/base.py index 9f4610644..4d41e4698 100644 --- a/src/pretix/base/models/base.py +++ b/src/pretix/base/models/base.py @@ -64,4 +64,9 @@ class LoggedModel(models.Model, LoggingMixin): abstract = True def all_logentries(self): + """ + Returns all log entries that are attached to this object. + + :return: A QuerySet of LogEntry objects + """ return self.logentries.all().select_related('user')