Commit cf7894be authored by Claude Paroz's avatar Claude Paroz
Browse files

Fixed #21113 -- Made LogEntry.change_message language independent

Thanks Tim Graham for the review.
parent 56aaae58
Loading
Loading
Loading
Loading
+47 −1
Original line number Diff line number Diff line
from __future__ import unicode_literals

import json

from django.conf import settings
from django.contrib.admin.utils import quote
from django.contrib.contenttypes.models import ContentType
@@ -7,6 +9,7 @@ from django.db import models
from django.urls import NoReverseMatch, reverse
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible, smart_text
from django.utils.text import get_text_list
from django.utils.translation import ugettext, ugettext_lazy as _

ADDITION = 1
@@ -18,6 +21,8 @@ class LogEntryManager(models.Manager):
    use_in_migrations = True

    def log_action(self, user_id, content_type_id, object_id, object_repr, action_flag, change_message=''):
        if isinstance(change_message, list):
            change_message = json.dumps(change_message)
        self.model.objects.create(
            user_id=user_id,
            content_type_id=content_type_id,
@@ -50,6 +55,7 @@ class LogEntry(models.Model):
    # Translators: 'repr' means representation (https://docs.python.org/3/library/functions.html#repr)
    object_repr = models.CharField(_('object repr'), max_length=200)
    action_flag = models.PositiveSmallIntegerField(_('action flag'))
    # change_message is either a string or a JSON structure
    change_message = models.TextField(_('change message'), blank=True)

    objects = LogEntryManager()
@@ -69,7 +75,7 @@ class LogEntry(models.Model):
        elif self.is_change():
            return ugettext('Changed "%(object)s" - %(changes)s') % {
                'object': self.object_repr,
                'changes': self.change_message,
                'changes': self.get_change_message(),
            }
        elif self.is_deletion():
            return ugettext('Deleted "%(object)s."') % {'object': self.object_repr}
@@ -85,6 +91,46 @@ class LogEntry(models.Model):
    def is_deletion(self):
        return self.action_flag == DELETION

    def get_change_message(self):
        """
        If self.change_message is a JSON structure, interpret it as a change
        string, properly translated.
        """
        if self.change_message and self.change_message[0] == '[':
            try:
                change_message = json.loads(self.change_message)
            except ValueError:
                return self.change_message
            messages = []
            for sub_message in change_message:
                if 'added' in sub_message:
                    if sub_message['added']:
                        sub_message['added']['name'] = ugettext(sub_message['added']['name'])
                        messages.append(ugettext('Added {name} "{object}".').format(**sub_message['added']))
                    else:
                        messages.append(ugettext('Added.'))

                elif 'changed' in sub_message:
                    sub_message['changed']['fields'] = get_text_list(
                        sub_message['changed']['fields'], ugettext('and')
                    )
                    if 'name' in sub_message['changed']:
                        sub_message['changed']['name'] = ugettext(sub_message['changed']['name'])
                        messages.append(ugettext('Changed {fields} for {name} "{object}".').format(
                            **sub_message['changed']
                        ))
                    else:
                        messages.append(ugettext('Changed {fields}.').format(**sub_message['changed']))

                elif 'deleted' in sub_message:
                    sub_message['deleted']['name'] = ugettext(sub_message['deleted']['name'])
                    messages.append(ugettext('Deleted {name} "{object}".').format(**sub_message['deleted']))

            change_message = ' '.join(msg[0].upper() + msg[1:] for msg in messages)
            return change_message or ugettext('No fields changed.')
        else:
            return self.change_message

    def get_edited_object(self):
        "Returns the edited object represented by this log entry"
        return self.content_type.get_object_for_this_type(pk=self.object_id)
+29 −16
Original line number Diff line number Diff line
@@ -44,7 +44,9 @@ from django.utils.html import escape, format_html
from django.utils.http import urlencode, urlquote
from django.utils.safestring import mark_safe
from django.utils.text import capfirst, get_text_list
from django.utils.translation import string_concat, ugettext as _, ungettext
from django.utils.translation import (
    override as translation_override, string_concat, ugettext as _, ungettext,
)
from django.views.decorators.csrf import csrf_protect
from django.views.generic import RedirectView

@@ -924,33 +926,44 @@ class ModelAdmin(BaseModelAdmin):
                return urlencode({'_changelist_filters': preserved_filters})
        return ''

    @translation_override(None)
    def construct_change_message(self, request, form, formsets, add=False):
        """
        Construct a change message from a changed object.
        Construct a JSON structure describing changes from a changed object.
        Translations are deactivated so that strings are stored untranslated.
        Translation happens later on LogEntry access.
        """
        change_message = []
        if add:
            change_message.append(_('Added.'))
            change_message.append({'added': {}})
        elif form.changed_data:
            change_message.append(_('Changed %s.') % get_text_list(form.changed_data, _('and')))
            change_message.append({'changed': {'fields': form.changed_data}})

        if formsets:
            for formset in formsets:
                for added_object in formset.new_objects:
                    change_message.append(_('Added %(name)s "%(object)s".')
                                          % {'name': force_text(added_object._meta.verbose_name),
                                             'object': force_text(added_object)})
                    change_message.append({
                        'added': {
                            'name': force_text(added_object._meta.verbose_name),
                            'object': force_text(added_object),
                        }
                    })
                for changed_object, changed_fields in formset.changed_objects:
                    change_message.append(_('Changed %(list)s for %(name)s "%(object)s".')
                                          % {'list': get_text_list(changed_fields, _('and')),
                    change_message.append({
                        'changed': {
                            'name': force_text(changed_object._meta.verbose_name),
                                             'object': force_text(changed_object)})
                            'object': force_text(changed_object),
                            'fields': changed_fields,
                        }
                    })
                for deleted_object in formset.deleted_objects:
                    change_message.append(_('Deleted %(name)s "%(object)s".')
                                          % {'name': force_text(deleted_object._meta.verbose_name),
                                             'object': force_text(deleted_object)})
        change_message = ' '.join(change_message)
        return change_message or _('No fields changed.')
                    change_message.append({
                        'deleted': {
                            'name': force_text(deleted_object._meta.verbose_name),
                            'object': force_text(deleted_object),
                        }
                    })
        return change_message

    def message_user(self, request, message, level=messages.INFO, extra_tags='',
                     fail_silently=False):
+1 −1
Original line number Diff line number Diff line
@@ -29,7 +29,7 @@
        <tr>
            <th scope="row">{{ action.action_time|date:"DATETIME_FORMAT" }}</th>
            <td>{{ action.user.get_username }}{% if action.user.get_full_name %} ({{ action.user.get_full_name }}){% endif %}</td>
            <td>{{ action.change_message }}</td>
            <td>{{ action.get_change_message }}</td>
        </tr>
        {% endfor %}
        </tbody>
+20 −1
Original line number Diff line number Diff line
@@ -2823,7 +2823,18 @@ password box.
.. attribute:: LogEntry.change_message

    The detailed description of the modification. In the case of an edit, for
    example, the message contains a list of the edited fields.
    example, the message contains a list of the edited fields. The Django admin
    site formats this content as a JSON structure, so that
    :meth:`get_change_message` can recompose a message translated in the current
    user language. Custom code might set this as a plain string though. You are
    advised to use the :meth:`get_change_message` method to retrieve this value
    instead of accessing it directly.

    .. versionchanged:: 1.10

        Previously, this attribute was always a plain string. It is
        now JSON-structured so that the message can be translated in the current
        user language. Old messages are untouched.

``LogEntry`` methods
--------------------
@@ -2832,6 +2843,14 @@ password box.

    A shortcut that returns the referenced object.

.. method:: LogEntry.get_change_message()

    .. versionadded:: 1.10

    Formats and translates :attr:`change_message` into the current user
    language. Messages created before Django 1.10 will always be displayed in
    the language in which they were logged.

.. currentmodule:: django.contrib.admin

.. _admin-reverse-urls:
+9 −0
Original line number Diff line number Diff line
@@ -51,6 +51,11 @@ Minor features
  model's changelist will now be rendered (without the add button, of course).
  This makes it easier to add custom tools in this case.

* The :class:`~django.contrib.admin.models.LogEntry` model now stores change
  messages in a JSON structure so that the message can be dynamically translated
  using the current active language. A new ``LogEntry.get_change_message()``
  method is now the preferred way of retrieving the change message.

:mod:`django.contrib.admindocs`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

@@ -417,6 +422,10 @@ Miscellaneous
* :djadmin:`loaddata` now raises a ``CommandError`` instead of showing a
  warning when the specified fixture file is not found.

* Instead of directly accessing the ``LogEntry.change_message`` attribute, it's
  now better to call the ``LogEntry.get_change_message()`` method which will
  provide the message in the current language.

.. _deprecated-features-1.10:

Features deprecated in 1.10
Loading