TabbycatDebate/tabbycat

View on GitHub
tabbycat/actionlog/mixins.py

Summary

Maintainability
A
0 mins
Test Coverage
F
36%
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.contrib.auth import get_user_model

from actionlog.consumers import ActionLogEntryConsumer
from tournaments.models import Round, Tournament
from utils.misc import get_ip_address

from .models import ActionLogEntry

User = get_user_model()


class LogActionMixin:
    """Mixin for views that log an action in the action log when a form is
    successfully submitted.

    Views using this mixin should specify an `action_log_type` and, if
    applicable, an `action_log_content_object_attr`. The mixin will add an
    `ActionLogEntry` instance when the form is successfully submitted.

    This mixin is best used with views that also derive from `FormMixin`
    somehow. For forms that don't, they can call `self.log_action()` explicitly.
    """

    action_log_type = None
    action_log_content_object_attr = None

    def get_action_log_type(self):
        """Returns the value that should go in the type field of the
        ActionLogEntry instance. The default implementation returns
        self.action_log_type. Subclasses may override this method.
        """
        return self.action_log_type

    def get_action_log_content_object(self):
        """Returns the content object that should be stored in this action.
        The default implementation looks in `self.action_log_content_object_attr`,
        and if present, it grabs `getattr(self, self.action_log_content_object_attr)`.
        For example, if the `action_log_content_object_attr` class attribute is
        set to "adj_feedback", it grabs `self.adj_feedback`.

        If `action_log_content_object_attr` is not provided, and there is a
        `round` or `tournament` property, it returns the result of
        one of these calls, in that order. Therefore, subclasses need not set
        `action_log_content_object_attr` if the correct content object is a
        Round or Tournament.

        If none of those three are available, it returns None.

        Subclasses can override this method to return the content object
        to be stored with the action log entry.
        """
        if self.action_log_content_object_attr is not None:
            return getattr(self, self.action_log_content_object_attr)
        elif hasattr(self, 'round') and isinstance(self.round, Round):
            return self.round
        elif hasattr(self, 'tournament') and isinstance(self.tournament, Tournament):
            return self.tournament
        else:
            return None

    def get_action_log_fields(self, **kwargs):
        """Returns a dict that should be passed as keyword arguments to the
        `ActionLogEntry` instance.

        The default implementation adds the following:
            - the `type` field, from `get_action_log_type()`
            - the `content_object` field, from `get_action_log_content_object()`
            - the `tournament` field if there is a valid `tournament` property
            - the `round` field if there is a valid `round` property
            - the `user` field if there is a valid user

        If overriding this method, subclasses should call the super() method.

        Note that the `ip_address` field is filled in `log_action()` calls.
        """
        kwargs.setdefault('type', self.get_action_log_type())
        kwargs.setdefault('content_object', self.get_action_log_content_object())

        if hasattr(self, 'round') and isinstance(self.round, Round):
            kwargs.setdefault('round', self.round)

        if hasattr(self, 'tournament') and isinstance(self.tournament, Tournament):
            kwargs.setdefault('tournament', self.tournament)

        if hasattr(self.request, 'user') and isinstance(self.request.user, User):
            kwargs.setdefault('user', self.request.user)

        return kwargs

    def log_action(self, **kwargs):
        """Logs the action. Subclasses can call this if the class doesn't
        have `FormMixin`. If keyword arguments are provided, they override the
        keyword arguments provided by `get_action_log_fields()`, except for
        `ip_address`, which cannot be overridden.
        """
        ip_address = get_ip_address(self.request)
        action_log_fields = self.get_action_log_fields()
        action_log_fields.update(kwargs)
        log = ActionLogEntry.objects.log(ip_address=ip_address, **action_log_fields)

        # Notify the actionlog consumer to broadcast the event
        if tournament := action_log_fields.get('tournament'):
            print('Broadcasting notification of ActionLogEntryConsumer')
            group_name = ActionLogEntryConsumer.group_prefix + "_" + tournament.slug
            async_to_sync(get_channel_layer().group_send)(group_name, {
                "type": "send_json",
                "data": log.serialize,
            })

    # If these methods exist, add `self.log_action()` to them.
    # (If they don't, this should be harmless.)

    def form_valid(self, form):
        self.log_action()
        return super().form_valid(form)

    def formset_valid(self, formset):
        self.log_action()
        return super().formset_valid(formset)