mirror of
https://github.com/django/django.git
synced 2024-12-22 17:16:24 +00:00
Fixed #34462 -- Made admin log actions in bulk.
This also deprecates ModelAdmin.log_deletion() and LogEntryManager.log_action().
This commit is contained in:
parent
45e0c5892f
commit
40b3975e7d
@ -45,9 +45,7 @@ def delete_selected(modeladmin, request, queryset):
|
|||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
n = len(queryset)
|
n = len(queryset)
|
||||||
if n:
|
if n:
|
||||||
for obj in queryset:
|
modeladmin.log_deletions(request, queryset)
|
||||||
obj_display = str(obj)
|
|
||||||
modeladmin.log_deletion(request, obj, obj_display)
|
|
||||||
modeladmin.delete_queryset(request, queryset)
|
modeladmin.delete_queryset(request, queryset)
|
||||||
modeladmin.message_user(
|
modeladmin.message_user(
|
||||||
request,
|
request,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
import warnings
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.admin.utils import quote
|
from django.contrib.admin.utils import quote
|
||||||
@ -6,6 +7,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import NoReverseMatch, reverse
|
from django.urls import NoReverseMatch, reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.deprecation import RemovedInDjango60Warning
|
||||||
from django.utils.text import get_text_list
|
from django.utils.text import get_text_list
|
||||||
from django.utils.translation import gettext
|
from django.utils.translation import gettext
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@ -33,6 +35,11 @@ class LogEntryManager(models.Manager):
|
|||||||
action_flag,
|
action_flag,
|
||||||
change_message="",
|
change_message="",
|
||||||
):
|
):
|
||||||
|
warnings.warn(
|
||||||
|
"LogEntryManager.log_action() is deprecated. Use log_actions() instead.",
|
||||||
|
RemovedInDjango60Warning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
if isinstance(change_message, list):
|
if isinstance(change_message, list):
|
||||||
change_message = json.dumps(change_message)
|
change_message = json.dumps(change_message)
|
||||||
return self.model.objects.create(
|
return self.model.objects.create(
|
||||||
@ -44,6 +51,55 @@ class LogEntryManager(models.Manager):
|
|||||||
change_message=change_message,
|
change_message=change_message,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def log_actions(
|
||||||
|
self, user_id, queryset, action_flag, change_message="", *, single_object=False
|
||||||
|
):
|
||||||
|
# RemovedInDjango60Warning.
|
||||||
|
if type(self).log_action != LogEntryManager.log_action:
|
||||||
|
warnings.warn(
|
||||||
|
"The usage of log_action() is deprecated. Implement log_actions() "
|
||||||
|
"instead.",
|
||||||
|
RemovedInDjango60Warning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
self.log_action(
|
||||||
|
user_id=user_id,
|
||||||
|
content_type_id=ContentType.objects.get_for_model(
|
||||||
|
obj, for_concrete_model=False
|
||||||
|
).id,
|
||||||
|
object_id=obj.pk,
|
||||||
|
object_repr=str(obj),
|
||||||
|
action_flag=action_flag,
|
||||||
|
change_message=change_message,
|
||||||
|
)
|
||||||
|
for obj in queryset
|
||||||
|
]
|
||||||
|
|
||||||
|
if isinstance(change_message, list):
|
||||||
|
change_message = json.dumps(change_message)
|
||||||
|
|
||||||
|
log_entry_list = [
|
||||||
|
self.model(
|
||||||
|
user_id=user_id,
|
||||||
|
content_type_id=ContentType.objects.get_for_model(
|
||||||
|
obj, for_concrete_model=False
|
||||||
|
).id,
|
||||||
|
object_id=obj.pk,
|
||||||
|
object_repr=str(obj)[:200],
|
||||||
|
action_flag=action_flag,
|
||||||
|
change_message=change_message,
|
||||||
|
)
|
||||||
|
for obj in queryset
|
||||||
|
]
|
||||||
|
|
||||||
|
if single_object and log_entry_list:
|
||||||
|
instance = log_entry_list[0]
|
||||||
|
instance.save()
|
||||||
|
return instance
|
||||||
|
|
||||||
|
return self.model.objects.bulk_create(log_entry_list)
|
||||||
|
|
||||||
|
|
||||||
class LogEntry(models.Model):
|
class LogEntry(models.Model):
|
||||||
action_time = models.DateTimeField(
|
action_time = models.DateTimeField(
|
||||||
|
@ -2,6 +2,7 @@ import copy
|
|||||||
import enum
|
import enum
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
import warnings
|
||||||
from functools import partial, update_wrapper
|
from functools import partial, update_wrapper
|
||||||
from urllib.parse import parse_qsl
|
from urllib.parse import parse_qsl
|
||||||
from urllib.parse import quote as urlquote
|
from urllib.parse import quote as urlquote
|
||||||
@ -54,6 +55,7 @@ from django.http.response import HttpResponseBase
|
|||||||
from django.template.response import SimpleTemplateResponse, TemplateResponse
|
from django.template.response import SimpleTemplateResponse, TemplateResponse
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.utils.deprecation import RemovedInDjango60Warning
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
@ -945,13 +947,12 @@ class ModelAdmin(BaseModelAdmin):
|
|||||||
"""
|
"""
|
||||||
from django.contrib.admin.models import ADDITION, LogEntry
|
from django.contrib.admin.models import ADDITION, LogEntry
|
||||||
|
|
||||||
return LogEntry.objects.log_action(
|
return LogEntry.objects.log_actions(
|
||||||
user_id=request.user.pk,
|
user_id=request.user.pk,
|
||||||
content_type_id=get_content_type_for_model(obj).pk,
|
queryset=[obj],
|
||||||
object_id=obj.pk,
|
|
||||||
object_repr=str(obj),
|
|
||||||
action_flag=ADDITION,
|
action_flag=ADDITION,
|
||||||
change_message=message,
|
change_message=message,
|
||||||
|
single_object=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def log_change(self, request, obj, message):
|
def log_change(self, request, obj, message):
|
||||||
@ -962,13 +963,12 @@ class ModelAdmin(BaseModelAdmin):
|
|||||||
"""
|
"""
|
||||||
from django.contrib.admin.models import CHANGE, LogEntry
|
from django.contrib.admin.models import CHANGE, LogEntry
|
||||||
|
|
||||||
return LogEntry.objects.log_action(
|
return LogEntry.objects.log_actions(
|
||||||
user_id=request.user.pk,
|
user_id=request.user.pk,
|
||||||
content_type_id=get_content_type_for_model(obj).pk,
|
queryset=[obj],
|
||||||
object_id=obj.pk,
|
|
||||||
object_repr=str(obj),
|
|
||||||
action_flag=CHANGE,
|
action_flag=CHANGE,
|
||||||
change_message=message,
|
change_message=message,
|
||||||
|
single_object=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def log_deletion(self, request, obj, object_repr):
|
def log_deletion(self, request, obj, object_repr):
|
||||||
@ -978,6 +978,11 @@ class ModelAdmin(BaseModelAdmin):
|
|||||||
|
|
||||||
The default implementation creates an admin LogEntry object.
|
The default implementation creates an admin LogEntry object.
|
||||||
"""
|
"""
|
||||||
|
warnings.warn(
|
||||||
|
"ModelAdmin.log_deletion() is deprecated. Use log_deletions() instead.",
|
||||||
|
RemovedInDjango60Warning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
from django.contrib.admin.models import DELETION, LogEntry
|
from django.contrib.admin.models import DELETION, LogEntry
|
||||||
|
|
||||||
return LogEntry.objects.log_action(
|
return LogEntry.objects.log_action(
|
||||||
@ -988,6 +993,31 @@ class ModelAdmin(BaseModelAdmin):
|
|||||||
action_flag=DELETION,
|
action_flag=DELETION,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def log_deletions(self, request, queryset):
|
||||||
|
"""
|
||||||
|
Log that objects will be deleted. Note that this method must be called
|
||||||
|
before the deletion.
|
||||||
|
|
||||||
|
The default implementation creates admin LogEntry objects.
|
||||||
|
"""
|
||||||
|
from django.contrib.admin.models import DELETION, LogEntry
|
||||||
|
|
||||||
|
# RemovedInDjango60Warning.
|
||||||
|
if type(self).log_deletion != ModelAdmin.log_deletion:
|
||||||
|
warnings.warn(
|
||||||
|
"The usage of log_deletion() is deprecated. Implement log_deletions() "
|
||||||
|
"instead.",
|
||||||
|
RemovedInDjango60Warning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return [self.log_deletion(request, obj, str(obj)) for obj in queryset]
|
||||||
|
|
||||||
|
return LogEntry.objects.log_actions(
|
||||||
|
user_id=request.user.pk,
|
||||||
|
queryset=queryset,
|
||||||
|
action_flag=DELETION,
|
||||||
|
)
|
||||||
|
|
||||||
def action_checkbox(self, obj):
|
def action_checkbox(self, obj):
|
||||||
"""
|
"""
|
||||||
A list_display column containing a checkbox widget.
|
A list_display column containing a checkbox widget.
|
||||||
@ -2174,7 +2204,7 @@ class ModelAdmin(BaseModelAdmin):
|
|||||||
obj_display = str(obj)
|
obj_display = str(obj)
|
||||||
attr = str(to_field) if to_field else self.opts.pk.attname
|
attr = str(to_field) if to_field else self.opts.pk.attname
|
||||||
obj_id = obj.serializable_value(attr)
|
obj_id = obj.serializable_value(attr)
|
||||||
self.log_deletion(request, obj, obj_display)
|
self.log_deletions(request, [obj])
|
||||||
self.delete_model(request, obj)
|
self.delete_model(request, obj)
|
||||||
|
|
||||||
return self.response_delete(request, obj_display, obj_id)
|
return self.response_delete(request, obj_display, obj_id)
|
||||||
|
@ -56,7 +56,8 @@ details on these changes.
|
|||||||
See the :ref:`Django 5.1 release notes <deprecated-features-5.1>` for more
|
See the :ref:`Django 5.1 release notes <deprecated-features-5.1>` for more
|
||||||
details on these changes.
|
details on these changes.
|
||||||
|
|
||||||
* ...
|
* The ``ModelAdmin.log_deletion()`` and ``LogEntryManager.log_action()``
|
||||||
|
methods will be removed.
|
||||||
|
|
||||||
.. _deprecation-removed-in-5.1:
|
.. _deprecation-removed-in-5.1:
|
||||||
|
|
||||||
|
@ -285,7 +285,10 @@ Features deprecated in 5.1
|
|||||||
Miscellaneous
|
Miscellaneous
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
* ...
|
* The ``ModelAdmin.log_deletion()`` and ``LogEntryManager.log_action()``
|
||||||
|
methods are deprecated. Subclasses should implement
|
||||||
|
``ModelAdmin.log_deletions()`` and ``LogEntryManager.log_actions()``
|
||||||
|
instead.
|
||||||
|
|
||||||
Features removed in 5.1
|
Features removed in 5.1
|
||||||
=======================
|
=======================
|
||||||
|
@ -16,7 +16,6 @@ from django.contrib.admin.views.main import (
|
|||||||
TO_FIELD_VAR,
|
TO_FIELD_VAR,
|
||||||
)
|
)
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.contrib.messages.storage.cookie import CookieStorage
|
from django.contrib.messages.storage.cookie import CookieStorage
|
||||||
from django.db import DatabaseError, connection, models
|
from django.db import DatabaseError, connection, models
|
||||||
from django.db.models import F, Field, IntegerField
|
from django.db.models import F, Field, IntegerField
|
||||||
@ -1636,8 +1635,7 @@ class GetAdminLogTests(TestCase):
|
|||||||
"""{% get_admin_log %} works without specifying a user."""
|
"""{% get_admin_log %} works without specifying a user."""
|
||||||
user = User(username="jondoe", password="secret", email="super@example.com")
|
user = User(username="jondoe", password="secret", email="super@example.com")
|
||||||
user.save()
|
user.save()
|
||||||
ct = ContentType.objects.get_for_model(User)
|
LogEntry.objects.log_actions(user.pk, [user], 1, single_object=True)
|
||||||
LogEntry.objects.log_action(user.pk, ct.pk, user.pk, repr(user), 1)
|
|
||||||
context = Context({"log_entries": LogEntry.objects.all()})
|
context = Context({"log_entries": LogEntry.objects.all()})
|
||||||
t = Template(
|
t = Template(
|
||||||
"{% load log %}"
|
"{% load log %}"
|
||||||
@ -1646,7 +1644,7 @@ class GetAdminLogTests(TestCase):
|
|||||||
"{{ entry|safe }}"
|
"{{ entry|safe }}"
|
||||||
"{% endfor %}"
|
"{% endfor %}"
|
||||||
)
|
)
|
||||||
self.assertEqual(t.render(context), "Added “<User: jondoe>”.")
|
self.assertEqual(t.render(context), "Added “jondoe”.")
|
||||||
|
|
||||||
def test_missing_args(self):
|
def test_missing_args(self):
|
||||||
msg = "'get_admin_log' statements require two arguments"
|
msg = "'get_admin_log' statements require two arguments"
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.contrib.admin.models import LogEntry, LogEntryManager
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@ -86,3 +87,26 @@ class VehicleMixin(Vehicle):
|
|||||||
|
|
||||||
class Car(VehicleMixin):
|
class Car(VehicleMixin):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InheritedLogEntryManager(LogEntryManager):
|
||||||
|
model = LogEntry
|
||||||
|
|
||||||
|
def log_action(
|
||||||
|
self,
|
||||||
|
user_id,
|
||||||
|
content_type_id,
|
||||||
|
object_id,
|
||||||
|
object_repr,
|
||||||
|
action_flag,
|
||||||
|
change_message="",
|
||||||
|
):
|
||||||
|
return LogEntry.objects.create(
|
||||||
|
user_id=user_id,
|
||||||
|
content_type_id=content_type_id,
|
||||||
|
object_id=str(object_id),
|
||||||
|
# Changing actual repr to test repr
|
||||||
|
object_repr="Test Repr",
|
||||||
|
action_flag=action_flag,
|
||||||
|
change_message=change_message,
|
||||||
|
)
|
||||||
|
@ -8,9 +8,10 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
|
from django.utils.deprecation import RemovedInDjango60Warning
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
|
|
||||||
from .models import Article, ArticleProxy, Car, Site
|
from .models import Article, ArticleProxy, Car, InheritedLogEntryManager, Site
|
||||||
|
|
||||||
|
|
||||||
@override_settings(ROOT_URLCONF="admin_utils.urls")
|
@override_settings(ROOT_URLCONF="admin_utils.urls")
|
||||||
@ -26,14 +27,22 @@ class LogEntryTests(TestCase):
|
|||||||
title="Title",
|
title="Title",
|
||||||
created=datetime(2008, 3, 12, 11, 54),
|
created=datetime(2008, 3, 12, 11, 54),
|
||||||
)
|
)
|
||||||
content_type_pk = ContentType.objects.get_for_model(Article).pk
|
cls.a2 = Article.objects.create(
|
||||||
LogEntry.objects.log_action(
|
site=cls.site,
|
||||||
|
title="Title 2",
|
||||||
|
created=datetime(2009, 3, 12, 11, 54),
|
||||||
|
)
|
||||||
|
cls.a3 = Article.objects.create(
|
||||||
|
site=cls.site,
|
||||||
|
title="Title 3",
|
||||||
|
created=datetime(2010, 3, 12, 11, 54),
|
||||||
|
)
|
||||||
|
LogEntry.objects.log_actions(
|
||||||
cls.user.pk,
|
cls.user.pk,
|
||||||
content_type_pk,
|
[cls.a1],
|
||||||
cls.a1.pk,
|
|
||||||
repr(cls.a1),
|
|
||||||
CHANGE,
|
CHANGE,
|
||||||
change_message="Changed something",
|
change_message="Changed something",
|
||||||
|
single_object=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -227,18 +236,95 @@ class LogEntryTests(TestCase):
|
|||||||
logentry = LogEntry.objects.first()
|
logentry = LogEntry.objects.first()
|
||||||
self.assertEqual(repr(logentry), str(logentry.action_time))
|
self.assertEqual(repr(logentry), str(logentry.action_time))
|
||||||
|
|
||||||
|
# RemovedInDjango60Warning.
|
||||||
def test_log_action(self):
|
def test_log_action(self):
|
||||||
content_type_pk = ContentType.objects.get_for_model(Article).pk
|
msg = "LogEntryManager.log_action() is deprecated. Use log_actions() instead."
|
||||||
log_entry = LogEntry.objects.log_action(
|
content_type_val = ContentType.objects.get_for_model(Article).pk
|
||||||
self.user.pk,
|
with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
|
||||||
content_type_pk,
|
log_entry = LogEntry.objects.log_action(
|
||||||
self.a1.pk,
|
self.user.pk,
|
||||||
repr(self.a1),
|
content_type_val,
|
||||||
CHANGE,
|
self.a1.pk,
|
||||||
change_message="Changed something else",
|
repr(self.a1),
|
||||||
)
|
CHANGE,
|
||||||
|
change_message="Changed something else",
|
||||||
|
)
|
||||||
self.assertEqual(log_entry, LogEntry.objects.latest("id"))
|
self.assertEqual(log_entry, LogEntry.objects.latest("id"))
|
||||||
|
|
||||||
|
def test_log_actions(self):
|
||||||
|
queryset = Article.objects.all().order_by("-id")
|
||||||
|
msg = "Deleted Something"
|
||||||
|
content_type = ContentType.objects.get_for_model(self.a1)
|
||||||
|
self.assertEqual(len(queryset), 3)
|
||||||
|
with self.assertNumQueries(1):
|
||||||
|
LogEntry.objects.log_actions(
|
||||||
|
self.user.pk,
|
||||||
|
queryset,
|
||||||
|
DELETION,
|
||||||
|
change_message=msg,
|
||||||
|
)
|
||||||
|
logs = (
|
||||||
|
LogEntry.objects.filter(action_flag=DELETION)
|
||||||
|
.order_by("id")
|
||||||
|
.values_list(
|
||||||
|
"user",
|
||||||
|
"content_type",
|
||||||
|
"object_id",
|
||||||
|
"object_repr",
|
||||||
|
"action_flag",
|
||||||
|
"change_message",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
expected_log_values = [
|
||||||
|
(
|
||||||
|
self.user.pk,
|
||||||
|
content_type.id,
|
||||||
|
str(obj.pk),
|
||||||
|
str(obj),
|
||||||
|
DELETION,
|
||||||
|
msg,
|
||||||
|
)
|
||||||
|
for obj in queryset
|
||||||
|
]
|
||||||
|
self.assertSequenceEqual(logs, expected_log_values)
|
||||||
|
|
||||||
|
# RemovedInDjango60Warning.
|
||||||
|
def test_log_action_fallback(self):
|
||||||
|
LogEntry.objects2 = InheritedLogEntryManager()
|
||||||
|
queryset = Article.objects.all().order_by("-id")
|
||||||
|
content_type = ContentType.objects.get_for_model(self.a1)
|
||||||
|
self.assertEqual(len(queryset), 3)
|
||||||
|
msg = (
|
||||||
|
"The usage of log_action() is deprecated. Implement log_actions() instead."
|
||||||
|
)
|
||||||
|
with self.assertNumQueries(3):
|
||||||
|
with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
|
||||||
|
LogEntry.objects2.log_actions(self.user.pk, queryset, DELETION)
|
||||||
|
log_values = (
|
||||||
|
LogEntry.objects.filter(action_flag=DELETION)
|
||||||
|
.order_by("id")
|
||||||
|
.values_list(
|
||||||
|
"user",
|
||||||
|
"content_type",
|
||||||
|
"object_id",
|
||||||
|
"object_repr",
|
||||||
|
"action_flag",
|
||||||
|
"change_message",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
expected_log_values = [
|
||||||
|
(
|
||||||
|
self.user.pk,
|
||||||
|
content_type.id,
|
||||||
|
str(obj.pk),
|
||||||
|
"Test Repr",
|
||||||
|
DELETION,
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
for obj in queryset
|
||||||
|
]
|
||||||
|
self.assertSequenceEqual(log_values, expected_log_values)
|
||||||
|
|
||||||
def test_recentactions_without_content_type(self):
|
def test_recentactions_without_content_type(self):
|
||||||
"""
|
"""
|
||||||
If a LogEntry is missing content_type it will not display it in span
|
If a LogEntry is missing content_type it will not display it in span
|
||||||
@ -248,7 +334,7 @@ class LogEntryTests(TestCase):
|
|||||||
link = reverse("admin:admin_utils_article_change", args=(quote(self.a1.pk),))
|
link = reverse("admin:admin_utils_article_change", args=(quote(self.a1.pk),))
|
||||||
should_contain = """<a href="%s">%s</a>""" % (
|
should_contain = """<a href="%s">%s</a>""" % (
|
||||||
escape(link),
|
escape(link),
|
||||||
escape(repr(self.a1)),
|
escape(str(self.a1)),
|
||||||
)
|
)
|
||||||
self.assertContains(response, should_contain)
|
self.assertContains(response, should_contain)
|
||||||
should_contain = "Article"
|
should_contain = "Article"
|
||||||
@ -320,28 +406,29 @@ class LogEntryTests(TestCase):
|
|||||||
self.assertEqual(log.get_action_flag_display(), display_name)
|
self.assertEqual(log.get_action_flag_display(), display_name)
|
||||||
|
|
||||||
def test_hook_get_log_entries(self):
|
def test_hook_get_log_entries(self):
|
||||||
LogEntry.objects.log_action(
|
LogEntry.objects.log_actions(
|
||||||
self.user.pk,
|
self.user.pk,
|
||||||
ContentType.objects.get_for_model(Article).pk,
|
[self.a1],
|
||||||
self.a1.pk,
|
|
||||||
"Article changed",
|
|
||||||
CHANGE,
|
CHANGE,
|
||||||
change_message="Article changed message",
|
change_message="Article changed message",
|
||||||
|
single_object=True,
|
||||||
)
|
)
|
||||||
c1 = Car.objects.create()
|
c1 = Car.objects.create()
|
||||||
LogEntry.objects.log_action(
|
LogEntry.objects.log_actions(
|
||||||
self.user.pk,
|
self.user.pk,
|
||||||
ContentType.objects.get_for_model(Car).pk,
|
[c1],
|
||||||
c1.pk,
|
|
||||||
"Car created",
|
|
||||||
ADDITION,
|
ADDITION,
|
||||||
change_message="Car created message",
|
change_message="Car created message",
|
||||||
|
single_object=True,
|
||||||
)
|
)
|
||||||
|
exp_str_article = escape(str(self.a1))
|
||||||
|
exp_str_car = escape(str(c1))
|
||||||
|
|
||||||
response = self.client.get(reverse("admin:index"))
|
response = self.client.get(reverse("admin:index"))
|
||||||
self.assertContains(response, "Article changed")
|
self.assertContains(response, exp_str_article)
|
||||||
self.assertContains(response, "Car created")
|
self.assertContains(response, exp_str_car)
|
||||||
|
|
||||||
# site "custom_admin" only renders log entries of registered models
|
# site "custom_admin" only renders log entries of registered models
|
||||||
response = self.client.get(reverse("custom_admin:index"))
|
response = self.client.get(reverse("custom_admin:index"))
|
||||||
self.assertContains(response, "Article changed")
|
self.assertContains(response, exp_str_article)
|
||||||
self.assertNotContains(response, "Car created")
|
self.assertNotContains(response, exp_str_car)
|
||||||
|
@ -4,9 +4,11 @@ from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
|
|||||||
from django.contrib.admin.views.main import IS_POPUP_VAR
|
from django.contrib.admin.views.main import IS_POPUP_VAR
|
||||||
from django.contrib.auth.models import Permission, User
|
from django.contrib.auth.models import Permission, User
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
|
from django.db import connection
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
from django.test.utils import CaptureQueriesContext
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from .admin import SubscriberAdmin
|
from .admin import SubscriberAdmin
|
||||||
@ -74,8 +76,21 @@ class AdminActionsTest(TestCase):
|
|||||||
self.assertContains(confirmation, "<li>Subscribers: 2</li>")
|
self.assertContains(confirmation, "<li>Subscribers: 2</li>")
|
||||||
self.assertContains(confirmation, "<li>External subscribers: 1</li>")
|
self.assertContains(confirmation, "<li>External subscribers: 1</li>")
|
||||||
self.assertContains(confirmation, ACTION_CHECKBOX_NAME, count=2)
|
self.assertContains(confirmation, ACTION_CHECKBOX_NAME, count=2)
|
||||||
self.client.post(
|
with CaptureQueriesContext(connection) as ctx:
|
||||||
reverse("admin:admin_views_subscriber_changelist"), delete_confirmation_data
|
self.client.post(
|
||||||
|
reverse("admin:admin_views_subscriber_changelist"),
|
||||||
|
delete_confirmation_data,
|
||||||
|
)
|
||||||
|
# Log entries are inserted in bulk.
|
||||||
|
self.assertEqual(
|
||||||
|
len(
|
||||||
|
[
|
||||||
|
q["sql"]
|
||||||
|
for q in ctx.captured_queries
|
||||||
|
if q["sql"].startswith("INSERT")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
1,
|
||||||
)
|
)
|
||||||
self.assertEqual(Subscriber.objects.count(), 0)
|
self.assertEqual(Subscriber.objects.count(), 0)
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from django.contrib.admin.models import CHANGE, LogEntry
|
from django.contrib.admin.models import CHANGE, LogEntry
|
||||||
from django.contrib.admin.tests import AdminSeleniumTestCase
|
from django.contrib.admin.tests import AdminSeleniumTestCase
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -61,15 +60,13 @@ class SeleniumTests(AdminSeleniumTestCase):
|
|||||||
password="secret",
|
password="secret",
|
||||||
email="super@example.com",
|
email="super@example.com",
|
||||||
)
|
)
|
||||||
content_type_pk = ContentType.objects.get_for_model(User).pk
|
|
||||||
for i in range(1, 1101):
|
for i in range(1, 1101):
|
||||||
LogEntry.objects.log_action(
|
LogEntry.objects.log_actions(
|
||||||
self.superuser.pk,
|
self.superuser.pk,
|
||||||
content_type_pk,
|
[self.superuser],
|
||||||
self.superuser.pk,
|
|
||||||
repr(self.superuser),
|
|
||||||
CHANGE,
|
CHANGE,
|
||||||
change_message=f"Changed something {i}",
|
change_message=f"Changed something {i}",
|
||||||
|
single_object=True,
|
||||||
)
|
)
|
||||||
self.admin_login(
|
self.admin_login(
|
||||||
username="super",
|
username="super",
|
||||||
|
@ -3808,33 +3808,27 @@ class AdminViewStringPrimaryKeyTest(TestCase):
|
|||||||
r"""-_.!~*'() ;/?:@&=+$, <>#%" {}|\^[]`"""
|
r"""-_.!~*'() ;/?:@&=+$, <>#%" {}|\^[]`"""
|
||||||
)
|
)
|
||||||
cls.m1 = ModelWithStringPrimaryKey.objects.create(string_pk=cls.pk)
|
cls.m1 = ModelWithStringPrimaryKey.objects.create(string_pk=cls.pk)
|
||||||
content_type_pk = ContentType.objects.get_for_model(
|
|
||||||
ModelWithStringPrimaryKey
|
|
||||||
).pk
|
|
||||||
user_pk = cls.superuser.pk
|
user_pk = cls.superuser.pk
|
||||||
LogEntry.objects.log_action(
|
LogEntry.objects.log_actions(
|
||||||
user_pk,
|
user_pk,
|
||||||
content_type_pk,
|
[cls.m1],
|
||||||
cls.pk,
|
|
||||||
cls.pk,
|
|
||||||
2,
|
2,
|
||||||
change_message="Changed something",
|
change_message="Changed something",
|
||||||
|
single_object=True,
|
||||||
)
|
)
|
||||||
LogEntry.objects.log_action(
|
LogEntry.objects.log_actions(
|
||||||
user_pk,
|
user_pk,
|
||||||
content_type_pk,
|
[cls.m1],
|
||||||
cls.pk,
|
|
||||||
cls.pk,
|
|
||||||
1,
|
1,
|
||||||
change_message="Added something",
|
change_message="Added something",
|
||||||
|
single_object=True,
|
||||||
)
|
)
|
||||||
LogEntry.objects.log_action(
|
LogEntry.objects.log_actions(
|
||||||
user_pk,
|
user_pk,
|
||||||
content_type_pk,
|
[cls.m1],
|
||||||
cls.pk,
|
|
||||||
cls.pk,
|
|
||||||
3,
|
3,
|
||||||
change_message="Deleted something",
|
change_message="Deleted something",
|
||||||
|
single_object=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -833,12 +833,59 @@ class ModelAdminTests(TestCase):
|
|||||||
self.assertEqual(fetched.change_message, str(message))
|
self.assertEqual(fetched.change_message, str(message))
|
||||||
self.assertEqual(fetched.object_repr, str(self.band))
|
self.assertEqual(fetched.object_repr, str(self.band))
|
||||||
|
|
||||||
|
def test_log_deletions(self):
|
||||||
|
ma = ModelAdmin(Band, self.site)
|
||||||
|
mock_request = MockRequest()
|
||||||
|
mock_request.user = User.objects.create(username="akash")
|
||||||
|
content_type = get_content_type_for_model(self.band)
|
||||||
|
Band.objects.create(
|
||||||
|
name="The Beatles",
|
||||||
|
bio="A legendary rock band from Liverpool.",
|
||||||
|
sign_date=date(1962, 1, 1),
|
||||||
|
)
|
||||||
|
Band.objects.create(
|
||||||
|
name="Mohiner Ghoraguli",
|
||||||
|
bio="A progressive rock band from Calcutta.",
|
||||||
|
sign_date=date(1975, 1, 1),
|
||||||
|
)
|
||||||
|
queryset = Band.objects.all().order_by("-id")[:3]
|
||||||
|
self.assertEqual(len(queryset), 3)
|
||||||
|
with self.assertNumQueries(1):
|
||||||
|
ma.log_deletions(mock_request, queryset)
|
||||||
|
logs = (
|
||||||
|
LogEntry.objects.filter(action_flag=DELETION)
|
||||||
|
.order_by("id")
|
||||||
|
.values_list(
|
||||||
|
"user_id",
|
||||||
|
"content_type",
|
||||||
|
"object_id",
|
||||||
|
"object_repr",
|
||||||
|
"action_flag",
|
||||||
|
"change_message",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
expected_log_values = [
|
||||||
|
(
|
||||||
|
mock_request.user.id,
|
||||||
|
content_type.id,
|
||||||
|
str(obj.pk),
|
||||||
|
str(obj),
|
||||||
|
DELETION,
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
for obj in queryset
|
||||||
|
]
|
||||||
|
self.assertSequenceEqual(logs, expected_log_values)
|
||||||
|
|
||||||
|
# RemovedInDjango60Warning.
|
||||||
def test_log_deletion(self):
|
def test_log_deletion(self):
|
||||||
ma = ModelAdmin(Band, self.site)
|
ma = ModelAdmin(Band, self.site)
|
||||||
mock_request = MockRequest()
|
mock_request = MockRequest()
|
||||||
mock_request.user = User.objects.create(username="bill")
|
mock_request.user = User.objects.create(username="bill")
|
||||||
content_type = get_content_type_for_model(self.band)
|
content_type = get_content_type_for_model(self.band)
|
||||||
created = ma.log_deletion(mock_request, self.band, str(self.band))
|
msg = "ModelAdmin.log_deletion() is deprecated. Use log_deletions() instead."
|
||||||
|
with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
|
||||||
|
created = ma.log_deletion(mock_request, self.band, str(self.band))
|
||||||
fetched = LogEntry.objects.filter(action_flag=DELETION).latest("id")
|
fetched = LogEntry.objects.filter(action_flag=DELETION).latest("id")
|
||||||
self.assertEqual(created, fetched)
|
self.assertEqual(created, fetched)
|
||||||
self.assertEqual(fetched.action_flag, DELETION)
|
self.assertEqual(fetched.action_flag, DELETION)
|
||||||
@ -848,6 +895,60 @@ class ModelAdminTests(TestCase):
|
|||||||
self.assertEqual(fetched.change_message, "")
|
self.assertEqual(fetched.change_message, "")
|
||||||
self.assertEqual(fetched.object_repr, str(self.band))
|
self.assertEqual(fetched.object_repr, str(self.band))
|
||||||
|
|
||||||
|
# RemovedInDjango60Warning.
|
||||||
|
def test_log_deletion_fallback(self):
|
||||||
|
class InheritedModelAdmin(ModelAdmin):
|
||||||
|
def log_deletion(self, request, obj, object_repr):
|
||||||
|
return super().log_deletion(request, obj, object_repr)
|
||||||
|
|
||||||
|
ima = InheritedModelAdmin(Band, self.site)
|
||||||
|
mock_request = MockRequest()
|
||||||
|
mock_request.user = User.objects.create(username="akash")
|
||||||
|
content_type = get_content_type_for_model(self.band)
|
||||||
|
Band.objects.create(
|
||||||
|
name="The Beatles",
|
||||||
|
bio="A legendary rock band from Liverpool.",
|
||||||
|
sign_date=date(1962, 1, 1),
|
||||||
|
)
|
||||||
|
Band.objects.create(
|
||||||
|
name="Mohiner Ghoraguli",
|
||||||
|
bio="A progressive rock band from Calcutta.",
|
||||||
|
sign_date=date(1975, 1, 1),
|
||||||
|
)
|
||||||
|
queryset = Band.objects.all().order_by("-id")[:3]
|
||||||
|
self.assertEqual(len(queryset), 3)
|
||||||
|
msg = (
|
||||||
|
"The usage of log_deletion() is deprecated. Implement log_deletions() "
|
||||||
|
"instead."
|
||||||
|
)
|
||||||
|
with self.assertNumQueries(3):
|
||||||
|
with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
|
||||||
|
ima.log_deletions(mock_request, queryset)
|
||||||
|
logs = (
|
||||||
|
LogEntry.objects.filter(action_flag=DELETION)
|
||||||
|
.order_by("id")
|
||||||
|
.values_list(
|
||||||
|
"user_id",
|
||||||
|
"content_type",
|
||||||
|
"object_id",
|
||||||
|
"object_repr",
|
||||||
|
"action_flag",
|
||||||
|
"change_message",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
expected_log_values = [
|
||||||
|
(
|
||||||
|
mock_request.user.id,
|
||||||
|
content_type.id,
|
||||||
|
str(obj.pk),
|
||||||
|
str(obj),
|
||||||
|
DELETION,
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
for obj in queryset
|
||||||
|
]
|
||||||
|
self.assertSequenceEqual(logs, expected_log_values)
|
||||||
|
|
||||||
def test_get_autocomplete_fields(self):
|
def test_get_autocomplete_fields(self):
|
||||||
class NameAdmin(ModelAdmin):
|
class NameAdmin(ModelAdmin):
|
||||||
search_fields = ["name"]
|
search_fields = ["name"]
|
||||||
|
Loading…
Reference in New Issue
Block a user