Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 22 additions & 20 deletions django/CONTRIBUTE.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,30 +76,30 @@ To test the library in a separate Django project:
```python
# settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken', # Required for Token authentication
'rest_framework_simplejwt', # Required for JWT authentication
'nside_wefa.common', # Must come before other nside_wefa apps
'nside_wefa.authentication', # Add the Authentication app
'nside_wefa.legal_consent', # Add the LegalConsent app
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"rest_framework.authtoken", # Required for Token authentication
"rest_framework_simplejwt", # Required for JWT authentication
"nside_wefa.common", # Must come before other nside_wefa apps
"nside_wefa.authentication", # Add the Authentication app
"nside_wefa.legal_consent", # Add the LegalConsent app
]

# Configuration
NSIDE_WEFA = {
'APP_NAME': 'My App',
'AUTHENTICATION': {
'TYPES': ['TOKEN', 'JWT'],
"APP_NAME": "My App",
"AUTHENTICATION": {
"TYPES": ["TOKEN", "JWT"],
},
"LEGAL_CONSENT": {
"VERSION": 1,
"EXPIRY_LIMIT": 365,
},
'LEGAL_CONSENT': {
'VERSION': 1,
'EXPIRY_LIMIT': 365,
}
}
```

Expand Down Expand Up @@ -214,6 +214,7 @@ from typing import Optional
from django.contrib.auth.models import User
from nside_wefa.legal_consent.models import LegalConsent


def create_user_agreement(user: User, version: Optional[int] = None) -> LegalConsent:
"""Create a legal consent for the given user."""
# Implementation here
Expand Down Expand Up @@ -301,6 +302,7 @@ Create an `apps.py` file:
```python
from django.apps import AppConfig


class YourLibraryConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "nside_wefa.your_library"
Expand Down
2 changes: 1 addition & 1 deletion django/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Wraps `django-auditlog` to give every product an append-only audit store with fo
"rest_framework",
"rest_framework.authtoken", # For token auth
"rest_framework_simplejwt", # For JWT auth
"auditlog", # Required by nside_wefa.audit
"auditlog", # Required by nside_wefa.audit
"nside_wefa.common",
"nside_wefa.authentication",
"nside_wefa.legal_consent",
Expand Down
2 changes: 1 addition & 1 deletion django/demo/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"""

from django.contrib import admin
from django.urls import path, include
from django.urls import include, path

urlpatterns = [
path("admin/", admin.site.urls),
Expand Down
6 changes: 3 additions & 3 deletions django/docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import os
import sys
from datetime import datetime
from datetime import UTC, datetime
from pathlib import Path

# -- Path setup --------------------------------------------------------------
Expand All @@ -22,7 +22,7 @@
# -- Project information -----------------------------------------------------
project = "N-SIDE WeFa"
organization = "N-SIDE"
current_year = datetime.now().year
current_year = datetime.now(tz=UTC).year
copyright = f"{current_year}, {organization}"

# -- Django setup ------------------------------------------------------------
Expand All @@ -32,7 +32,7 @@
import django # type: ignore

django.setup()
except Exception as exc: # pragma: no cover - docs build environment only
except Exception as exc: # noqa: BLE001 - pragma: no cover; docs must build even if Django setup fails
# Don't fail import-time; Sphinx will still build non-autodoc pages.
# The CI sets DJANGO_SETTINGS_MODULE and has Django installed.
print(f"[sphinx conf] Warning: Django setup failed: {exc}")
Expand Down
10 changes: 7 additions & 3 deletions django/nside_wefa/audit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,13 @@ The recommended primary path. Call at the bottom of `models.py`:
from django.db import models
from nside_wefa.audit import register


class Order(models.Model):
status = models.CharField(max_length=32)
total = models.DecimalField(max_digits=10, decimal_places=2)
total = models.DecimalField(max_digits=10, decimal_places=2)
secret_token = models.CharField(max_length=64)


register(
Order,
include_fields=["status", "total"],
Expand All @@ -93,10 +95,11 @@ register(
```python
from nside_wefa.audit import audited


