import uuid from asgiref.local import Local from django.utils.functional import LazyObject from wagtail import hooks from wagtail.utils.registry import ObjectTypeRegistry class LogFormatter: """ Defines how to format log messages / comments for a particular action type. Messages that depend on log entry data should override format_message / format_comment; static messages can just be set as the 'message' / 'comment' attribute. To be registered with log_registry.register_action. """ label = "" message = "" comment = "" def format_message(self, log_entry): return self.message def format_comment(self, log_entry): return self.comment _active = Local() class LogContext: """ Stores data about the environment in which a logged action happens - e.g. the active user - to be stored in the log entry for that action. """ def __init__(self, user=None, generate_uuid=True): self.user = user if generate_uuid: self.uuid = uuid.uuid4() else: self.uuid = None def __enter__(self): self._old_log_context = getattr(_active, "value", None) activate(self) return self def __exit__(self, type, value, traceback): if self._old_log_context: activate(self._old_log_context) else: deactivate() empty_log_context = LogContext(generate_uuid=False) def activate(log_context): _active.value = log_context def deactivate(): del _active.value def get_active_log_context(): return getattr(_active, "value", empty_log_context) class LogActionRegistry: """ A central store for log actions. The expected format for registered log actions: Namespaced action, Action label, Action message (or callable) """ def __init__(self): # Has the register_log_actions hook been run for this registry? self.has_scanned_for_actions = False # Holds the formatter objects, keyed by action self.formatters = {} # Holds a list of action, action label tuples for use in filters self.choices = [] # Tracks which LogEntry model should be used for a given object class self.log_entry_models_by_type = ObjectTypeRegistry() # All distinct log entry models registered with register_model self.log_entry_models = set() def scan_for_actions(self): if not self.has_scanned_for_actions: for fn in hooks.get_hooks("register_log_actions"): fn(self) self.has_scanned_for_actions = True def register_model(self, cls, log_entry_model): self.log_entry_models_by_type.register(cls, value=log_entry_model) self.log_entry_models.add(log_entry_model) def register_action(self, action, *args): def register_formatter_class(formatter_cls): formatter = formatter_cls() self.formatters[action] = formatter self.choices.append((action, formatter.label)) if args: # register_action has been invoked as register_action(action, label, message); create a LogFormatter # subclass and register that label, message = args formatter_cls = type( "_LogFormatter", (LogFormatter,), {"label": label, "message": message} ) register_formatter_class(formatter_cls) else: # register_action has been invoked as a @register_action(action) decorator; return the function that # will register the class return register_formatter_class def get_choices(self): self.scan_for_actions() return self.choices def get_formatter(self, log_entry): self.scan_for_actions() return self.formatters.get(log_entry.action) def action_exists(self, action): self.scan_for_actions() return action in self.formatters def get_log_entry_models(self): self.scan_for_actions() return self.log_entry_models def get_action_label(self, action): return self.formatters[action].label def get_log_model_for_model(self, model): self.scan_for_actions() return self.log_entry_models_by_type.get_by_type(model) def get_log_model_for_instance(self, instance): if isinstance(instance, LazyObject): # for LazyObject instances such as request.user, ensure we're looking up the real type instance = instance._wrapped return self.get_log_model_for_model(type(instance)) def log(self, instance, action, user=None, uuid=None, **kwargs): self.scan_for_actions() # find the log entry model for the given object type log_entry_model = self.get_log_model_for_instance(instance) if log_entry_model is None: # no logger registered for this object type - silently bail return user = user or get_active_log_context().user uuid = uuid or get_active_log_context().uuid return log_entry_model.objects.log_action( instance, action, user=user, uuid=uuid, **kwargs ) def get_logs_for_instance(self, instance): log_entry_model = self.get_log_model_for_instance(instance) if log_entry_model is None: # this model has no logs; return an empty queryset of the basic log model from wagtail.models import ModelLogEntry return ModelLogEntry.objects.none() return log_entry_model.objects.for_instance(instance) registry = LogActionRegistry() def log(instance, action, **kwargs): return registry.log(instance, action, **kwargs)