models.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. import json
  2. import warnings
  3. from django.conf import settings
  4. from django.contrib.admin.utils import quote
  5. from django.contrib.contenttypes.models import ContentType
  6. from django.db import models
  7. from django.urls import NoReverseMatch, reverse
  8. from django.utils import timezone
  9. from django.utils.deprecation import RemovedInDjango60Warning
  10. from django.utils.text import get_text_list
  11. from django.utils.translation import gettext
  12. from django.utils.translation import gettext_lazy as _
  13. ADDITION = 1
  14. CHANGE = 2
  15. DELETION = 3
  16. ACTION_FLAG_CHOICES = [
  17. (ADDITION, _("Addition")),
  18. (CHANGE, _("Change")),
  19. (DELETION, _("Deletion")),
  20. ]
  21. class LogEntryManager(models.Manager):
  22. use_in_migrations = True
  23. def log_action(
  24. self,
  25. user_id,
  26. content_type_id,
  27. object_id,
  28. object_repr,
  29. action_flag,
  30. change_message="",
  31. ):
  32. warnings.warn(
  33. "LogEntryManager.log_action() is deprecated. Use log_actions() instead.",
  34. RemovedInDjango60Warning,
  35. stacklevel=2,
  36. )
  37. if isinstance(change_message, list):
  38. change_message = json.dumps(change_message)
  39. return self.model.objects.create(
  40. user_id=user_id,
  41. content_type_id=content_type_id,
  42. object_id=str(object_id),
  43. object_repr=object_repr[:200],
  44. action_flag=action_flag,
  45. change_message=change_message,
  46. )
  47. def log_actions(
  48. self, user_id, queryset, action_flag, change_message="", *, single_object=False
  49. ):
  50. # RemovedInDjango60Warning.
  51. if type(self).log_action != LogEntryManager.log_action:
  52. warnings.warn(
  53. "The usage of log_action() is deprecated. Implement log_actions() "
  54. "instead.",
  55. RemovedInDjango60Warning,
  56. stacklevel=2,
  57. )
  58. return [
  59. self.log_action(
  60. user_id=user_id,
  61. content_type_id=ContentType.objects.get_for_model(
  62. obj, for_concrete_model=False
  63. ).id,
  64. object_id=obj.pk,
  65. object_repr=str(obj),
  66. action_flag=action_flag,
  67. change_message=change_message,
  68. )
  69. for obj in queryset
  70. ]
  71. if isinstance(change_message, list):
  72. change_message = json.dumps(change_message)
  73. log_entry_list = [
  74. self.model(
  75. user_id=user_id,
  76. content_type_id=ContentType.objects.get_for_model(
  77. obj, for_concrete_model=False
  78. ).id,
  79. object_id=obj.pk,
  80. object_repr=str(obj)[:200],
  81. action_flag=action_flag,
  82. change_message=change_message,
  83. )
  84. for obj in queryset
  85. ]
  86. if single_object and log_entry_list:
  87. instance = log_entry_list[0]
  88. instance.save()
  89. return instance
  90. return self.model.objects.bulk_create(log_entry_list)
  91. class LogEntry(models.Model):
  92. action_time = models.DateTimeField(
  93. _("action time"),
  94. default=timezone.now,
  95. editable=False,
  96. )
  97. user = models.ForeignKey(
  98. settings.AUTH_USER_MODEL,
  99. models.CASCADE,
  100. verbose_name=_("user"),
  101. )
  102. content_type = models.ForeignKey(
  103. ContentType,
  104. models.SET_NULL,
  105. verbose_name=_("content type"),
  106. blank=True,
  107. null=True,
  108. )
  109. object_id = models.TextField(_("object id"), blank=True, null=True)
  110. # Translators: 'repr' means representation
  111. # (https://docs.python.org/library/functions.html#repr)
  112. object_repr = models.CharField(_("object repr"), max_length=200)
  113. action_flag = models.PositiveSmallIntegerField(
  114. _("action flag"), choices=ACTION_FLAG_CHOICES
  115. )
  116. # change_message is either a string or a JSON structure
  117. change_message = models.TextField(_("change message"), blank=True)
  118. objects = LogEntryManager()
  119. class Meta:
  120. verbose_name = _("log entry")
  121. verbose_name_plural = _("log entries")
  122. db_table = "django_admin_log"
  123. ordering = ["-action_time"]
  124. def __repr__(self):
  125. return str(self.action_time)
  126. def __str__(self):
  127. if self.is_addition():
  128. return gettext("Added “%(object)s”.") % {"object": self.object_repr}
  129. elif self.is_change():
  130. return gettext("Changed “%(object)s” — %(changes)s") % {
  131. "object": self.object_repr,
  132. "changes": self.get_change_message(),
  133. }
  134. elif self.is_deletion():
  135. return gettext("Deleted “%(object)s.”") % {"object": self.object_repr}
  136. return gettext("LogEntry Object")
  137. def is_addition(self):
  138. return self.action_flag == ADDITION
  139. def is_change(self):
  140. return self.action_flag == CHANGE
  141. def is_deletion(self):
  142. return self.action_flag == DELETION
  143. def get_change_message(self):
  144. """
  145. If self.change_message is a JSON structure, interpret it as a change
  146. string, properly translated.
  147. """
  148. if self.change_message and self.change_message[0] == "[":
  149. try:
  150. change_message = json.loads(self.change_message)
  151. except json.JSONDecodeError:
  152. return self.change_message
  153. messages = []
  154. for sub_message in change_message:
  155. if "added" in sub_message:
  156. if sub_message["added"]:
  157. sub_message["added"]["name"] = gettext(
  158. sub_message["added"]["name"]
  159. )
  160. messages.append(
  161. gettext("Added {name} “{object}”.").format(
  162. **sub_message["added"]
  163. )
  164. )
  165. else:
  166. messages.append(gettext("Added."))
  167. elif "changed" in sub_message:
  168. sub_message["changed"]["fields"] = get_text_list(
  169. [
  170. gettext(field_name)
  171. for field_name in sub_message["changed"]["fields"]
  172. ],
  173. gettext("and"),
  174. )
  175. if "name" in sub_message["changed"]:
  176. sub_message["changed"]["name"] = gettext(
  177. sub_message["changed"]["name"]
  178. )
  179. messages.append(
  180. gettext("Changed {fields} for {name} “{object}”.").format(
  181. **sub_message["changed"]
  182. )
  183. )
  184. else:
  185. messages.append(
  186. gettext("Changed {fields}.").format(
  187. **sub_message["changed"]
  188. )
  189. )
  190. elif "deleted" in sub_message:
  191. sub_message["deleted"]["name"] = gettext(
  192. sub_message["deleted"]["name"]
  193. )
  194. messages.append(
  195. gettext("Deleted {name} “{object}”.").format(
  196. **sub_message["deleted"]
  197. )
  198. )
  199. change_message = " ".join(msg[0].upper() + msg[1:] for msg in messages)
  200. return change_message or gettext("No fields changed.")
  201. else:
  202. return self.change_message
  203. def get_edited_object(self):
  204. """Return the edited object represented by this log entry."""
  205. return self.content_type.get_object_for_this_type(pk=self.object_id)
  206. def get_admin_url(self):
  207. """
  208. Return the admin URL to edit the object represented by this log entry.
  209. """
  210. if self.content_type and self.object_id:
  211. url_name = "admin:%s_%s_change" % (
  212. self.content_type.app_label,
  213. self.content_type.model,
  214. )
  215. try:
  216. return reverse(url_name, args=(quote(self.object_id),))
  217. except NoReverseMatch:
  218. pass
  219. return None