Dynamic Models

General use case

Sometimes you need to generate reports from database tables that don’t have a corresponding Django model. This is common with:

  • Legacy database tables

  • Data warehouse / analytics tables

  • Tables managed by other systems or ETL pipelines

  • Temporary or staging tables

Django Slick Reporting provides get_dynamic_model which introspects any database table and creates a real Django model at runtime. Since the result is a genuine Django model, all report features work natively: group by, time series, crosstab, aggregation, filtering, and charts.

Basic usage

Use the get_dynamic_model utility to create a model from an existing table:

from slick_reporting.dynamic_model import get_dynamic_model

# Introspect the table and get a Django model class
SalesData = get_dynamic_model("legacy_sales")

# Use it like any Django model
SalesData.objects.all()
SalesData.objects.filter(region="US").count()

Then use it with ReportGenerator or ReportView as usual:

from slick_reporting.views import ReportView, Chart
from slick_reporting.fields import ComputationField
from slick_reporting.dynamic_model import get_dynamic_model
from django.db.models import Sum

SalesData = get_dynamic_model("legacy_sales")

class LegacySalesReport(ReportView):
    report_model = SalesData
    date_field = "sale_date"
    group_by = "product_id"

    columns = [
        "product_id",
        ComputationField.create(
            method=Sum,
            field="amount",
            name="amount__sum",
            verbose_name="Total Sales",
        ),
    ]

    chart_settings = [
        Chart(
            "Total Sales",
            Chart.BAR,
            data_source=["amount__sum"],
            title_source=["product_id"],
        ),
    ]

Using table_name shortcut

Instead of calling get_dynamic_model yourself, you can set table_name directly on ReportView or pass it to ReportGenerator:

# On a view
class LegacySalesReport(ReportView):
    table_name = "legacy_sales"
    date_field = "sale_date"
    group_by = "product_id"

    columns = [
        "product_id",
        ComputationField.create(
            method=Sum,
            field="amount",
            name="amount__sum",
            verbose_name="Total Sales",
        ),
    ]
# With ReportGenerator directly
from slick_reporting.generator import ReportGenerator

report = ReportGenerator(
    table_name="legacy_sales",
    date_field="sale_date",
    group_by="product_id",
    columns=[
        "product_id",
        ComputationField.create(Sum, "amount", name="amount__sum", verbose_name="Total Sales"),
    ],
    start_date=start,
    end_date=end,
)
data = report.get_report_data()

When table_name is set and report_model is not, the dynamic model is created automatically.

Using a different database

If the table is in a non-default database, pass the database parameter:

WarehouseData = get_dynamic_model("fact_sales", database="warehouse")

This uses the database alias defined in your Django DATABASES setting.

Working with database schemas

On PostgreSQL, tables can live in different schemas (e.g. analytics, staging).

Prerequisites: The schema must be accessible via the connection’s search_path so Django’s introspection can find the table. You can configure this in your database settings:

# settings.py
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "mydb",
        "OPTIONS": {
            "options": "-c search_path=analytics,public",
        },
    }
}

Then pass the schema parameter so ORM queries reference the correct schema:

SalesData = get_dynamic_model("sales", schema="analytics")

This sets the model’s db_table to "analytics"."sales" so all generated SQL uses the schema-qualified table name.

Schema support by database backend:

Backend

How schemas work

PostgreSQL

Use schema parameter + ensure schema is in search_path

MySQL

Schemas are databases. Use the database parameter instead

SQLite

No schema concept. Just use table_name

How it works

get_dynamic_model performs the following steps:

  1. Connects to the database and verifies the table exists.

  2. Uses Django’s connection.introspection to read column names, types, and constraints.

  3. Maps each database column to the appropriate Django field (IntegerField, CharField, DateField, etc.).

  4. Creates a Django model class at runtime with managed = False (no migrations needed).

  5. Registers the model in Django’s app registry so the ORM works fully.

  6. Caches the result so subsequent calls for the same table return the same model class.

Since the result is a standard Django model, all ReportGenerator features work without any special handling: group_by, time_series_pattern, crosstab_field, ComputationField, form generation, and chart rendering.

Caching

Dynamic models are cached in memory. Calling get_dynamic_model("my_table") multiple times returns the same model class. The cache key includes the database alias and schema, so models for different databases or schemas are cached separately.

Limitations

  • No foreign key relationships. Foreign key columns are introspected as plain integer or varchar fields. This means group_by traversal across relationships (e.g. product__category) is not available. You can group by the FK column directly (e.g. product_id).

  • No automatic form filters for FK fields. Since there are no FK relations, the auto-generated filter form will only contain date range fields. Supply a custom form_class if you need additional filters.

  • Schema must be in search_path. On PostgreSQL, Django’s introspection only finds tables visible in the current search_path. The schema must be configured at the connection level.

  • Table structure changes require restart. Because models are cached, if the table structure changes (columns added/removed), you need to restart the application or clear the cache.