Fixed #12952 -- Adjusted admin log change messages to use form labels instead of field names.

This commit is contained in:
Sanyam Khurana 2019-06-14 21:50:29 +05:30 committed by Carlton Gibson
parent 1564e42ad3
commit 87f5d07eed
10 changed files with 128 additions and 17 deletions

View File

@ -114,7 +114,7 @@ class LogEntry(models.Model):
elif 'changed' in sub_message: elif 'changed' in sub_message:
sub_message['changed']['fields'] = get_text_list( sub_message['changed']['fields'] = get_text_list(
sub_message['changed']['fields'], gettext('and') [gettext(field_name) for field_name in sub_message['changed']['fields']], gettext('and')
) )
if 'name' in sub_message['changed']: if 'name' in sub_message['changed']:
sub_message['changed']['name'] = gettext(sub_message['changed']['name']) sub_message['changed']['name'] = gettext(sub_message['changed']['name'])

View File

@ -489,12 +489,21 @@ def construct_change_message(form, formsets, add):
Translations are deactivated so that strings are stored untranslated. Translations are deactivated so that strings are stored untranslated.
Translation happens later on LogEntry access. Translation happens later on LogEntry access.
""" """
# Evaluating `form.changed_data` prior to disabling translations is required
# to avoid fields affected by localization from being included incorrectly,
# e.g. where date formats differ such as MM/DD/YYYY vs DD/MM/YYYY.
changed_data = form.changed_data
with translation_override(None):
# Deactivate translations while fetching verbose_name for form
# field labels and using `field_name`, if verbose_name is not provided.
# Translations will happen later on LogEntry access.
changed_field_labels = _get_changed_field_labels_from_form(form, changed_data)
change_message = [] change_message = []
if add: if add:
change_message.append({'added': {}}) change_message.append({'added': {}})
elif form.changed_data: elif form.changed_data:
change_message.append({'changed': {'fields': form.changed_data}}) change_message.append({'changed': {'fields': changed_field_labels}})
if formsets: if formsets:
with translation_override(None): with translation_override(None):
for formset in formsets: for formset in formsets:
@ -510,7 +519,7 @@ def construct_change_message(form, formsets, add):
'changed': { 'changed': {
'name': str(changed_object._meta.verbose_name), 'name': str(changed_object._meta.verbose_name),
'object': str(changed_object), 'object': str(changed_object),
'fields': changed_fields, 'fields': _get_changed_field_labels_from_form(formset.forms[0], changed_fields),
} }
}) })
for deleted_object in formset.deleted_objects: for deleted_object in formset.deleted_objects:
@ -521,3 +530,14 @@ def construct_change_message(form, formsets, add):
} }
}) })
return change_message return change_message
def _get_changed_field_labels_from_form(form, changed_data):
changed_field_labels = []
for field_name in changed_data:
try:
verbose_field_name = form.fields[field_name].label or field_name
except KeyError:
verbose_field_name = field_name
changed_field_labels.append(str(verbose_field_name))
return changed_field_labels

View File

@ -322,6 +322,12 @@ backends.
:class:`~django.db.models.DateTimeField` in ``datetime_cast_date_sql()``, :class:`~django.db.models.DateTimeField` in ``datetime_cast_date_sql()``,
``datetime_extract_sql()``, etc. ``datetime_extract_sql()``, etc.
:mod:`django.contrib.admin`
---------------------------
* Admin's model history change messages now prefers more readable field labels
instead of field names.
:mod:`django.contrib.gis` :mod:`django.contrib.gis`
------------------------- -------------------------

View File

@ -1,11 +1,30 @@
from django import forms
from django.contrib import admin from django.contrib import admin
from .models import Article, ArticleProxy, Site from .models import Article, ArticleProxy, Site
class ArticleAdminForm(forms.ModelForm):
nolabel_form_field = forms.BooleanField(required=False)
class Meta:
model = Article
fields = ['title']
@property
def changed_data(self):
data = super().changed_data
if data:
# Add arbitrary name to changed_data to test
# change message construction.
return data + ['not_a_form_field']
return data
class ArticleInline(admin.TabularInline): class ArticleInline(admin.TabularInline):
model = Article model = Article
fields = ['title'] fields = ['title']
form = ArticleAdminForm
class SiteAdmin(admin.ModelAdmin): class SiteAdmin(admin.ModelAdmin):