@audited(include_fields=["status", "total"])
class Order(models.Model):
status = models.CharField(max_length=32)
total = models.DecimalField(max_digits=10, decimal_places=2)
total = models.DecimalField(max_digits=10, decimal_places=2)
```

### Path C — `AuditAppConfigMixin`
Expand All @@ -107,10 +110,11 @@ Keep models clean by declaring registrations on your `AppConfig`:
from django.apps import AppConfig
from nside_wefa.audit import AuditAppConfigMixin


class OrdersConfig(AuditAppConfigMixin, AppConfig):
name = "orders"
audited_models = {
"Order": {"include_fields": ["status", "total"]},
"Order": {"include_fields": ["status", "total"]},
"Customer": {"exclude_fields": ["last_login_ip"]},
}
```
Expand Down
10 changes: 5 additions & 5 deletions django/nside_wefa/audit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ def __getattr__(name: str):


__all__ = [
"register",
"audited",
"AuditAppConfigMixin",
"AuditEventImmutableError",
Comment thread
srozen marked this conversation as resolved.
"AuditWriteError",
Comment thread
srozen marked this conversation as resolved.
"Outcome",
Comment thread
srozen marked this conversation as resolved.
"audited",
Comment thread
srozen marked this conversation as resolved.
"log",
"register",
Comment thread
srozen marked this conversation as resolved.
"set_actor",
"Outcome",
"AuditWriteError",
"AuditEventImmutableError",
]
12 changes: 6 additions & 6 deletions django/nside_wefa/audit/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
audit events specifically are read-only.
"""

from typing import Any, Optional
from typing import Any

from auditlog.models import LogEntry
from django.contrib import admin
Expand Down Expand Up @@ -66,17 +66,17 @@ class LogEntryAdmin(admin.ModelAdmin):
"serialized_data",
)

def has_add_permission(self, request: HttpRequest) -> bool: # noqa: D401
def has_add_permission(self, request: HttpRequest) -> bool:
return False

def has_change_permission(
self, request: HttpRequest, obj: Optional[Any] = None
) -> bool: # noqa: D401
self, request: HttpRequest, obj: Any | None = None
) -> bool:
return False

def has_delete_permission(
self, request: HttpRequest, obj: Optional[Any] = None
) -> bool: # noqa: D401
self, request: HttpRequest, obj: Any | None = None
) -> bool:
return False

@admin.display(description="action")
Expand Down
20 changes: 10 additions & 10 deletions django/nside_wefa/audit/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import enum
import logging
from typing import Any, Dict, Optional
from typing import Any

from auditlog import get_logentry_model
from auditlog.context import set_actor as _auditlog_set_actor
Expand Down Expand Up @@ -63,11 +63,11 @@ def log(
action: str,
*,
actor: Any = _UNSET,
target: Optional[models.Model] = None,
changes: Optional[Dict[str, Any]] = None,
metadata: Optional[Dict[str, Any]] = None,
target: models.Model | None = None,
changes: dict[str, Any] | None = None,
metadata: dict[str, Any] | None = None,
outcome: Outcome = Outcome.SUCCESS,
) -> Optional[AbstractLogEntry]:
) -> AbstractLogEntry | None:
"""Write an audit event and return the resulting log entry.

