django-field-audit 1.3.0__tar.gz → 1.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-field-audit might be problematic. Click here for more details.

Files changed (36) hide show
  1. {django_field_audit-1.3.0 → django_field_audit-1.4.0}/PKG-INFO +57 -39
  2. {django_field_audit-1.3.0 → django_field_audit-1.4.0}/README.md +47 -18
  3. django_field_audit-1.4.0/field_audit/__init__.py +4 -0
  4. {django_field_audit-1.3.0 → django_field_audit-1.4.0}/field_audit/field_audit.py +20 -14
  5. {django_field_audit-1.3.0 → django_field_audit-1.4.0}/field_audit/management/commands/bootstrap_field_audit_events.py +7 -4
  6. {django_field_audit-1.3.0 → django_field_audit-1.4.0}/field_audit/models.py +221 -179
  7. django_field_audit-1.4.0/field_audit/services.py +378 -0
  8. {django_field_audit-1.3.0 → django_field_audit-1.4.0}/field_audit/utils.py +5 -2
  9. django_field_audit-1.4.0/pyproject.toml +71 -0
  10. django_field_audit-1.3.0/django_field_audit.egg-info/PKG-INFO +0 -344
  11. django_field_audit-1.3.0/django_field_audit.egg-info/SOURCES.txt +0 -32
  12. django_field_audit-1.3.0/django_field_audit.egg-info/dependency_links.txt +0 -1
  13. django_field_audit-1.3.0/django_field_audit.egg-info/top_level.txt +0 -1
  14. django_field_audit-1.3.0/field_audit/__init__.py +0 -3
  15. django_field_audit-1.3.0/setup.cfg +0 -16
  16. django_field_audit-1.3.0/setup.py +0 -53
  17. django_field_audit-1.3.0/tests/test_apps.py +0 -18
  18. django_field_audit-1.3.0/tests/test_auditors.py +0 -238
  19. django_field_audit-1.3.0/tests/test_bootstrap_field_audit_events.py +0 -144
  20. django_field_audit-1.3.0/tests/test_django_compat.py +0 -148
  21. django_field_audit-1.3.0/tests/test_field_audit.py +0 -180
  22. django_field_audit-1.3.0/tests/test_m2m.py +0 -229
  23. django_field_audit-1.3.0/tests/test_middleware.py +0 -25
  24. django_field_audit-1.3.0/tests/test_models.py +0 -1451
  25. django_field_audit-1.3.0/tests/test_utils.py +0 -83
  26. {django_field_audit-1.3.0 → django_field_audit-1.4.0}/LICENSE +0 -0
  27. {django_field_audit-1.3.0 → django_field_audit-1.4.0}/field_audit/apps.py +0 -0
  28. {django_field_audit-1.3.0 → django_field_audit-1.4.0}/field_audit/auditors.py +0 -0
  29. {django_field_audit-1.3.0 → django_field_audit-1.4.0}/field_audit/const.py +0 -0
  30. {django_field_audit-1.3.0 → django_field_audit-1.4.0}/field_audit/management/__init__.py +0 -0
  31. {django_field_audit-1.3.0 → django_field_audit-1.4.0}/field_audit/management/commands/__init__.py +0 -0
  32. {django_field_audit-1.3.0 → django_field_audit-1.4.0}/field_audit/middleware.py +0 -0
  33. {django_field_audit-1.3.0 → django_field_audit-1.4.0}/field_audit/migrations/0001_initial.py +0 -0
  34. {django_field_audit-1.3.0 → django_field_audit-1.4.0}/field_audit/migrations/0002_add_is_bootstrap_column.py +0 -0
  35. {django_field_audit-1.3.0 → django_field_audit-1.4.0}/field_audit/migrations/0003_alter_auditevent_change_context_and_more.py +0 -0
  36. {django_field_audit-1.3.0 → django_field_audit-1.4.0}/field_audit/migrations/__init__.py +0 -0