View File

@ -53,9 +53,9 @@ class LogEntryTests(TestCase):
response = self.client.post(change_url, post_data) response = self.client.post(change_url, post_data)
self.assertRedirects(response, reverse('admin:admin_utils_article_changelist')) self.assertRedirects(response, reverse('admin:admin_utils_article_changelist'))
logentry = LogEntry.objects.filter(content_type__model__iexact='article').latest('id') logentry = LogEntry.objects.filter(content_type__model__iexact='article').latest('id')
self.assertEqual(logentry.get_change_message(), 'Changed title and hist.') self.assertEqual(logentry.get_change_message(), 'Changed Title and History.')
with translation.override('fr'): with translation.override('fr'):
self.assertEqual(logentry.get_change_message(), 'Modification de title et hist.') self.assertEqual(logentry.get_change_message(), 'Modification de Title et Historique.')
add_url = reverse('admin:admin_utils_article_add') add_url = reverse('admin:admin_utils_article_add')
post_data['title'] = 'New' post_data['title'] = 'New'
@ -85,7 +85,7 @@ class LogEntryTests(TestCase):
response = self.client.post(change_url, post_data) response = self.client.post(change_url, post_data)
self.assertRedirects(response, reverse('admin:admin_utils_article_changelist')) self.assertRedirects(response, reverse('admin:admin_utils_article_changelist'))
logentry = LogEntry.objects.filter(content_type__model__iexact='article').latest('id') logentry = LogEntry.objects.filter(content_type__model__iexact='article').latest('id')
self.assertEqual(logentry.get_change_message(), 'Changed title and hist.') self.assertEqual(logentry.get_change_message(), 'Changed Title and History.')
def test_logentry_change_message_formsets(self): def test_logentry_change_message_formsets(self):
""" """
@ -123,23 +123,25 @@ class LogEntryTests(TestCase):
self.assertEqual( self.assertEqual(
json.loads(logentry.change_message), json.loads(logentry.change_message),
[ [
{"changed": {"fields": ["domain"]}}, {"changed": {"fields": ["Domain"]}},
{"added": {"object": "Added article", "name": "article"}}, {"added": {"object": "Added article", "name": "article"}},
{"changed": {"fields": ["title"], "object": "Changed Title", "name": "article"}}, {"changed": {"fields": ["Title", "not_a_form_field"], "object": "Changed Title", "name": "article"}},
{"deleted": {"object": "Title second article", "name": "article"}}, {"deleted": {"object": "Title second article", "name": "article"}},
] ]
) )
self.assertEqual( self.assertEqual(
logentry.get_change_message(), logentry.get_change_message(),
'Changed domain. Added article "Added article". ' 'Changed Domain. Added article "Added article". '
'Changed title for article "Changed Title". Deleted article "Title second article".' 'Changed Title and not_a_form_field for article "Changed Title". '
'Deleted article "Title second article".'
) )
with translation.override('fr'): with translation.override('fr'):
self.assertEqual( self.assertEqual(
logentry.get_change_message(), logentry.get_change_message(),
"Modification de domain. Ajout de article « Added article ». " "Modification de Domain. Ajout de article « Added article ». "
"Modification de title pour l'objet article « Changed Title ». " "Modification de Title et not_a_form_field pour l'objet "
"article « Changed Title ». "
"Suppression de article « Title second article »." "Suppression de article « Title second article »."
) )

View File

