Skip to main content

django-icv-core

CI PyPI version Python versions Django versions License: MIT

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 and auto-managed created_at/updated_at timestamps
  • SoftDeleteModel: Safe record removal with soft_delete() and restore(); default manager excludes deleted records automatically
  • SoftDeleteManager / SoftDeleteQuerySet: .active(), .deleted(), .with_deleted() on every soft-delete model
  • CurrentUserMiddleware: Async-safe (contextvars-backed) request user for automatic created_by/updated_by population
  • Audit subsystem (opt-in via ICV_CORE_AUDIT_ENABLED): Immutable AuditEntry, AdminActivityLog, SystemAlert, AuditMixin, @audited decorator, 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.testing provides 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 bundled icv_core migration creates the audit tables whenever icv_core is migrated. ICV_CORE_AUDIT_ENABLED gates audit writes and signal-handler connection only. Add django.contrib.contenttypes to INSTALLED_APPS before 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:
        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

No default ordering. BaseModel deliberately sets no default Meta.ordering (ADR-066): a Meta.ordering on a shared base defeats values()/values_list() combined with distinct() in every inheriting model. A model that needs newest-first (or any other default order) declares ordering in its own Meta, and any values()/distinct() query on an ordered model must call .order_by() explicitly, since distinct() folds ordering columns into the comparison.

UUID version runtime generation

By default the primary key is a random UUID v4. ICV_CORE_UUID_VERSION = 7 is consulted only when a VersionedUUIDField reaches pre_save() with no primary-key value. Ordinary BaseModel.objects.create() receives the migration-safe uuid.uuid4 default first, so its primary key remains v4.

The version is a runtime choice, it is never baked into migrations (the pk always freezes as UUIDField(default=uuid.uuid4)), so changing it generates no migration and affects only rows created afterwards.

To choose the runtime-generated version for a creation path that explicitly clears the primary key before saving, override the pk with VersionedUUIDField(uuid_version=...). This does not alter its migration state and, by itself, does not change ordinary model construction, which still receives uuid.uuid4 as its default:

from icv_core.models import BaseModel
from icv_core.models.base import VersionedUUIDField


class SiteEvent(BaseModel):
    # Used only when the creation path sets event.id = None before saving.
    id = VersionedUUIDField(primary_key=True, editable=False, uuid_version=7)

The uuid_version override is runtime-only and does not appear in the field's migration state. It does not make ordinary SiteEvent.objects.create() values v7.


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. The user is stored in a contextvars.ContextVar, so concurrent requests handled on the same thread under ASGI (async views, or sync_to_async-bridged sync views) each see their own value; a request never observes another concurrently-running request's user.

# 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 icv-core 1.0.0 per ADR-025 (ruling T3). 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

Most settings use the ICV_CORE_ prefix. Every setting has a sensible default: only override what you need. The exception is ICV_AUTH_USER_MODEL, a fleet-global name shared by every icv package (ADR-037), never a per-package ICV_CORE_ variant.

Core

Setting Default Description
ICV_CORE_UUID_VERSION 4 Used only when a VersionedUUIDField has no primary-key value at pre_save(): 4 = random; 7 = time-sorted. Ordinary BaseModel.objects.create() uses the migration-safe v4 default.
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_AUTH_USER_MODEL settings.AUTH_USER_MODEL Which model icv-core's user FKs target (AuditEntry.user, AdminActivityLog.admin_user, SystemAlert.resolved_by, ComplianceModel.created_by/updated_by). Fleet-global (ADR-037); leave unset to use the project's own AUTH_USER_MODEL

Audit

Setting Default Description
ICV_CORE_AUDIT_ENABLED False Gates audit writes and audit signal-handler connection. It does not control migration-created audit tables.
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 Compatibility setting currently unread by runtime code. It does not enable global model tracking.
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 Report entries older than ICV_CORE_AUDIT_RETENTION_DAYS that are eligible for archival. It does not archive, export, or delete rows.
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.

Metadata

Release files for django-icv-core 0.6.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-icv-core 0.6.1
File Size Uploaded
django_icv_core-0.6.1.tar.gz 67.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-icv-core 0.6.1
File Interpreter ABI Platform
django_icv_core-0.6.1-py3-none-any.whl Python 3 none any Details

Total release size: 119.2 kB

Release files / django_icv_core-0.6.1.tar.gz

Download URL django_icv_core-0.6.1.tar.gz
Size 67.6 kB
Tags Source
SHA-256 checksum
How to use checksums
c7531d904823124455358e38f46d97f13962c077916e77f4dafd686ec9e22277
BLAKE2b-256 checksum
How to use checksums
a07311b5feb6779de3e5be353b0285da4ca2258d67223c2273fe1c7a03b8a395
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.

Transparency log

Release files / django_icv_core-0.6.1-py3-none-any.whl

Download URL django_icv_core-0.6.1-py3-none-any.whl
Size 51.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
330c3ffdd96b64bbadd8d8b02c05ff4f793d65bd534166b6394cf9b6c03dd532
BLAKE2b-256 checksum
How to use checksums
2c4b559af07213c2d7f4c68f5ca7457be43ed9af9bca392089f07ccaad617561
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.6.1 This release

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.5

2 release files

0.4.4

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page