@@ -1,12 +1,11 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: django-field-audit
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: Audit Field Changes on Django Models
5
- Home-page: https://github.com/dimagi/django-field-audit
6
- Maintainer: Joel Miller
7
- Maintainer-email: jmiller@dimagi.com
8
- License: BSD License
9
- Classifier: Development Status :: 3 - Alpha
5
+ Author-email: Joel Miller <jmiller@dimagi.com>, Simon Kelly <simongdkelly@gmail.com>, Graham Herceg <gherceg@dimagi.com>, Chris Smit <chris.smit@dimagi.com>, Daniel Miller <millerdev@gmail.com>
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ Classifier: Development Status :: 5 - Production/Stable
10
9
  Classifier: Environment :: Web Environment
11
10
  Classifier: Framework :: Django
12
11
  Classifier: Intended Audience :: Developers
@@ -18,22 +17,11 @@ Classifier: Programming Language :: Python :: 3.9
18
17
  Classifier: Programming Language :: Python :: 3.10
19
18
  Classifier: Programming Language :: Python :: 3.11
20
19
  Classifier: Programming Language :: Python :: 3.12
21
- Classifier: Framework :: Django
22
- Classifier: Framework :: Django :: 3
23
- Classifier: Framework :: Django :: 3.2
24
- Classifier: Framework :: Django :: 4
25
- Classifier: Framework :: Django :: 4.2
26
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
- Description-Content-Type: text/markdown
28
21
  License-File: LICENSE
29
- Dynamic: classifier
30
- Dynamic: description
31
- Dynamic: description-content-type
32
- Dynamic: home-page
33
- Dynamic: license
34
- Dynamic: maintainer
35
- Dynamic: maintainer-email
36
- Dynamic: summary
22
+ Requires-Dist: django>=3.2
23
+ Project-URL: Homepage, https://github.com/dimagi/django-field-audit
24
+ Project-URL: Repository, https://github.com/dimagi/django-field-audit
37
25
 
38
26
  # Audit Field Changes on Django Models
39
27
 
@@ -102,6 +90,42 @@ FIELD_AUDIT_AUDITORS = []
102
90
  |:----------------------------------|:---------------------------------------------------------------|:------------------------
103
91
  | `FIELD_AUDIT_AUDITEVENT_MANAGER` | A custom manager to use for the `AuditEvent` Model. | `field_audit.models.DefaultAuditEventManager`
104
92
  | `FIELD_AUDIT_AUDITORS` | A custom list of auditors for acquiring `change_context` info. | `["field_audit.auditors.RequestAuditor", "field_audit.auditors.SystemUserAuditor"]`
93
+ | `FIELD_AUDIT_SERVICE_CLASS` | A custom service class for audit logic implementation. | `field_audit.services.AuditService`
94
+
95
+ ### Custom Audit Service
96
+
97
+ The audit logic has been extracted into a separate `AuditService` class to improve separation of concerns and enable easier customization of audit behavior. Users can provide custom audit implementations by subclassing `AuditService` and configuring the `FIELD_AUDIT_SERVICE_CLASS` setting.
98
+
99
+ #### Creating a Custom Audit Service
100
+
101
+ ```python
102
+ # myapp/audit.py
103
+
104
+ from field_audit import AuditService
105
+
106
+ class CustomAuditService(AuditService):
107
+ def get_field_value(self, instance, field_name, bootstrap=False):
108
+ # Custom logic for extracting field values
109
+ value = super().get_field_value(instance, field_name, bootstrap)
110
+
111
+ # Example: custom serialization or transformation
112
+ if field_name == 'sensitive_field':
113
+ value = '[REDACTED]'
114
+
115
+ return value
116
+ ```
117
+
118
+ Then configure it in your Django settings:
119
+
120
+ ```python
121
+ # settings.py
122
+
123
+ FIELD_AUDIT_SERVICE_CLASS = 'myapp.audit.CustomAuditService'
124
+ ```
125
+
126
+ #### Backward Compatibility
127
+
128
+ The original `AuditEvent` class methods are maintained for backward compatibility but are now deprecated in favor of the service-based approach. These methods will issue deprecation warnings and delegate to the configured audit service.
105
129
 
