django-icv-core is the foundation layer for the ICV Django ecosystem. It gives every model in your project a UUID primary key, auto-managed timestamps, and optional soft-delete behaviour, with zero boilerplate. Add the optional audit subsystem and you get immutable event logs, admin activity tracking, and system alerts with a single setting.
Use it as a standalone package or as the base for other django-icv-* packages.
Features
BaseModel: UUID primary key (v4 by default, v7 opt-in) and auto-managedcreated_at/updated_attimestampsSoftDeleteModel: Safe record removal withsoft_delete()andrestore(); default manager excludes deleted records automaticallySoftDeleteManager/SoftDeleteQuerySet:.active(),.deleted(),.with_deleted()on every soft-delete modelCurrentUserMiddleware: Thread-local request user for automaticcreated_by/updated_bypopulation- Audit subsystem (opt-in via
ICV_CORE_AUDIT_ENABLED): ImmutableAuditEntry,AdminActivityLog,SystemAlert,AuditMixin,@auditeddecorator, and management commands - Template tags:
cents_to_currency,cents_to_amount,time_since_short - Django system checks: Configuration validation at startup
- Test utilities:
icv_core.testingprovides factory-boy factories and pytest fixtures for consuming projects
Requirements
- Python 3.11+
- Django 5.2 or 6.0
Installation
pip install django-icv-core
Add icv_core to INSTALLED_APPS:
INSTALLED_APPS = [
# ...
"icv_core",
]
Run migrations:
python manage.py migrate
Note: The audit subsystem tables are only created when
ICV_CORE_AUDIT_ENABLED = True. Adddjango.contrib.contenttypestoINSTALLED_APPSbefore enabling audit.
Quick Start
BaseModel: UUID primary keys and timestamps
Inherit from BaseModel and every record gets a UUID primary key and automatic timestamps. No extra fields to define.
from django.db import models
from icv_core.models import BaseModel
class Article(BaseModel):
title = models.CharField(max_length=255)
body = models.TextField()
class Meta(BaseModel.Meta):
verbose_name_plural = "articles"
article = Article.objects.create(title="Hello, world", body="...")
article.id # UUID4: e.g. '3f2504e0-4f89-11d3-9a0c-0305e82c3301'
article.created_at # datetime: set on creation, never changes
article.updated_at # datetime: updated automatically on every save
SoftDeleteModel: safe record removal
Records are never hard-deleted by default. soft_delete() sets is_active=False and records the timestamp. The default manager silently excludes deleted records from all queries.
from icv_core.models import SoftDeleteModel
class Subscription(SoftDeleteModel):
plan = models.CharField(max_length=50)
customer_email = models.EmailField()
sub = Subscription.objects.create(plan="pro", customer_email="user@example.com")
# Soft-delete: hides the record from default queries
sub.soft_delete()
sub.is_active # False
sub.deleted_at # datetime
# Default manager returns active records only
Subscription.objects.all() # excludes deleted records
Subscription.objects.active() # same as above: explicit alias
# Access deleted or all records when needed
Subscription.objects.deleted() # deleted records only
Subscription.objects.with_deleted() # everything
Subscription.all_objects.all() # raw manager: no filtering applied
# Restore
sub.restore()
sub.is_active # True
sub.deleted_at # None
Hard deletion is blocked unless you explicitly allow it:
# Raises ProtectedError by default
sub.delete()
# Permanent removal: use deliberately
sub.hard_delete()
# Or allow .delete() project-wide
ICV_CORE_ALLOW_HARD_DELETE = True
CurrentUserMiddleware: automatic created_by/updated_by
Add the middleware to make the current request user available to models without passing it through every service call.
# settings.py
MIDDLEWARE = [
# ...
"django.contrib.auth.middleware.AuthenticationMiddleware",
"icv_core.middleware.CurrentUserMiddleware", # must come after AuthenticationMiddleware
# ...
]
ICV_CORE_TRACK_CREATED_BY = True
from icv_core.middleware import get_current_user
user = get_current_user() # returns None outside of a request context
Audit subsystem
Enable the audit subsystem in settings:
ICV_CORE_AUDIT_ENABLED = True
AuditEntry: an immutable record of a system event. Raises ImmutableRecordError on update and ProtectedError on delete.
from icv_core.audit.services import log_event
from icv_core.audit.models import AuditEntry
# Record a security event
log_event(
event_type=AuditEntry.EventType.SECURITY,
action=AuditEntry.Action.PERMISSION_DENIED,
user=request.user,
description="Attempted access to restricted resource.",
metadata={"path": request.path},
)
AuditMixin: add to any model to track CREATE, UPDATE, and DELETE automatically:
from icv_core.audit.mixins import AuditMixin
from icv_core.models import BaseModel
class Contract(AuditMixin, BaseModel):
title = models.CharField(max_length=255)
value = models.DecimalField(max_digits=10, decimal_places=2)
@audited decorator: wrap a view or service function to record its execution:
from icv_core.audit.decorators import audited
from icv_core.audit.models import AuditEntry
@audited(event_type=AuditEntry.EventType.DATA, action=AuditEntry.Action.DELETE)
def cancel_membership(user, membership):
membership.soft_delete()
SystemAlert: raise and resolve operational alerts:
from icv_core.audit.services import raise_alert, resolve_alert
from icv_core.audit.models import SystemAlert
alert = raise_alert(
alert_type=SystemAlert.AlertType.PAYMENT,
severity="error",
title="Payment processor unreachable",
message="Stripe API returned 503 for the last 5 minutes.",
)
# Later, once resolved
resolve_alert(alert, resolved_by=request.user, notes="Stripe incident resolved.")
Tenancy (deprecated)
icv_core.tenancy (TenantAwareMixin, TenantOwnedMixin, TenantScopedManager, and the get/set/clear_current_tenant context helpers) is deprecated and will be removed in a future release. Subclassing either mixin now emits a DeprecationWarning.
Use django-boundary instead. It provides row-level isolation backed by PostgreSQL Row Level Security, automatic query filtering, and a configurable tenant FK name.
Deprecated (icv_core.tenancy) |
Replacement (django-boundary) |
|---|---|
TenantAwareMixin (FK on_delete=PROTECT) |
boundary.models.TenantModel |
TenantOwnedMixin (FK on_delete=CASCADE) |
boundary.models.TenantModel (CASCADE is the default) |
TenantScopedManager (explicit .for_tenant()) |
boundary.models.TenantManager (automatic filtering) |
get_current_tenant() |
boundary.context.TenantContext.get() |
set_current_tenant() |
boundary.context.TenantContext.set() |
clear_current_tenant() |
boundary.context.TenantContext.clear() |
tenant_context() |
boundary.context.TenantContext.using() |
Template tags
{% load icv_core %}
{# Format pence/cents as a currency string #}
{{ order.total_pence|cents_to_currency:"GBP" }} {# £35.00 #}
{{ order.total_pence|cents_to_currency:"USD" }} {# $35.00 #}
{# Raw decimal amount without currency symbol #}
{{ order.total_pence|cents_to_amount }} {# 35.00 #}
{# Short human-readable relative time #}
{{ comment.created_at|time_since_short }} {# 2h ago / 3d ago / just now #}
Settings reference
All settings use the ICV_CORE_ prefix. Every setting has a sensible default: only override what you need.
Core
| Setting | Default | Description |
|---|---|---|
ICV_CORE_UUID_VERSION |
4 |
UUID version for primary keys. 4 = random; 7 = time-sorted (requires Python 3.12+) |
ICV_CORE_ALLOW_HARD_DELETE |
False |
When True, .delete() performs a hard delete on SoftDeleteModel instead of raising ProtectedError |
ICV_CORE_TRACK_CREATED_BY |
False |
Enable created_by/updated_by tracking. Requires CurrentUserMiddleware |
ICV_CORE_DEFAULT_ORDERING |
"-created_at" |
Default ordering applied to BaseModel subclasses |
Audit
| Setting | Default | Description |
|---|---|---|
ICV_CORE_AUDIT_ENABLED |
False |
Master switch. No tables are created and no signals connect when False |
ICV_CORE_AUDIT_RETENTION_DAYS |
365 |
Days before audit entries are eligible for archival |
ICV_CORE_AUDIT_EXCLUDE_MODELS |
[] |
Models excluded from AuditMixin auto-tracking. Format: ["app_label.ModelName"] |
ICV_CORE_AUDIT_TRACK_FIELD_CHANGES |
True |
Capture old and new field values on UPDATE |
ICV_CORE_AUDIT_CAPTURE_IP |
True |
Record the request IP address in audit entries |
ICV_CORE_AUDIT_CAPTURE_USER_AGENT |
True |
Record the request user agent in audit entries |
ICV_CORE_AUDIT_AUTO_MODEL_TRACKING |
False |
Automatically log CREATE/UPDATE/DELETE on all BaseModel subclasses |
ICV_CORE_AUDIT_ALERT_SEVERITY_LEVELS |
["info", "warning", "error", "critical"] |
Available severity levels for SystemAlert |
Management commands
| Command | Description |
|---|---|
icv_core_check |
Validate package configuration and emit any warnings |
icv_core_audit_archive |
Archive audit entries older than ICV_CORE_AUDIT_RETENTION_DAYS |
icv_core_audit_stats |
Print a summary of audit entry counts by event type and action |
Testing utilities
icv_core.testing provides factory-boy factories and pytest fixtures for use in your own project's test suite.
# In your tests
from icv_core.testing.factories import AuditEntryFactory, SystemAlertFactory
entry = AuditEntryFactory(action="CREATE", event_type="DATA")
alert = SystemAlertFactory(severity="critical", alert_type="payment")
Import the included pytest fixtures by adding icv_core.testing.fixtures to your conftest.py:
# conftest.py
pytest_plugins = ["icv_core.testing.fixtures"]
Signals
icv-core emits the following signals. Connect to them from your own apps for loose coupling.
| Signal | Sent when |
|---|---|
icv_core.signals.pre_soft_delete |
Before a record is soft-deleted |
icv_core.signals.post_soft_delete |
After a record is soft-deleted |
icv_core.signals.pre_restore |
Before a soft-deleted record is restored |
icv_core.signals.post_restore |
After a soft-deleted record is restored |
icv_core.audit.signals.audit_entry_created |
After an AuditEntry is written |
icv_core.audit.signals.system_alert_raised |
After a SystemAlert is raised |
icv_core.audit.signals.system_alert_resolved |
After a SystemAlert is resolved |
Licence
MIT: see LICENSE.