django-activity-audit 1.3.0.dev11__tar.gz → 1.3.0.dev13__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.
Files changed (26) hide show
  1. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/PKG-INFO +1 -1
  2. django_activity_audit-1.3.0.dev13/activity_audit/REVIEW.md +297 -0
  3. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/__init__.py +6 -2
  4. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/apps.py +3 -0
  5. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/constants.py +11 -0
  6. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/formatters.py +6 -9
  7. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/handlers.py +16 -3
  8. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/settings.py +0 -43
  9. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/signals.py +1 -1
  10. django_activity_audit-1.3.0.dev13/activity_audit/unregistered.py +33 -0
  11. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/utils.py +13 -2
  12. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/pyproject.toml +1 -1
  13. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/.gitignore +0 -0
  14. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/.pre-commit-config.yaml +0 -0
  15. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/LICENSE +0 -0
  16. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/MANIFEST.in +0 -0
  17. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/README.md +0 -0
  18. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/README.rst +0 -0
  19. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/logger_levels.py +0 -0
  20. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/middleware.py +0 -0
  21. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/protocols.py +0 -0
  22. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/docs/django-activity-audit.md +0 -0
  23. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/docs/user-activity-feed-plan.md +0 -0
  24. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/hatch +0 -0
  25. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/pytest.ini +0 -0
  26. {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-activity-audit
3
- Version: 1.3.0.dev11
3
+ Version: 1.3.0.dev13
4
4
  Summary: A Django package for easy CRUD operation logging and container logs
5
5
  Project-URL: Homepage, https://github.com/shree256/django-activity-audit
6
6
  Project-URL: Repository, https://github.com/shree256/django-activity-audit
@@ -0,0 +1,297 @@
1
+ # activity_audit — Code Review
2
+
3
+ > Reviewed: 2026-06-08
4
+ > Reviewer: Claude Code
5
+ > Scope: Full app audit following bug fixes for `AppRegistryNotReady` and `request_id` thread-local propagation
6
+
7
+ ---
8
+
9
+ ## Recent Fixes (Verified Correct)
10
+
11
+ | Fix | File | Status |
12
+ |-----|------|--------|
13
+ | `AppRegistryNotReady` — model imports deferred to `unregistered.py`, loaded in `apps.ready()` | `unregistered.py`, `apps.py`, `settings.py` | ✅ Correct |
14
+ | `request_id` not written to `app.log` — captured via `prepare()` in handler (producer thread) before record is queued to background `QueueListener` thread | `handlers.py` | ✅ Correct |
15
+ | `watchfiles` reload loop — log files moved to `/logs/` (outside `/app/`), `watchfiles` logger silenced at WARNING | `config/settings/local.py`, `local.yml` | ✅ Correct |
16
+
17
+ ---
18
+
19
+ ## Critical Issues (Must Fix)
20
+
21
+ ### 1. Cross-request PHI data leak — shared `self.log_data` in middleware
22
+ **File:** `middleware.py:127–140`
23
+
24
+ `self.log_data` is constructed once in `__init__` and stored on the middleware instance. Middleware instances are shared across all requests. Under concurrent ASGI (multiple coroutines on one instance), request A's `user_id`, `request_repr`, `response_repr` overwrite request B's in the same dict — PHI from one patient's session can be written into another patient's audit record.
25
+
26
+ **Fix:** Build `log_data` as a local dict inside `__call__` and `__acall__`, not on `self`.
27
+
28
+ ```python
29
+ # Before (in __init__)
30
+ self.log_data = {"service_name": SERVICE_NAME, ...}
31
+
32
+ # After (top of __call__ and __acall__)
33
+ log_data = {"service_name": SERVICE_NAME, ...}
34
+ ```
35
+
36
+ ---
37
+
38
+ ### 2. `QuerySet.bulk_create/bulk_update` monkey-patched N times — wrappers nest
39
+ **File:** `signals.py:129–130, 244–247`
40
+
41
+ Inside `patch_model_event`, the patching logic does:
42
+ ```python
43
+ original_bulk_create = models.QuerySet.bulk_create # captured per call
44
+ ...
45
+ models.QuerySet.bulk_create = bulk_create_with_signals # assigned globally
46
+ ```
47
+ This runs once per audited model. The second model captures the already-patched
48
+ method as its "original", the third wraps the double-patched version, and so on.
49
+ After N models are patched, every `bulk_create` call traverses N nested wrappers.
50
+
51
+ **Fix:** Extract the `QuerySet` patches into a separate function called exactly once from `setup_model_signals()`, before the per-model loop.
52
+
53
+ ---
54
+
55
+ ### 3. `get_calling_model()` frame-walking is broken — bulk auditing is non-functional
56
+ **File:** `signals.py:57–79, 193–196, 225–228`
57
+
58
+ ```python
59
+ if calling_model == model_class.__name__:
60
+ ```
61
+ `calling_model` is `module_name.split(".")[-1]` (e.g. `"views"`, `"tasks"`).
62
+ `model_class.__name__` is a class name (e.g. `"Patient"`, `"Appointment"`).
63
+ These will almost never match, so bulk audit logs are silently never written for virtually all models.
64
+
65
+ **Fix:** Rethink bulk auditing using Django signals (`post_bulk_create` is unavailable natively, but a custom `QuerySet` mixin on a project-level base queryset is more reliable than frame introspection).
66
+
67
+ ---
68
+
69
+ ### 4. `request.body` access can crash real requests
70
+ **File:** `middleware.py:165–170, 229–234`
71
+
72
+ Only `json.JSONDecodeError` is caught. The following are unhandled and will 500 the request:
73
+ - `RawPostDataException` — body stream already consumed by DRF/multipart parsers
74
+ - `RequestDataTooBig` — body exceeds `DATA_UPLOAD_MAX_MEMORY_SIZE`
75
+ - `UnicodeDecodeError` — binary upload body
76
+
77
+ Audit middleware must never break actual requests.
78
+
79
+ **Fix:**
80
+ ```python
81
+ try:
82
+ body = json.loads(request.body)
83
+ request_data["body"] = body
84
+ except Exception:
85
+ pass # never let audit logging crash the request
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Warnings (Should Fix)
91
+
92
+ ### 5. PHI/SSN written into `audit.log` via `instance_to_dict`
93
+ **File:** `signals.py:117–118`
94
+
95
+ `model_to_dict(instance, fields=[f.name for f in instance._meta.fields])` dumps all
96
+ concrete fields including encrypted `social_security_number` and other PHI directly
97
+ into the audit log file. This directly violates HIPAA rules documented in `CLAUDE.md`:
98
+ > "Never log PHI to console" / "SSN handling — only access via `get_ssn` endpoint"
99
+
100
+ **Fix:** Add a per-model field deny-list or use a model-level `AUDIT_EXCLUDE_FIELDS`
101
+ attribute. At minimum, exclude all `EncryptedField` instances.
102
+
103
+ ---
104
+
105
+ ### 6. Regex patterns recompiled on every request in `should_log_url`
106
+ **File:** `middleware.py:84–100`
107
+
108
+ ```python
109
+ for unregistered_url in UNREGISTERED_URLS:
110
+ pattern = re.compile(unregistered_url) # compiled on every request
111
+ ```
112
+ This is in the hot path for every HTTP request.
113
+
114
+ **Fix:** Compile once at module load:
115
+ ```python
116
+ _UNREGISTERED_PATTERNS = [re.compile(p) for p in UNREGISTERED_URLS]
117
+ _REGISTERED_PATTERNS = [re.compile(p) for p in REGISTERED_URLS]
118
+ ```
119
+
120
+ ---
121
+
122
+ ### 7. `clear_request()` not called on early return — thread-local leaks
123
+ **File:** `middleware.py:148–153`
124
+
125
+ ```python
126
+ set_current_request(request)
127
+ set_request_id(request_id)
128
+
129
+ if not should_log_url(request.path):
130
+ return self.get_response(request) # clear_request() never called
131
+ ```
132
+ The thread-local `request` and `request_id` persist on the worker thread and
133
+ are seen by the next request handled by that thread.
134
+
135
+ **Fix:** Use `try/finally`:
136
+ ```python
137
+ set_current_request(request)
138
+ set_request_id(request_id)
139
+ try:
140
+ if not should_log_url(request.path):
141
+ return self.get_response(request)
142
+ ...
143
+ finally:
144
+ clear_request()
145
+ ```
146
+
147
+ ---
148
+
149
+ ### 8. Bulk wrappers never call `should_audit` — unregistered models audited on bulk paths
150
+ **File:** `signals.py:179–242`
151
+
152
+ `bulk_create_with_signals` and `bulk_update_with_signals` have no `should_audit` guard,
153
+ so `Session`, `Permission`, `Migration` etc. are audited on bulk paths but excluded on
154
+ single-save paths. Inconsistent and wasteful.
155
+
156
+ ---
157
+
158
+ ### 9. `SFTPClient.upload` — unconditional assignment clobbers success and real errors
159
+ **File:** `protocols.py:209–237`
160
+
161
+ ```python
162
+ if not error:
163
+ try:
164
+ ...upload...
165
+ except Exception:
166
+ self.log_payload["error_message"] = str(e) # real error
167
+
168
+ # This runs unconditionally — overwrites both the success case and the real exception:
169
+ self.log_payload["error_message"] = f"Path validation failed. Error: {str(error)}"
170
+ ```
171
+ On a fully successful upload, `error_message` is set to `"Path validation failed. Error: None"`.
172
+
173
+ **Fix:** Wrap the final assignment in an `else` block.
174
+
175
+ ---
176
+
177
+ ### 10. `HTTPClient.request` returns `None` on exception and calls `.json()` unconditionally
178
+ **File:** `protocols.py:97–118`
179
+
180
+ ```python
181
+ except Exception:
182
+ ...
183
+ return response # response is None here
184
+ ```
185
+ Callers expecting a `requests.Response` will raise `AttributeError`. On the success path,
186
+ `response.json()` is called regardless of `Content-Type`, which will raise for non-JSON
187
+ responses and discard the real response object.
188
+
189
+ ---
190
+
191
+ ### 11. `push_usage_log` imports `get_user_details` through `signals` — unwanted side effect
192
+ **File:** `utils.py:139`
193
+
194
+ ```python
195
+ from .signals import get_user_details
196
+ ```
197
+ Importing `signals` triggers `setup_model_signals()` at the bottom of that module as a
198
+ side effect. Import directly from the canonical source:
199
+ ```python
200
+ from .middleware import get_user_details
201
+ ```
202
+
203
+ ---
204
+
205
+ ### 12. Sentry filter attached at settings-load time — before Sentry is initialised
206
+ **File:** `settings.py:24–27`
207
+
208
+ ```python
209
+ root_logger = logging.getLogger()
210
+ for handler in root_logger.handlers:
211
+ if handler.__class__.__name__.startswith("Sentry"):
212
+ handler.addFilter(AuditToSentryFilter())
213
+ ```
214
+ At settings-load time Sentry SDK has not yet added its handler, so this loop finds nothing
215
+ and the filter is never attached. Move to `apps.ready()`.
216
+
217
+ ---
218
+
219
+ ## Suggestions (Consider)
220
+
221
+ ### 13. `default_app_config` is dead
222
+ **File:** `__init__.py:1`
223
+
224
+ Deprecated in Django 3.2, removed in Django 4.1. It is a no-op. Remove it.
225
+
226
+ ---
227
+
228
+ ### 14. Timestamp formatting duplicated across all 4 formatters
229
+ **File:** `formatters.py:42–44` (and 3 other formatters)
230
+
231
+ ```python
232
+ datetime.datetime.fromtimestamp(record.created).strftime(self.timestamp_format)[:-3]
233
+ ```
234
+ This block is copy-pasted in `JsonFormatter`, `APIFormatter`, `AuditFormatter`, and
235
+ `LoginFormatter`. Also uses server local time with no timezone. Extract a shared:
236
+ ```python
237
+ class BaseAuditFormatter(logging.Formatter):
238
+ def _timestamp(self, record) -> str:
239
+ return datetime.datetime.fromtimestamp(
240
+ record.created, tz=datetime.timezone.utc
241
+ ).strftime(self.timestamp_format)[:-3]
242
+ ```
243
+
244
+ ---
245
+
246
+ ### 15. `apps.ready()` no-op attribute accesses are misleading
247
+ **File:** `apps.py:14–16`
248
+
249
+ ```python
250
+ logger_levels.AUDIT
251
+ logger_levels.API
252
+ logger_levels.LOGIN
253
+ ```
254
+ These are bare attribute reads that do nothing — the level registration already
255
+ happened when `logger_levels` was imported. The import itself is the side effect.
256
+ Remove the three lines or replace with a comment.
257
+
258
+ ---
259
+
260
+ ### 16. `AuditToSentryFilter` mutates `record.levelname` — corrupts file logs
261
+ **File:** `settings.py:8–20`
262
+
263
+ Overwriting `record.levelname = "INFO"` means formatters downstream will log `"INFO"`
264
+ instead of `"AUDIT"` / `"API"` in file logs too (filters run before formatting).
265
+ If Sentry integration is needed, use a `before_send` hook in Sentry config instead.
266
+
267
+ ---
268
+
269
+ ### 17. Minor style issues
270
+
271
+ | Location | Issue |
272
+ |----------|-------|
273
+ | `middleware.py:64` | `id = str(user.id)` shadows the builtin `id` |
274
+ | `protocols.py:193` | `f"Connection not established"` — f-string with no placeholders |
275
+ | `signals.py:88` | `instance_repr: str` type hint but callers pass `dict` |
276
+ | `middleware.py:21–25` | `MockRequest.__init__` passes `*args, **kwargs` to `object.__init__` which rejects them |
277
+ | `formatters.py`, `signals.py` | Several lines exceed 80-char limit — run `make format` |
278
+
279
+ ---
280
+
281
+ ## Priority Order
282
+
283
+ | Priority | Issue | File |
284
+ |----------|-------|------|
285
+ | 🔴 P0 | Cross-request PHI data leak (`self.log_data`) | `middleware.py` |
286
+ | 🔴 P0 | PHI/SSN in `audit.log` via `instance_to_dict` | `signals.py` |
287
+ | 🔴 P0 | `request.body` access can crash requests | `middleware.py` |
288
+ | 🟠 P1 | `QuerySet` patch nested N times | `signals.py` |
289
+ | 🟠 P1 | Bulk auditing effectively broken (`get_calling_model`) | `signals.py` |
290
+ | 🟠 P1 | `clear_request()` missing on early return | `middleware.py` |
291
+ | 🟡 P2 | Regex recompiled on every request | `middleware.py` |
292
+ | 🟡 P2 | Bulk wrappers skip `should_audit` | `signals.py` |
293
+ | 🟡 P2 | `SFTPClient.upload` error message clobbered | `protocols.py` |
294
+ | 🟡 P2 | `HTTPClient.request` returns `None` silently | `protocols.py` |
295
+ | 🟡 P2 | `push_usage_log` imports via `signals` | `utils.py` |
296
+ | 🟡 P2 | Sentry filter never attaches | `settings.py` |
297
+ | 🔵 P3 | Timestamp duplication, `default_app_config`, style nits | various |
@@ -8,7 +8,9 @@ from activity_audit.utils import (
8
8
  get_async_login_handler,
9
9
  get_audit_handler,
10
10
  get_console_formatter,
11
- get_json_formatter,
11
+ get_app_formatter,
12
+ get_api_formatter,
13
+ get_audit_formatter,
12
14
  get_json_handler,
13
15
  get_login_handler,
14
16
  )
@@ -17,7 +19,9 @@ from . import logger_levels
17
19
 
18
20
  __all__ = [
19
21
  "get_console_formatter",
20
- "get_json_formatter",
22
+ "get_app_formatter",
23
+ "get_api_formatter",
24
+ "get_audit_formatter",
21
25
  "get_json_handler",
22
26
  "get_api_handler",
23
27
  "get_audit_handler",
@@ -15,5 +15,8 @@ class AuditLoggingConfig(AppConfig):
15
15
  logger_levels.API
16
16
  logger_levels.LOGIN
17
17
 
18
+ # Populate UNREGISTERED_CLASSES (requires app registry to be ready)
19
+ from . import unregistered # noqa
20
+
18
21
  # Initialize signals
19
22
  from . import signals # noqa
@@ -1,3 +1,13 @@
1
+ import enum
2
+
3
+
4
+ class LogType(str, enum.Enum):
5
+ APP = "app"
6
+ API = "api"
7
+ AUDIT = "audit"
8
+ LOGIN = "login"
9
+
10
+
1
11
  CONSOLE_FORMAT = (
2
12
  "%(levelname)s %(asctime)s %(pathname)s %(module)s %(funcName)s %(message)s"
3
13
  )
@@ -6,3 +16,4 @@ REQUEST_TYPES = [
6
16
  "internal",
7
17
  "external",
8
18
  ]
19
+
@@ -4,8 +4,7 @@ import json
4
4
  import logging
5
5
  import uuid
6
6
 
7
- from activity_audit.middleware import get_request_id
8
-
7
+ from .constants import LogType
9
8
 
10
9
  def _json_default(obj):
11
10
  """
@@ -31,7 +30,7 @@ def _json_default(obj):
31
30
  return str(obj)
32
31
 
33
32
 
34
- class JsonFormatter(logging.Formatter):
33
+ class AppFormatter(logging.Formatter):
35
34
  def __init__(self, timestamp_format: str = "%Y-%m-%d %H:%M:%S.%f"):
36
35
  super().__init__()
37
36
  self.timestamp_format = timestamp_format
@@ -49,20 +48,16 @@ class JsonFormatter(logging.Formatter):
49
48
  "path": record.pathname,
50
49
  "module": record.module,
51
50
  "function": record.funcName,
52
- "request_id": get_request_id() or "",
51
+ "request_id": getattr(record, "request_id", "") or "",
53
52
  "message": record.getMessage(),
54
53
  "exception": "",
55
- # "extra": {},
54
+ "log_type": LogType.APP,
56
55
  }
57
56
 
58
57
  # Add exception info if present for ERROR
59
58
  if record.exc_info:
60
59
  log_data["exception"] = "{}".format(self.formatException(record.exc_info))
61
60
 
62
- # Add extra fields if present
63
- # if hasattr(record, "extra"):
64
- # log_data.update(record.extra)
65
-
66
61
  return json.dumps(log_data, default=_json_default)
67
62
 
68
63
 
@@ -82,6 +77,7 @@ class APIFormatter(logging.Formatter):
82
77
  "level": record.levelname,
83
78
  "name": record.name,
84
79
  "message": record.getMessage(),
80
+ "log_type": LogType.API,
85
81
  }
86
82
 
87
83
  # Add all audit-specific fields if they exist
@@ -116,6 +112,7 @@ class AuditFormatter(logging.Formatter):
116
112
  "level": record.levelname,
117
113
  "name": record.name,
118
114
  "message": record.getMessage(),
115
+ "log_type": LogType.AUDIT,
119
116
  }
120
117
 
121
118
  audit_fields = [
@@ -3,7 +3,8 @@ import queue
3
3
 
4
4
  from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler
5
5
 
6
- from .formatters import APIFormatter, AuditFormatter, JsonFormatter, LoginFormatter
6
+ from .formatters import APIFormatter, AuditFormatter, AppFormatter, LoginFormatter
7
+ from .middleware import get_request_id
7
8
 
8
9
 
9
10
  class BaseAuditHandler(RotatingFileHandler):
@@ -108,6 +109,13 @@ class AsyncBaseAuditHandler(QueueHandler):
108
109
  )
109
110
  self._listener.start()
110
111
 
112
+ def prepare(self, record):
113
+ # Capture request_id in the calling thread before the record is queued,
114
+ # since thread-locals are not accessible from the QueueListener thread.
115
+ record = super().prepare(record)
116
+ record.request_id = get_request_id() or ""
117
+ return record
118
+
111
119
  def close(self):
112
120
  self._listener.stop()
113
121
  super().close()
@@ -137,7 +145,7 @@ class AsyncLoginLogHandler(AsyncBaseAuditHandler):
137
145
  class AsyncJsonHandler(QueueHandler):
138
146
  """
139
147
  Non-blocking handler for general JSON logs. Wraps a RotatingFileHandler
140
- with JsonFormatter on the background thread.
148
+ with AppFormatter on the background thread.
141
149
  """
142
150
 
143
151
  def __init__(
@@ -155,12 +163,17 @@ class AsyncJsonHandler(QueueHandler):
155
163
  sync_handler = RotatingFileHandler(
156
164
  filename, mode, maxBytes, backupCount, encoding, delay
157
165
  )
158
- sync_handler.setFormatter(JsonFormatter())
166
+ sync_handler.setFormatter(AppFormatter())
159
167
  self._listener = QueueListener(
160
168
  log_queue, sync_handler, respect_handler_level=True
161
169
  )
162
170
  self._listener.start()
163
171
 
172
+ def prepare(self, record):
173
+ record = super().prepare(record)
174
+ record.request_id = get_request_id() or ""
175
+ return record
176
+
164
177
  def close(self):
165
178
  self._listener.stop()
166
179
  super().close()
@@ -1,12 +1,6 @@
1
1
  import logging
2
2
 
3
- from django.apps import apps
4
3
  from django.conf import settings
5
- from django.contrib.auth.models import Permission
6
- from django.contrib.contenttypes.models import ContentType
7
- from django.contrib.sessions.models import Session
8
- from django.db.migrations import Migration
9
- from django.db.migrations.recorder import MigrationRecorder
10
4
 
11
5
 
12
6
  # Handles AUDIT/API level as INFO for Sentry
@@ -32,43 +26,6 @@ for handler in root_logger.handlers:
32
26
  if handler.__class__.__name__.startswith("Sentry"):
33
27
  handler.addFilter(AuditToSentryFilter())
34
28
 
35
- UNREGISTERED_CLASSES = [
36
- Migration,
37
- Session,
38
- Permission,
39
- ContentType,
40
- MigrationRecorder.Migration,
41
- ]
42
-
43
- # Remove silk models audit logging
44
- SILK_INSTALLED = apps.is_installed("silk")
45
- if SILK_INSTALLED:
46
- from silk.models import (
47
- BaseProfile,
48
- Profile,
49
- Request,
50
- Response,
51
- SQLQuery,
52
- SQLQueryManager,
53
- )
54
-
55
- UNREGISTERED_CLASSES.extend(
56
- [
57
- Request,
58
- Response,
59
- SQLQueryManager,
60
- SQLQuery,
61
- BaseProfile,
62
- Profile,
63
- ]
64
- )
65
-
66
- # Import and unregister LogEntry class only if Django Admin app is installed
67
- if apps.is_installed("django.contrib.admin"):
68
- from django.contrib.admin.models import LogEntry
69
-
70
- UNREGISTERED_CLASSES.extend([LogEntry])
71
-
72
29
  # URL patterns to exclude from logging
73
30
  UNREGISTERED_URLS = [r"^/admin/", r"^/static/", r"^/favicon.ico$"]
74
31
  UNREGISTERED_URLS = getattr(
@@ -16,7 +16,7 @@ from django.dispatch import receiver
16
16
  from django.forms.models import model_to_dict
17
17
 
18
18
  from activity_audit.middleware import get_request_id, get_user_details
19
- from activity_audit.settings import UNREGISTERED_CLASSES
19
+ from activity_audit.unregistered import UNREGISTERED_CLASSES
20
20
 
21
21
  logger = logging.getLogger("audit.model")
22
22
 
@@ -0,0 +1,33 @@
1
+ from django.apps import apps
2
+ from django.contrib.auth.models import Permission
3
+ from django.contrib.contenttypes.models import ContentType
4
+ from django.contrib.sessions.models import Session
5
+ from django.db.migrations import Migration
6
+ from django.db.migrations.recorder import MigrationRecorder
7
+
8
+ UNREGISTERED_CLASSES = [
9
+ Migration,
10
+ Session,
11
+ Permission,
12
+ ContentType,
13
+ MigrationRecorder.Migration,
14
+ ]
15
+
16
+ if apps.is_installed("silk"):
17
+ from silk.models import (
18
+ BaseProfile,
19
+ Profile,
20
+ Request,
21
+ Response,
22
+ SQLQuery,
23
+ SQLQueryManager,
24
+ )
25
+
26
+ UNREGISTERED_CLASSES.extend(
27
+ [Request, Response, SQLQueryManager, SQLQuery, BaseProfile, Profile]
28
+ )
29
+
30
+ if apps.is_installed("django.contrib.admin"):
31
+ from django.contrib.admin.models import LogEntry
32
+
33
+ UNREGISTERED_CLASSES.extend([LogEntry])
@@ -7,9 +7,19 @@ def get_console_formatter() -> dict:
7
7
  }
8
8
 
9
9
 
10
- def get_json_formatter() -> dict:
10
+ def get_app_formatter() -> dict:
11
11
  return {
12
- "()": "activity_audit.formatters.JsonFormatter",
12
+ "()": "activity_audit.formatters.AppFormatter",
13
+ }
14
+
15
+ def get_api_formatter() -> dict:
16
+ return {
17
+ "()": "activity_audit.formatters.APIFormatter"
18
+ }
19
+
20
+ def get_audit_formatter() -> dict:
21
+ return {
22
+ "()": "activity_audit.formatters.AuditFormatter"
13
23
  }
14
24
 
15
25
 
@@ -30,6 +40,7 @@ def get_json_handler(
30
40
  }
31
41
 
32
42
 
43
+
33
44
  def get_api_handler(
34
45
  filename: str = "audit_logs/api.log",
35
46
  ) -> dict:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "django-activity-audit"
3
- version = "1.3.0.dev11"
3
+ version = "1.3.0.dev13"
4
4
  description = "A Django package for easy CRUD operation logging and container logs"
5
5
  authors = [
6
6
  { name = "Shreeshan", email = "shreeshan256@gmail.com" }