106
130
  ### Model Auditing
107
131
 
@@ -280,12 +304,11 @@ All feature and bug contributions are expected to be covered by tests.
280
304
 
281
305
  ### Setup for developers
282
306
 
283
- Create/activate a python virtualenv and install the required dependencies.
307
+ This project uses [uv](https://docs.astral.sh/uv/) for dependency management. Install uv and then install the project dependencies:
284
308
 
285
309
  ```shell
286
310
  cd django-field-audit
287
- mkvirtualenv django-field-audit # or however you choose to setup your environment
288
- pip install django pynose flake8 coverage
311
+ uv sync
289
312
  ```
290
313
 
291
314
  ### Running tests
@@ -296,18 +319,18 @@ your local Python's `sqlite3` library ships with the `JSON1` extension enabled
296
319
 
297
320
  - Tests
298
321
  ```shell
299
- nosetests
322
+ uv run pytest
300
323
  ```
301
324
 
302
325
  - Style check
303
326
  ```shell
304
- flake8 --config=setup.cfg
327
+ ruff check
305
328
  ```
306
329
 
307
330
  - Coverage
308
331
  ```shell
309
- coverage run -m nose
310
- coverage report -m
332
+ uv run coverage run -m pytest
333
+ uv run coverage report -m
311
334
  ```
312
335
 
313
336
  ### Adding migrations
@@ -315,30 +338,25 @@ your local Python's `sqlite3` library ships with the `JSON1` extension enabled
315
338
  The example `manage.py` is available for making new migrations.
316
339
 
317
340
  ```shell
318
- python example/manage.py makemigrations field_audit
341
+ uv run python example/manage.py makemigrations field_audit
319
342
  ```
320
343
 
321
- ### Uploading to PyPI
344
+ ### Publishing a new version to PyPI
322
345
 
323
- First bump the package version in the `field_audit/__init__.py` file. Then create a changelog entry in the CHANGELOG.md
324
- file. After these changes are merged, you should tag the main branch with the new version. Then, package and upload the generated files to PyPI.
346
+ Push a new tag to Github using the format vX.Y.Z where X.Y.Z matches the version
347
+ in [`__init__.py`](field_audit/__init__.py). Also ensure that the changelog is up to date.
325
348
 
326
- ```shell
327
- pip install -r pkg-requires.txt
328
-
329
- python setup.py sdist bdist_wheel
330
- twine upload dist/*
331
- ```
349
+ Publishing is automated with [Github Actions](.github/workflows/pypi.yml).
332
350
 
333
351
  ## TODO
334
352
 
335
353
  - Implement auditing for the remaining "special" QuerySet write operations:
336
354
  - `bulk_update()`
337
355
  - Write full library documentation using github.io.
338
- - Switch to `pytest` to support Python 3.10.
339
356
 
340
357
  ### Backlog
341
358
 
342
359
  - Add to optimization for `instance.save(save_fields=[...])` [maybe].
343
360
  - Support adding new audit fields on the same model at different times (instead
344
361
  of raising `AlreadyAudited`) [maybe].
362
+
@@ -65,6 +65,42 @@ FIELD_AUDIT_AUDITORS = []
65
65
  |:----------------------------------|:---------------------------------------------------------------|:------------------------
66
66
  | `FIELD_AUDIT_AUDITEVENT_MANAGER` | A custom manager to use for the `AuditEvent` Model. | `field_audit.models.DefaultAuditEventManager`
67
67
  | `FIELD_AUDIT_AUDITORS` | A custom list of auditors for acquiring `change_context` info. | `["field_audit.auditors.RequestAuditor", "field_audit.auditors.SystemUserAuditor"]`
68
+ | `FIELD_AUDIT_SERVICE_CLASS` | A custom service class for audit logic implementation. | `field_audit.services.AuditService`
69
+
70
+ ### Custom Audit Service
71
+
72
+ The audit logic has been extracted into a separate `AuditService` class to improve separation of concerns and enable easier customization of audit behavior. Users can provide custom audit implementations by subclassing `AuditService` and configuring the `FIELD_AUDIT_SERVICE_CLASS` setting.
73
+
74
+ #### Creating a Custom Audit Service
75
+
76
+ ```python
77
+ # myapp/audit.py
78
+
79
+ from field_audit import AuditService
80
+
81
+ class CustomAuditService(AuditService):
82
+ def get_field_value(self, instance, field_name, bootstrap=False):
83
+ # Custom logic for extracting field values
84
+ value = super().get_field_value(instance, field_name, bootstrap)
85
+
86
+ # Example: custom serialization or transformation
87
+ if field_name == 'sensitive_field':
88
+ value = '[REDACTED]'
89
+
90
+ return value
91
+ ```
92
+
93
+ Then configure it in your Django settings:
94
+
95
+ ```python
96
+ # settings.py
97
+
98
+ FIELD_AUDIT_SERVICE_CLASS = 'myapp.audit.CustomAuditService'
99
+ ```
100
+
101
+ #### Backward Compatibility
102
+
103
+ The original `AuditEvent` class methods are maintained for backward compatibility but are now deprecated in favor of the service-based approach. These methods will issue deprecation warnings and delegate to the configured audit service.
68
104
 
69
105
  ### Model Auditing
70
106
 
@@ -243,12 +279,11 @@ All feature and bug contributions are expected to be covered by tests.
243
279
 
244
280
  ### Setup for developers
245
281
 
246
- Create/activate a python virtualenv and install the required dependencies.
282
+ This project uses [uv](https://docs.astral.sh/uv/) for dependency management. Install uv and then install the project dependencies:
247
283
 
248
284
  ```shell
249
285
  cd django-field-audit
250
- mkvirtualenv django-field-audit # or however you choose to setup your environment
251
- pip install django pynose flake8 coverage
286
+ uv sync
252
287
  ```
253
288
 
254
289
  ### Running tests
@@ -259,18 +294,18 @@ your local Python's `sqlite3` library ships with the `JSON1` extension enabled
259
294
 
260
295
  - Tests
261
296
  ```shell
262
- nosetests
297
+ uv run pytest
263
298
  ```
264
299
 
265
300
  - Style check
266
301
  ```shell
267
- flake8 --config=setup.cfg
302
+ ruff check
268
303
  ```
269
304
 
270
305
  - Coverage
271
306
  ```shell
272
- coverage run -m nose
273
- coverage report -m
307
+ uv run coverage run -m pytest
308
+ uv run coverage report -m
274
309
  ```
275
310
 
276
311
  ### Adding migrations
@@ -278,27 +313,21 @@ your local Python's `sqlite3` library ships with the `JSON1` extension enabled
278
313
  The example `manage.py` is available for making new migrations.
279
314
 
280
315
  ```shell
281
- python example/manage.py makemigrations field_audit
316
+ uv run python example/manage.py makemigrations field_audit
282
317
  ```
283
318
 
284
- ### Uploading to PyPI
285
-
286
- First bump the package version in the `field_audit/__init__.py` file. Then create a changelog entry in the CHANGELOG.md
287
- file. After these changes are merged, you should tag the main branch with the new version. Then, package and upload the generated files to PyPI.
319
+ ### Publishing a new version to PyPI
288
320
 
289
- ```shell
290
- pip install -r pkg-requires.txt
321
+ Push a new tag to Github using the format vX.Y.Z where X.Y.Z matches the version
322
+ in [`__init__.py`](field_audit/__init__.py). Also ensure that the changelog is up to date.
291
323
 
292
- python setup.py sdist bdist_wheel
293
- twine upload dist/*
294
- ```
324
+ Publishing is automated with [Github Actions](.github/workflows/pypi.yml).
295
325
 
296
326
  ## TODO
297
327
 
298
328
  - Implement auditing for the remaining "special" QuerySet write operations:
299
329
  - `bulk_update()`
300
330
  - Write full library documentation using github.io.
301
- - Switch to `pytest` to support Python 3.10.
302
331
 
303
332
  ### Backlog
304
333
 
@@ -0,0 +1,4 @@
1
+ from .field_audit import audit_fields # noqa: F401
2
+ from .services import AuditService, get_audit_service # noqa: F401
3
+
4
+ __version__ = "1.4.0"
@@ -57,7 +57,7 @@ def audit_fields(*field_names, class_path=None, audit_special_queryset_writes=Fa
57
57
  raise AlreadyAudited(cls)
58
58
  if not issubclass(cls, models.Model):
59
59
  raise ValueError(f"expected Model subclass, got: {cls}")
60
- AuditEvent.attach_field_names(cls, field_names)
60
+ service.attach_field_names(cls, field_names)
61
61
  if audit_special_queryset_writes:
62
62
  _verify_auditing_manager(cls)
63
63
  cls.__init__ = _decorate_init(cls.__init__)
@@ -70,7 +70,8 @@ def audit_fields(*field_names, class_path=None, audit_special_queryset_writes=Fa
70
70
  return cls
71
71
  if not field_names:
72
72
  raise ValueError("at least one field name is required")
73
- from .models import AuditEvent
73
+ from .services import get_audit_service
74
+ service = get_audit_service()
74
75
  return wrapper
75
76
 
76
77
 
@@ -101,8 +102,9 @@ def _decorate_init(init):
101
102
  @wraps(init)
102
103
  def wrapper(self, *args, **kw):
103
104
  init(self, *args, **kw)
104
- AuditEvent.attach_initial_values(self)
105
- from .models import AuditEvent
105
+ service.attach_initial_values(self)
106
+ from .services import get_audit_service
107
+ service = get_audit_service()
106
108
  return wrapper
107
109
 
108
110
 
@@ -124,7 +126,7 @@ def _decorate_db_write(func):
124
126
  db = router.db_for_write(type(self))
125
127
  with transaction.atomic(using=db):
126
128
  ret = func(self, *args, **kw)
127
- AuditEvent.audit_field_changes(
129
+ service.audit_field_changes(
128
130
  self,
129
131
  is_create,
130
132
  is_delete,
@@ -136,7 +138,8 @@ def _decorate_db_write(func):
136
138
  is_delete = func.__name__ == "delete"
137
139
  if not is_save and not is_delete:
138
140
  raise ValueError(f"invalid function for decoration: {func}")
139
- from .models import AuditEvent
141
+ from .services import get_audit_service
142
+ service = get_audit_service()
140
143
  return wrapper
141
144
 
142
145
 
@@ -150,10 +153,11 @@ def _decorate_refresh_from_db(func):
150
153
  @wraps(func)
151
154
  def wrapper(self, using=None, fields=None, **kwargs):
152
155
  if fields is not None:
153
- fields = set(fields) | set(AuditEvent.field_names(self))
156
+ fields = set(fields) | set(service.get_field_names(self))
154
157
  func(self, using, fields, **kwargs)
155
158
 
156
- from .models import AuditEvent
159
+ from .services import get_audit_service
160
+ service = get_audit_service()
157
161
  return wrapper
158
162
 
159
163
 
@@ -185,7 +189,9 @@ def _m2m_changed_handler(sender, instance, action, pk_set, **kwargs):
185
189
  :param action: A string indicating the type of update
186
190
  :param pk_set: For add/remove actions, set of primary key values
187
191
  """
188
- from .models import AuditEvent
192
+ from .services import get_audit_service
193
+
194
+ service = get_audit_service()
189
195
 
190
196
  if action not in ('post_add', 'post_remove', 'post_clear', 'pre_clear'):
191
197
  return
@@ -206,17 +212,17 @@ def _m2m_changed_handler(sender, instance, action, pk_set, **kwargs):
206
212
  field_name = field.name
207
213
  break
208
214
 
209
- if not m2m_field or field_name not in AuditEvent.field_names(instance):
215
+ if not m2m_field or field_name not in service.get_field_names(instance):
210
216
  return
211
217
 
212
218
  if action == 'pre_clear':
213
219
  # `pk_set` not supplied for clear actions. Determine initial values
214
220
  # in the `pre_clear` event
215
- AuditEvent.attach_initial_m2m_values(instance, field_name)
221
+ service.attach_initial_m2m_values(instance, field_name)
216
222
  return
217
223
 
218
224
  if action == 'post_clear':
219
- initial_values = AuditEvent.get_initial_m2m_values(instance, field_name)
225
+ initial_values = service.get_initial_m2m_values(instance, field_name)
220
226
  if not initial_values:
221
227
  return
222
228
  delta = {field_name: {'remove': initial_values}}
@@ -228,13 +234,13 @@ def _m2m_changed_handler(sender, instance, action, pk_set, **kwargs):
228
234
  delta = {field_name: {delta_key: list(pk_set)}}
229
235
 
230
236
  req = request.get()
231
- event = AuditEvent.create_audit_event(
237
+ event = service.create_audit_event(
232
238
  instance.pk, instance.__class__, delta, False, False, req
233
239
  )
234
240
  if event is not None:
235
241
  event.save()
236
242
 
237
- AuditEvent.clear_initial_m2m_field_values(instance, field_name)
243
+ service.clear_initial_m2m_field_values(instance, field_name)
238
244
 
239
245
 
240
246
  def get_audited_models():
@@ -4,7 +4,7 @@ from django.core.management.base import BaseCommand, CommandError
4
4
 
5
5
  from field_audit.const import BOOTSTRAP_BATCH_SIZE
6
6
  from field_audit.field_audit import get_audited_models
7
- from field_audit.models import AuditEvent
7
+ from field_audit.services import get_audit_service
8
8
 
9
9
 
10
10
  class Command(BaseCommand):
@@ -64,6 +64,9 @@ class Command(BaseCommand):
64
64
  if batch_size == 0:
65
65
  batch_size = None
66
66
  self.batch_size = batch_size
67
+
68
+ self.service = get_audit_service()
69
+
67
70
  for name in models:
68
71
  model_class = self.models[name]
69
72
  self.operations[operation](self, model_class)
@@ -74,7 +77,7 @@ class Command(BaseCommand):
74
77
  with self.bootstrap_action_log(log_head) as stream:
75
78
  count = self.do_bootstrap(
76
79
  model_class,
77
- AuditEvent.bootstrap_existing_model_records,
80
+ self.service.bootstrap_existing_model_records,
78
81
  iter_records=query.iterator,
79
82
  )
80
83
  stream.write(f"done ({count})")
@@ -82,11 +85,11 @@ class Command(BaseCommand):
82
85
  def top_up_missing(self, model_class):
83
86
  log_head = f"top-up: {model_class} ... "
84
87
  with self.bootstrap_action_log(log_head) as stream:
85
- count = self.do_bootstrap(model_class, AuditEvent.bootstrap_top_up)
88
+ count = self.do_bootstrap(model_class, self.service.bootstrap_top_up)
86
89
  stream.write(f"done ({count})")
87
90
 
88
91
  def do_bootstrap(self, model_class, bootstrap_method, **bootstrap_kw):
89
- field_names = AuditEvent.field_names(model_class)
92
+ field_names = self.service.get_field_names(model_class)
90
93
  if not field_names:
91
94
  raise CommandError(
92
95
  f"invalid fields ({field_names!r}) for model: {model_class}"