kiwitcms/Kiwi

View on GitHub
tcms/core/history.py

Summary

Maintainability
A
35 mins
Test Coverage
# pylint: disable=unused-argument, no-self-use, avoid-list-comprehension
import difflib

from django.db.models import signals
from django.http import HttpResponseRedirect
from django.template.defaultfilters import safe
from django.utils.translation import gettext_lazy as _
from simple_history.admin import SimpleHistoryAdmin
from simple_history.models import HistoricalRecords

from tcms.core.templatetags.extra_filters import bleach_input


def diff_objects(old_instance, new_instance, fields):
    """
    Diff two objects by examining the given fields and
    return a string.
    """
    full_diff = []

    for field in fields:
        field_diff = []
        old_value = getattr(old_instance, field.attname)
        new_value = getattr(new_instance, field.attname)

        # clean stored XSS
        if isinstance(old_value, str):
            old_value = bleach_input(old_value)
        if isinstance(new_value, str):
            new_value = bleach_input(new_value)

        for line in difflib.unified_diff(
            str(old_value).split("\n"),
            str(new_value).split("\n"),
            fromfile=field.attname,
            tofile=field.attname,
            lineterm="",
        ):
            field_diff.append(line)
        full_diff.extend(field_diff)

    return "\n".join(full_diff)


def history_email_for(instance, title):
    """
    Generate the subject and email body that is sent via
    email notifications post update!
    """
    history = instance.history.latest()

    subject = _("UPDATE: %(model_name)s #%(pk)d - %(title)s") % {
        "model_name": instance.__class__.__name__,
        "pk": instance.pk,
        "title": title,
    }
    # no multi-line email headers
    subject = subject.replace("\n", " ").replace("\r", " ")

    body = (
        _(
            """Updated on %(history_date)s
Updated by %(username)s

%(diff)s

For more information:
%(instance_url)s"""
        )
        % {
            "history_date": history.history_date.strftime("%c"),
            "username": getattr(history.history_user, "username", ""),
            "diff": history.history_change_reason,
            "instance_url": instance.get_full_url(),
        }
    )
    return subject, body


class KiwiHistoricalRecords(HistoricalRecords):
    """
    This class will keep track of what fields were changed
    inside of the ``history_change_reason`` field. This gives us
    a crude changelog until upstream introduces their new interface.
    """

    def pre_save(self, instance, **kwargs):
        """
        Signal handlers don't have access to the previous version of
        an object so we have to load it from the database!
        """
        if kwargs.get("raw", False):
            return

        if instance.pk and hasattr(instance, "history"):
            instance.previous = instance.__class__.objects.filter(
                pk=instance.pk
            ).first()

    def post_save(self, instance, created, using=None, **kwargs):
        """
        Calculate the changelog and call the inherited method to
        write the data into the database.
        """
        if kwargs.get("raw", False):
            return

        if hasattr(instance, "previous") and instance.previous:
            # note: simple_history.utils.update_change_reason() performs an extra
            # DB query so it is better to use the private field instead!
            # In older simple_history version this field wasn't private but was renamed
            # in 2.10.0 hence the pylint disable!
            instance._change_reason = diff_objects(  # pylint: disable=protected-access
                instance.previous, instance, self.fields_included(instance)
            )
        super().post_save(instance, created, using, **kwargs)

    def finalize(self, sender, **kwargs):
        """
        Connect the pre_save signal handler after calling the inherited method.
        """
        super().finalize(sender, **kwargs)
        signals.pre_save.connect(self.pre_save, sender=sender, weak=False)


class ReadOnlyHistoryAdmin(SimpleHistoryAdmin):
    """
    Custom history admin which shows all fields
    as read-only.
    """

    history_list_display = ["Diff"]

    def Diff(self, obj):  # pylint: disable=invalid-name
        return safe(f"<pre>{obj.history_change_reason}</pre>")

    def get_readonly_fields(self, request, obj=None):
        # make all fields readonly
        readonly_fields = list(
            set(
                [field.name for field in self.opts.local_fields]
                + [field.name for field in self.opts.local_many_to_many]
            )
        )
        return readonly_fields

    def response_change(self, request, obj):
        super().response_change(request, obj)
        return HttpResponseRedirect(obj.get_absolute_url())