Merge commit 'ba19a8ca35894a9f4f36fe1efb6823ab0da910c6'

This commit is contained in:
Alieh Rymašeŭski 2022-06-13 14:01:50 +03:00
commit 4915969283
11 changed files with 348 additions and 13 deletions

View file

@ -13,7 +13,7 @@ jobs:
services:
postgres:
image: postgres:11
image: postgres:12
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres

View file

@ -1,8 +1,13 @@
# Changes
#### Improvements
- feat: Add register model from settings ([#368](https://github.com/jazzband/django-auditlog/pull/368))
#### Fixes
- Fix inconsistent changes with JSONField ([#355](https://github.com/jazzband/django-auditlog/pull/355))
- Disable `add` button in admin ui ([#378](https://github.com/jazzband/django-auditlog/pull/378))
## 2.0.0 (2022-05-09)

View file

@ -52,5 +52,9 @@ class LogEntryAdmin(admin.ModelAdmin, LogEntryAdminMixin):
show_full_result_count = False
paginator = TimeLimitedPaginator
def has_add_permission(self, request):
# As audit admin doesn't allow log creation from admin
return False
admin.site.register(LogEntry, LogEntryAdmin)

View file

@ -5,3 +5,8 @@ class AuditlogConfig(AppConfig):
name = "auditlog"
verbose_name = "Audit log"
default_auto_field = "django.db.models.AutoField"
def ready(self):
from auditlog.registry import auditlog
auditlog.register_from_settings()

17
auditlog/conf.py Normal file
View file

@ -0,0 +1,17 @@
from django.conf import settings
# Register all models when set to True
settings.AUDITLOG_INCLUDE_ALL_MODELS = getattr(
settings, "AUDITLOG_INCLUDE_ALL_MODELS", False
)
# Exclude models in registration process
# It will be considered when `AUDITLOG_INCLUDE_ALL_MODELS` is True
settings.AUDITLOG_EXCLUDE_TRACKING_MODELS = getattr(
settings, "AUDITLOG_EXCLUDE_TRACKING_MODELS", ()
)
# Register models and define their logging behaviour
settings.AUDITLOG_INCLUDE_TRACKING_MODELS = getattr(
settings, "AUDITLOG_INCLUDE_TRACKING_MODELS", ()
)

View file

@ -1,6 +1,18 @@
import copy
from collections import defaultdict
from typing import Callable, Collection, Dict, List, Optional, Tuple
from typing import (
Any,
Callable,
Collection,
Dict,
Iterable,
List,
Optional,
Tuple,
Union,
)
from django.apps import apps
from django.db.models import Model
from django.db.models.base import ModelBase
from django.db.models.signals import (
@ -11,6 +23,8 @@ from django.db.models.signals import (
pre_save,
)
from auditlog.conf import settings
DispatchUID = Tuple[int, int, int]
@ -19,6 +33,8 @@ class AuditlogModelRegistry:
A registry that keeps track of the models that use Auditlog to track changes.
"""
DEFAULT_EXCLUDE_MODELS = ("auditlog.LogEntry", "admin.LogEntry")
def __init__(
self,
create: bool = True,
@ -184,5 +200,92 @@ class AuditlogModelRegistry:
"""Generate a dispatch_uid which is unique for a combination of self, signal, and receiver."""
return id(self), id(signal), id(receiver)
def _get_model_classes(self, app_model: str) -> List[ModelBase]:
try:
try:
app_label, model_name = app_model.split(".")
return [apps.get_model(app_label, model_name)]
except ValueError:
return apps.get_app_config(app_model).get_models()
except LookupError:
return []
def _get_exclude_models(
self, exclude_tracking_models: Iterable[str]
) -> List[ModelBase]:
exclude_models = [
model
for app_model in exclude_tracking_models + self.DEFAULT_EXCLUDE_MODELS
for model in self._get_model_classes(app_model)
]
return exclude_models
def _register_models(self, models: Iterable[Union[str, Dict[str, Any]]]) -> None:
models = copy.deepcopy(models)
for model in models:
if isinstance(model, str):
for model_class in self._get_model_classes(model):
self.unregister(model_class)
self.register(model_class)
elif isinstance(model, dict):
model["model"] = self._get_model_classes(model["model"])[0]
self.unregister(model["model"])
self.register(**model)
def register_from_settings(self):
"""
Register models from settings variables
"""
if not isinstance(settings.AUDITLOG_INCLUDE_ALL_MODELS, bool):
raise TypeError("Setting 'AUDITLOG_INCLUDE_ALL_MODELS' must be a boolean")
if not isinstance(settings.AUDITLOG_EXCLUDE_TRACKING_MODELS, (list, tuple)):
raise TypeError(
"Setting 'AUDITLOG_EXCLUDE_TRACKING_MODELS' must be a list or tuple"
)
if (
not settings.AUDITLOG_INCLUDE_ALL_MODELS
and settings.AUDITLOG_EXCLUDE_TRACKING_MODELS
):
raise ValueError(
"In order to use setting 'AUDITLOG_EXCLUDE_TRACKING_MODELS', "
"setting 'AUDITLOG_INCLUDE_ALL_MODELS' must set to 'True'"
)
if not isinstance(settings.AUDITLOG_INCLUDE_TRACKING_MODELS, (list, tuple)):
raise TypeError(
"Setting 'AUDITLOG_INCLUDE_TRACKING_MODELS' must be a list or tuple"
)
for item in settings.AUDITLOG_INCLUDE_TRACKING_MODELS:
if not isinstance(item, (str, dict)):
raise TypeError(
"Setting 'AUDITLOG_INCLUDE_TRACKING_MODELS' items must be str or dict"
)
if isinstance(item, dict):
if "model" not in item:
raise ValueError(
"Setting 'AUDITLOG_INCLUDE_TRACKING_MODELS' dict items must contain 'model' key"
)
if "." not in item["model"]:
raise ValueError(
"Setting 'AUDITLOG_INCLUDE_TRACKING_MODELS' model must be in the format <app_name>.<model_name>"
)
if settings.AUDITLOG_INCLUDE_ALL_MODELS:
exclude_models = self._get_exclude_models(
settings.AUDITLOG_EXCLUDE_TRACKING_MODELS
)
models = apps.get_models()
for model in models:
if model in exclude_models:
continue
self.register(model)
self._register_models(settings.AUDITLOG_INCLUDE_TRACKING_MODELS)
auditlog = AuditlogModelRegistry()

View file

@ -4,19 +4,20 @@ import json
from unittest import mock
from dateutil.tz import gettz
from django.apps import apps
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.db.models.signals import pre_save
from django.test import RequestFactory, TestCase
from django.test import RequestFactory, TestCase, override_settings
from django.utils import dateformat, formats, timezone
from auditlog.context import set_actor
from auditlog.diff import model_instance_diff
from auditlog.middleware import AuditlogMiddleware
from auditlog.models import LogEntry
from auditlog.registry import auditlog
from auditlog.registry import AuditlogModelRegistry, auditlog
from auditlog_tests.models import (
AdditionalDataIncludedModel,
AltPrimaryKeyModel,
@ -929,6 +930,155 @@ class UnregisterTest(TestCase):
self.assertEqual(LogEntry.objects.count(), 0, msg="There are no log entries")
class RegisterModelSettingsTest(TestCase):
def setUp(self):
self.test_auditlog = AuditlogModelRegistry()
def tearDown(self):
for model in self.test_auditlog.get_models():
self.test_auditlog.unregister(model)
def test_get_model_classes(self):
self.assertEqual(
len(list(self.test_auditlog._get_model_classes("auditlog"))),
len(list(apps.get_app_config("auditlog").get_models())),
)
self.assertEqual([], self.test_auditlog._get_model_classes("fake_model"))
def test_get_exclude_models(self):
# By default it returns DEFAULT_EXCLUDE_MODELS
self.assertEqual(len(self.test_auditlog._get_exclude_models(())), 2)
# Exclude just one model
self.assertTrue(
SimpleExcludeModel
in self.test_auditlog._get_exclude_models(
("auditlog_tests.SimpleExcludeModel",)
)
)
# Exclude all model of an app
self.assertTrue(
SimpleExcludeModel
in self.test_auditlog._get_exclude_models(("auditlog_tests",))
)
def test_register_models_no_models(self):
self.test_auditlog._register_models(())
self.assertEqual(self.test_auditlog._registry, {})
def test_register_models_register_single_model(self):
self.test_auditlog._register_models(("auditlog_tests.SimpleExcludeModel",))
self.assertTrue(self.test_auditlog.contains(SimpleExcludeModel))
self.assertEqual(len(self.test_auditlog._registry), 1)
def test_register_models_register_app(self):
self.test_auditlog._register_models(("auditlog_tests",))
self.assertTrue(self.test_auditlog.contains(SimpleExcludeModel))
self.assertTrue(self.test_auditlog.contains(ChoicesFieldModel))
self.assertEqual(len(self.test_auditlog.get_models()), 19)
def test_register_models_register_model_with_attrs(self):
self.test_auditlog._register_models(
(
{
"model": "auditlog_tests.SimpleExcludeModel",
"include_fields": ["label"],
"exclude_fields": [
"text",
],
},
)
)
self.assertTrue(self.test_auditlog.contains(SimpleExcludeModel))
fields = self.test_auditlog.get_model_fields(SimpleExcludeModel)
self.assertEqual(fields["include_fields"], ["label"])
self.assertEqual(fields["exclude_fields"], ["text"])
def test_register_from_settings_invalid_settings(self):
with override_settings(AUDITLOG_INCLUDE_ALL_MODELS="str"):
with self.assertRaisesMessage(
TypeError, "Setting 'AUDITLOG_INCLUDE_ALL_MODELS' must be a boolean"
):
self.test_auditlog.register_from_settings()
with override_settings(AUDITLOG_EXCLUDE_TRACKING_MODELS="str"):
with self.assertRaisesMessage(
TypeError,
"Setting 'AUDITLOG_EXCLUDE_TRACKING_MODELS' must be a list or tuple",
):
self.test_auditlog.register_from_settings()
with override_settings(AUDITLOG_EXCLUDE_TRACKING_MODELS=("app1.model1",)):
with self.assertRaisesMessage(
ValueError,
"In order to use setting 'AUDITLOG_EXCLUDE_TRACKING_MODELS', "
"setting 'AUDITLOG_INCLUDE_ALL_MODELS' must set to 'True'",
):
self.test_auditlog.register_from_settings()
with override_settings(AUDITLOG_INCLUDE_TRACKING_MODELS="str"):
with self.assertRaisesMessage(
TypeError,
"Setting 'AUDITLOG_INCLUDE_TRACKING_MODELS' must be a list or tuple",
):
self.test_auditlog.register_from_settings()
with override_settings(AUDITLOG_INCLUDE_TRACKING_MODELS=(1, 2)):
with self.assertRaisesMessage(
TypeError,
"Setting 'AUDITLOG_INCLUDE_TRACKING_MODELS' items must be str or dict",
):
self.test_auditlog.register_from_settings()
with override_settings(AUDITLOG_INCLUDE_TRACKING_MODELS=({"test": "test"},)):
with self.assertRaisesMessage(
ValueError,
"Setting 'AUDITLOG_INCLUDE_TRACKING_MODELS' dict items must contain 'model' key",
):
self.test_auditlog.register_from_settings()
with override_settings(AUDITLOG_INCLUDE_TRACKING_MODELS=({"model": "test"},)):
with self.assertRaisesMessage(
ValueError,
"Setting 'AUDITLOG_INCLUDE_TRACKING_MODELS' model must be in the format <app_name>.<model_name>",
):
self.test_auditlog.register_from_settings()
@override_settings(
AUDITLOG_INCLUDE_ALL_MODELS=True,
AUDITLOG_EXCLUDE_TRACKING_MODELS=("auditlog_tests.SimpleExcludeModel",),
)
def test_register_from_settings_register_all_models_with_exclude_models(self):
self.test_auditlog.register_from_settings()
self.assertFalse(self.test_auditlog.contains(SimpleExcludeModel))
self.assertTrue(self.test_auditlog.contains(ChoicesFieldModel))
@override_settings(
AUDITLOG_INCLUDE_TRACKING_MODELS=(
{
"model": "auditlog_tests.SimpleExcludeModel",
"include_fields": ["label"],
"exclude_fields": [
"text",
],
},
)
)
def test_register_from_settings_register_models(self):
self.test_auditlog.register_from_settings()
self.assertTrue(self.test_auditlog.contains(SimpleExcludeModel))
fields = self.test_auditlog.get_model_fields(SimpleExcludeModel)
self.assertEqual(fields["include_fields"], ["label"])
self.assertEqual(fields["exclude_fields"], ["text"])
class ChoicesFieldModelTest(TestCase):
def setUp(self):
self.obj = ChoicesFieldModel.objects.create(
@ -1070,7 +1220,7 @@ class AdminPanelTest(TestCase):
res = self.client.get("/admin/auditlog/logentry/")
assert res.status_code == 200
res = self.client.get("/admin/auditlog/logentry/add/")
assert res.status_code == 200
assert res.status_code == 403
res = self.client.get(f"/admin/auditlog/logentry/{log_pk}/", follow=True)
assert res.status_code == 200
res = self.client.get(f"/admin/auditlog/logentry/{log_pk}/delete/")

View file

@ -9,7 +9,8 @@
import os
import sys
from datetime import date
from importlib.metadata import version
from pkg_resources import get_distribution
# -- Path setup --------------------------------------------------------------
@ -32,7 +33,7 @@ project = "django-auditlog"
author = "Jan-Jelle Kester and contributors"
copyright = f"2013-{date.today().year}, {author}"
release = version("django-auditlog")
release = get_distribution("django-auditlog").version
# for example take major/minor
version = ".".join(release.split(".")[:2])

View file

@ -91,6 +91,60 @@ For example, to mask the field ``address``, use::
Masking fields
Settings
--------
**AUDITLOG_INCLUDE_ALL_MODELS**
You can use this setting to register all your models:
.. code-block:: python
AUDITLOG_INCLUDE_ALL_MODELS=True
.. versionadded:: 2.1.0
**AUDITLOG_EXCLUDE_TRACKING_MODELS**
You can use this setting to exclude models in registration process.
It will be considered when ``AUDITLOG_INCLUDE_ALL_MODELS`` is `True`.
.. code-block:: python
AUDITLOG_EXCLUDE_TRACKING_MODELS = (
"<app_name>",
"<app_name>.<model>"
)
.. versionadded:: 2.1.0
**AUDITLOG_INCLUDE_TRACKING_MODELS**
You can use this setting to configure your models registration and other behaviours.
It must be a list or tuple. Each item in this setting can be a:
* ``str``: To register a model.
* ``dict``: To register a model and define its logging behaviour. e.g. include_fields, exclude_fields.
.. code-block:: python
AUDITLOG_INCLUDE_TRACKING_MODELS = (
"<appname>.<model1>",
{
"model": "<appname>.<model1>",
"include_fields": ["field1", "field2"],
"exclude_fields": ["field3", "field4"],
"mapping_fields": {
"field1": "FIELD",
},
"mask_fields": ["field5", "field6"],
},
"<appname>.<model3>",
)
.. versionadded:: 2.1.0
Actors
------

View file

@ -1,7 +1,3 @@
[build-system]
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2"]
build-backend = "setuptools.build_meta"
[tool.black]
target-version = ["py37"]

View file

@ -2,7 +2,7 @@
envlist =
{py37,py38,py39,py310}-django32
{py38,py39,py310}-django{40,main}
py38-docs
py37-docs
py38-lint
[testenv]
@ -33,7 +33,7 @@ basepython =
py38: python3.8
py37: python3.7
[testenv:py38-docs]
[testenv:py37-docs]
changedir = docs/source
deps = -rdocs/requirements.txt
commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html