Loading django/contrib/admin/models.py +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 Loading @@ -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 Loading @@ -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, Loading Loading @@ -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() Loading @@ -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} Loading @@ -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) Loading django/contrib/admin/options.py +29 −16 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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): Loading django/contrib/admin/templates/admin/object_history.html +1 −1 Original line number Diff line number Diff line Loading @@ -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> Loading docs/ref/contrib/admin/index.txt +20 −1 Original line number Diff line number Diff line Loading @@ -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 -------------------- Loading @@ -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: Loading docs/releases/1.10.txt +9 −0 Original line number Diff line number Diff line Loading @@ -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` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Loading Loading @@ -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 Loading
django/contrib/admin/models.py +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 Loading @@ -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 Loading @@ -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, Loading Loading @@ -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() Loading @@ -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} Loading @@ -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) Loading
django/contrib/admin/options.py +29 −16 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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): Loading
django/contrib/admin/templates/admin/object_history.html +1 −1 Original line number Diff line number Diff line Loading @@ -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> Loading
docs/ref/contrib/admin/index.txt +20 −1 Original line number Diff line number Diff line Loading @@ -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 -------------------- Loading @@ -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: Loading
docs/releases/1.10.txt +9 −0 Original line number Diff line number Diff line Loading @@ -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` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Loading Loading @@ -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