@ -893,8 +893,27 @@ class CityInlineAdmin(admin.TabularInline):
view_on_site = False view_on_site = False
class StateAdminForm(forms.ModelForm):
nolabel_form_field = forms.BooleanField(required=False)
class Meta:
model = State
fields = '__all__'
labels = {"name": "State name (from form's Meta.labels)"}
@property
def changed_data(self):
data = super().changed_data
if data:
# Add arbitrary name to changed_data to test
# change message construction.
return data + ['not_a_form_field']
return data
class StateAdmin(admin.ModelAdmin): class StateAdmin(admin.ModelAdmin):
inlines = [CityInlineAdmin] inlines = [CityInlineAdmin]
form = StateAdminForm
class RestaurantInlineAdmin(admin.TabularInline): class RestaurantInlineAdmin(admin.TabularInline):

View File

@ -864,12 +864,12 @@ class EmptyModelMixin(models.Model):
class State(models.Model): class State(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100, verbose_name='State verbose_name')
class City(models.Model): class City(models.Model):
state = models.ForeignKey(State, models.CASCADE) state = models.ForeignKey(State, models.CASCADE)
name = models.CharField(max_length=100) name = models.CharField(max_length=100, verbose_name='City verbose_name')
def get_absolute_url(self): def get_absolute_url(self):
return '/dummy/%s/' % self.pk return '/dummy/%s/' % self.pk

View File

@ -0,0 +1,45 @@
from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import User
from django.test import TestCase, override_settings
from django.urls import reverse
from .models import City, State
@override_settings(ROOT_URLCONF='admin_views.urls')
class AdminHistoryViewTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.superuser = User.objects.create_superuser(
username='super', password='secret', email='super@example.com',
)
def setUp(self):
self.client.force_login(self.superuser)
def test_changed_message_uses_form_lables(self):
"""
Admin's model history change messages use form labels instead of
field names.
"""
state = State.objects.create(name='My State Name')
city = City.objects.create(name='My City Name', state=state)
change_dict = {
'name': 'My State Name 2',
'nolabel_form_field': True,
'city_set-0-name': 'My City name 2',
'city_set-0-id': city.pk,
'city_set-TOTAL_FORMS': '3',
'city_set-INITIAL_FORMS': '1',
'city_set-MAX_NUM_FORMS': '0',
}
state_change_url = reverse('admin:admin_views_state_change', args=(state.pk,))
self.client.post(state_change_url, change_dict)
logentry = LogEntry.objects.filter(content_type__model__iexact='state').latest('id')
self.assertEqual(
logentry.get_change_message(),
'Changed State name (from form\'s Meta.labels), '
'nolabel_form_field and not_a_form_field. '
'Changed City verbose_name for city "%s".' % city
)

View File

@ -900,7 +900,7 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
self.assertRedirects(response, reverse('admin:admin_views_readablepizza_changelist')) self.assertRedirects(response, reverse('admin:admin_views_readablepizza_changelist'))
pizza_ctype = ContentType.objects.get_for_model(ReadablePizza, for_concrete_model=False) pizza_ctype = ContentType.objects.get_for_model(ReadablePizza, for_concrete_model=False)
log = LogEntry.objects.filter(content_type=pizza_ctype, object_id=pizza.pk).first() log = LogEntry.objects.filter(content_type=pizza_ctype, object_id=pizza.pk).first()
self.assertEqual(log.get_change_message(), 'Changed toppings.') self.assertEqual(log.get_change_message(), 'Changed Toppings.')
def test_allows_attributeerror_to_bubble_up(self): def test_allows_attributeerror_to_bubble_up(self):
""" """

View File

@ -1173,7 +1173,7 @@ class ChangelistTests(AuthViewsTestCase):
) )
self.assertRedirects(response, reverse('auth_test_admin:auth_user_changelist')) self.assertRedirects(response, reverse('auth_test_admin:auth_user_changelist'))
row = LogEntry.objects.latest('id') row = LogEntry.objects.latest('id')
self.assertEqual(row.get_change_message(), 'Changed email.') self.assertEqual(row.get_change_message(), 'Changed Email address.')
def test_user_not_change(self): def test_user_not_change(self):
response = self.client.post( response = self.client.post(