Returns an instance of whichever model auditlog has been told to use —
Expand Down Expand Up @@ -98,7 +98,7 @@ def log(
redacted_changes = _redact(changes, redact_fields) if changes else None
redacted_metadata = _redact(metadata, redact_fields) if metadata else {}

additional_data: Dict[str, Any] = {
additional_data: dict[str, Any] = {
"outcome": outcome.value,
"action": action,
}
Expand All @@ -110,7 +110,7 @@ def log(
# base, so it's safe to read off the active class.
log_model = get_logentry_model()

create_kwargs: Dict[str, Any] = {
create_kwargs: dict[str, Any] = {
"action": log_model.Action.UPDATE,
"additional_data": additional_data,
}
Expand Down Expand Up @@ -149,7 +149,7 @@ def log(

try:
return log_model.objects.create(**create_kwargs)
except Exception as exc: # noqa: BLE001 — by design; see RAISE_ON_FAILURE
except Exception as exc:
if raise_on_failure:
raise AuditWriteError(
f"Failed to write audit event {action!r}: {exc}"
Expand Down Expand Up @@ -182,7 +182,7 @@ def _resolve_actor(actor: Any) -> Any:
return actor


def _redact(payload: Dict[str, Any], redact_fields: list) -> Dict[str, Any]:
def _redact(payload: dict[str, Any], redact_fields: list) -> dict[str, Any]:
"""Return a shallow copy of ``payload`` with sensitive values masked.

Keys are matched case-insensitively. Nested dicts are walked recursively
Expand All @@ -192,7 +192,7 @@ def _redact(payload: Dict[str, Any], redact_fields: list) -> Dict[str, Any]:
if not isinstance(payload, dict):
return payload
redact_lower = {f.lower() for f in redact_fields}
out: Dict[str, Any] = {}
out: dict[str, Any] = {}
for key, value in payload.items():
if isinstance(key, str) and key.lower() in redact_lower:
out[key] = "[REDACTED]"
Expand Down
3 changes: 1 addition & 2 deletions django/nside_wefa/audit/builtin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

import importlib
import logging
from typing import Tuple

from django.apps import apps as django_apps

Expand All @@ -22,7 +21,7 @@
logger = logging.getLogger("nside_wefa.audit")

# Source name → (Django app label that must be installed, importable submodule).
KNOWN_SOURCES: dict[str, Tuple[str, str]] = {
KNOWN_SOURCES: dict[str, tuple[str, str]] = {
"auth": ("auth", "nside_wefa.audit.builtin.auth"),
"legal_consent": (
"nside_wefa.legal_consent",
Expand Down
9 changes: 4 additions & 5 deletions django/nside_wefa/audit/builtin/locale.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,10 @@ def _snapshot_code(sender: Any, instance: Any, **kwargs: Any) -> None:


def _on_locale_saved(sender: Any, instance: Any, created: bool, **kwargs: Any) -> None:
if created:
# New row with an empty code: not interesting.
if instance.code is None:
setattr(instance, _SNAPSHOT_ATTR, instance.code)
return
# New row with an empty code: not interesting.
if created and instance.code is None:
setattr(instance, _SNAPSHOT_ATTR, instance.code)
return

previous = getattr(instance, _SNAPSHOT_ATTR, None)
current = instance.code
Expand Down
16 changes: 8 additions & 8 deletions django/nside_wefa/audit/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
unknown source name is an error.
"""

from typing import Any, List
from typing import Any

from django.core.checks import Error, register

Expand All @@ -34,21 +34,21 @@


@register()
def wefa_apps_dependencies_check(app_configs, **kwargs) -> List[Error]:
def wefa_apps_dependencies_check(app_configs, **kwargs) -> list[Error]:
"""Validate INSTALLED_APPS order.

``nside_wefa.common`` and ``auditlog`` are independent prerequisites of
``nside_wefa.audit`` — their relative order does not matter, but each
must precede ``nside_wefa.audit``.
"""
errors: List[Error] = []
errors: list[Error] = []
errors.extend(check_apps_dependencies_order([CommonConfig.name, AuditConfig.name]))
errors.extend(check_apps_dependencies_order(["auditlog", AuditConfig.name]))
return errors


@register()
def audit_settings_check(app_configs, **kwargs) -> List[Error]:
def audit_settings_check(app_configs, **kwargs) -> list[Error]:
"""Validate the ``NSIDE_WEFA.AUDIT`` settings section.

``NSIDE_WEFA.AUDIT`` itself is optional — a missing or empty section is
Expand Down Expand Up @@ -93,16 +93,16 @@ def audit_settings_check(app_configs, **kwargs) -> List[Error]:
),
}

errors: List[Error] = []
errors: list[Error] = []
for key, validator in validators.items():
if key in section:
errors.extend(validator(section[key]))
return errors


def _validate_exclude_models(value: Any) -> List[Error]:
def _validate_exclude_models(value: Any) -> list[Error]:
"""``EXCLUDE_MODELS`` must be a list of resolvable ``"app.Model"`` labels."""
errors: List[Error] = []
errors: list[Error] = []
if not isinstance(value, list):
return [
Error(
Expand All @@ -120,7 +120,7 @@ def _validate_exclude_models(value: Any) -> List[Error]:
def _validate_non_empty_string(setting_path: str):
"""Inline helper for plain non-empty string settings."""

def _validator(value: Any) -> List[Error]:
def _validator(value: Any) -> list[Error]:
if not isinstance(value, str) or not value:
return [
Error(
Expand Down
3 changes: 2 additions & 1 deletion django/nside_wefa/audit/immutability.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
DELETE`` at the database level; this is documented in the README.
"""

from collections.abc import Iterator
from contextlib import contextmanager
from typing import Any, Iterator
from typing import Any

from django.db.models.signals import pre_delete, pre_save

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

import csv
import json
from typing import Any, Iterable
from collections.abc import Iterable
from typing import Any

from auditlog import get_logentry_model
from auditlog.models import AbstractLogEntry
Expand Down
Loading
Loading