Skip to content

utils

TimeLimitedPaginator

Bases: Paginator

Paginator that enforces a timeout on the count operation. If the operations times out, a fake bogus value is returned instead.

Source code in src/django_admin_magic/utils.py
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
class TimeLimitedPaginator(Paginator):
    """
    Paginator that enforces a timeout on the count operation.
    If the operations times out, a fake bogus value is
    returned instead.
    """

    def __init__(self, object_list, per_page, orphans=0, allow_empty_first_page=True):
        # Validate per_page
        if per_page is not None and (not isinstance(per_page, int) or per_page <= 0):
            raise ValueError("per_page must be a positive integer or None")
        super().__init__(object_list, per_page, orphans, allow_empty_first_page)

    @cached_property
    def count(self):
        # We set the timeout in a db transaction to prevent it from
        # affecting other transactions.
        try:
            with transaction.atomic(), connection.cursor() as cursor:
                # Only set statement_timeout for PostgreSQL
                if connection.vendor == "postgresql":
                    cursor.execute("SET LOCAL statement_timeout TO 1000;")
                return super().count
        except OperationalError:
            with transaction.atomic(), connection.cursor() as cursor:
                # Obtain estimated values (only valid with PostgreSQL)
                if not self.object_list.query.model:  # type: ignore
                    raise

                # Only use PostgreSQL-specific query for PostgreSQL
                if connection.vendor == "postgresql":
                    cursor.execute(
                        "SELECT reltuples FROM pg_class WHERE relname = %s",
                        [self.object_list.query.model._meta.db_table],  # type: ignore
                    )
                    res = cursor.fetchone()
                    if res:
                        return int(res[0])

                # For non-PostgreSQL databases, return a reasonable estimate
                # or fall back to the actual count (which might be slow)
                logger.warning(
                    f"Count operation failed for {self.object_list.query.model._meta.db_table}. "
                    f"Database vendor: {connection.vendor}. Falling back to actual count."
                )
                return super().count

autoreg_disabled()

Determine whether auto admin registration should be disabled for the current context.

Disables for: - Explicit project setting/env flag - Migrations (makemigrations/migrate) and other configured skip commands - When django.contrib.admin is not installed and configured to skip

Source code in src/django_admin_magic/utils.py
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
def autoreg_disabled() -> bool:
    """
    Determine whether auto admin registration should be disabled for the current context.

    Disables for:
    - Explicit project setting/env flag
    - Migrations (makemigrations/migrate) and other configured skip commands
    - When django.contrib.admin is not installed and configured to skip
    """
    # Explicit setting toggle
    try:
        if getattr(app_settings, "DISABLED", False):
            return True
    except Exception:
        # If settings are not ready, continue with other checks
        pass

    # Environment variable overrides
    if os.environ.get("DJANGO_ADMIN_MAGIC_DISABLE") == "1" or os.environ.get("AUTO_ADMIN_DISABLE") == "1":
        return True

    # Skip specific management commands
    cmd = sys.argv[1] if len(sys.argv) > 1 else ""
    skip_commands = set(getattr(app_settings, "SKIP_COMMANDS", []))
    if cmd in skip_commands:
        return True

    # Skip when admin not installed (based on config)
    try:
        if getattr(app_settings, "SKIP_IF_ADMIN_NOT_INSTALLED", True) and not django_apps.is_installed(
            "django.contrib.admin"
        ):
            return True
    except Exception:
        # apps registry may not be ready yet; fallback to settings
        try:
            from django.conf import settings as dj_settings

            if getattr(app_settings, "SKIP_IF_ADMIN_NOT_INSTALLED", True) and (
                not hasattr(dj_settings, "INSTALLED_APPS") or "django.contrib.admin" not in dj_settings.INSTALLED_APPS
            ):
                return True
        except Exception:
            pass

    return False

create_auto_admin_registrar(app_label=None)

Create an auto admin registrar for the current app.

This function is designed to be used in admin.py files to automatically register all models in the current app with the admin site.

Parameters:

Name Type Description Default
app_label str

The app label to register. If None, will be automatically determined from the current package.

None

Returns:

Name Type Description
AdminModelRegistrar

The registrar instance

Example

In your app's admin.py file:

from django_admin_magic.utils import create_auto_admin_registrar

registrar = create_auto_admin_registrar()

All models in this app are now registered with the admin site

Source code in src/django_admin_magic/utils.py
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
def create_auto_admin_registrar(app_label: str = None):
    """
    Create an auto admin registrar for the current app.

    This function is designed to be used in admin.py files to automatically
    register all models in the current app with the admin site.

    Args:
        app_label (str, optional): The app label to register. If None, will be
                                 automatically determined from the current package.

    Returns:
        AdminModelRegistrar: The registrar instance

    Example:
        # In your app's admin.py file:
        from django_admin_magic.utils import create_auto_admin_registrar

        registrar = create_auto_admin_registrar()
        # All models in this app are now registered with the admin site

    """
    from .registrar import AdminModelRegistrar, NoOpRegistrar

    if autoreg_disabled():
        return NoOpRegistrar()

    inferred_app_label = app_label
    if inferred_app_label is None:
        inferred_app_label = infer_current_app_label()

    if not inferred_app_label:
        logger.debug("Unable to infer app label for auto admin registrar; skipping registration.")
        return NoOpRegistrar()

    return AdminModelRegistrar.register_app(inferred_app_label)

create_auto_admin_registrar_for_all_apps()

Create an auto admin registrar that discovers and registers all apps.

Returns:

Name Type Description
AdminModelRegistrar

The registrar instance

Example

In your admin.py file:

from django_admin_magic.utils import create_auto_admin_registrar_for_all_apps

registrar = create_auto_admin_registrar_for_all_apps()

Source code in src/django_admin_magic/utils.py
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
def create_auto_admin_registrar_for_all_apps():
    """
    Create an auto admin registrar that discovers and registers all apps.

    Returns:
        AdminModelRegistrar: The registrar instance

    Example:
        # In your admin.py file:
        from django_admin_magic.utils import create_auto_admin_registrar_for_all_apps

        registrar = create_auto_admin_registrar_for_all_apps()

    """
    from .registrar import AdminModelRegistrar, NoOpRegistrar

    if autoreg_disabled():
        return NoOpRegistrar()

    return AdminModelRegistrar.register_all_discovered_apps()

create_auto_admin_registrar_for_apps(app_labels)

Create an auto admin registrar for multiple apps.

Parameters:

Name Type Description Default
app_labels list[str]

List of app labels to register

required

Returns:

Name Type Description
AdminModelRegistrar

The registrar instance

Example

In your admin.py file:

from django_admin_magic.utils import create_auto_admin_registrar_for_apps

registrar = create_auto_admin_registrar_for_apps(['myapp1', 'myapp2'])

Source code in src/django_admin_magic/utils.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
def create_auto_admin_registrar_for_apps(app_labels: list[str]):
    """
    Create an auto admin registrar for multiple apps.

    Args:
        app_labels (list[str]): List of app labels to register

    Returns:
        AdminModelRegistrar: The registrar instance

    Example:
        # In your admin.py file:
        from django_admin_magic.utils import create_auto_admin_registrar_for_apps

        registrar = create_auto_admin_registrar_for_apps(['myapp1', 'myapp2'])

    """
    from .registrar import AdminModelRegistrar, NoOpRegistrar

    if autoreg_disabled():
        return NoOpRegistrar()

    return AdminModelRegistrar.register_apps(app_labels)

get_all_child_classes(cls)

Recursively retrieves all child classes of a given class.

Parameters:

Name Type Description Default
cls Type

The class to inspect for child classes.

required

Returns:

Type Description
list[type]

List[Type]: A list of all direct and indirect subclasses of the given class.

Source code in src/django_admin_magic/utils.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def get_all_child_classes(cls: type) -> list[type]:
    """
    Recursively retrieves all child classes of a given class.

    Args:
        cls (Type): The class to inspect for child classes.

    Returns:
        List[Type]: A list of all direct and indirect subclasses of the given class.

    """
    child_classes = cls.__subclasses__()  # Get direct subclasses
    all_children = child_classes[:]  # Start with direct subclasses

    for child in child_classes:
        # Recursively add subclasses of each child
        all_children.extend(get_all_child_classes(child))

    return all_children

infer_current_app_label()

Infer the current Django app label from the caller's module using Django's app registry.

This is safer than relying on 'package' which may not be set during certain management commands (e.g., migrations) or when imported in unusual contexts.

Source code in src/django_admin_magic/utils.py
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
def infer_current_app_label() -> str | None:
    """
    Infer the current Django app label from the caller's module using Django's app registry.

    This is safer than relying on '__package__' which may not be set during certain
    management commands (e.g., migrations) or when imported in unusual contexts.
    """
    module_name = None
    module = None
    try:
        current_frame = inspect.currentframe()
        if current_frame is not None and current_frame.f_back is not None:
            module = inspect.getmodule(current_frame.f_back)
            if module is not None:
                module_name = module.__name__
    finally:
        # Help GC with frame references
        del current_frame

    if module_name:
        try:
            app_config = django_apps.get_containing_app_config(module_name)
            if app_config is not None:
                return app_config.label
        except Exception:
            pass

        # Fallback: try prefix match with installed app configs
        for config in django_apps.get_app_configs():
            if module_name.startswith(config.name):
                return config.label

    # Secondary fallback using module.__package__ if available
    try:
        if module is not None and getattr(module, "__package__", None):
            pkg = module.__package__
            for config in django_apps.get_app_configs():
                if pkg.startswith(config.name):
                    return config.label
    except Exception:
        pass

    return None

is_linkify_function(field)

Check if a field is a linkify function (either linkify or linkify_gfk).

Parameters:

Name Type Description Default
field

The field to check

required

Returns:

Name Type Description
bool

True if the field is a linkify function, False otherwise

Source code in src/django_admin_magic/utils.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def is_linkify_function(field):
    """
    Check if a field is a linkify function (either linkify or linkify_gfk).

    Args:
        field: The field to check

    Returns:
        bool: True if the field is a linkify function, False otherwise

    """
    if not callable(field):
        return False

    # Check if it's a linkify function by examining its function name or attributes
    # Both linkify and linkify_gfk functions have specific patterns
    func_name = getattr(field, "__name__", "")

    # Check for the specific function names used in linkify functions
    if func_name in ("_linkify", "_linkify_gfk", "_linkify_m2m"):
        return True

    # Additional check: look for the short_description attribute which is set on linkify functions
    if hasattr(field, "short_description") and hasattr(field, "admin_order_field"):
        return True

    # Check if the function was created by our linkify functions by examining the closure
    try:
        # Get the function's code object to check if it contains linkify-specific patterns
        if hasattr(field, "__code__"):
            # This is a more robust way to detect our linkify functions
            # We can check if the function has the expected attributes
            if hasattr(field, "short_description") and hasattr(field, "admin_order_field"):
                return True
    except (AttributeError, TypeError):
        pass

    return False

linkify(field_name)

Converts a foreign key value into clickable links.

If field_name is 'parent', link text will be str(obj.parent) Link will be admin url for the admin url for obj.parent.id:change

Source code in src/django_admin_magic/utils.py
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
def linkify(field_name):
    """
    Converts a foreign key value into clickable links.

    If field_name is 'parent', link text will be str(obj.parent)
    Link will be admin url for the admin url for obj.parent.id:change
    """

    def _linkify(obj):
        linked_obj = getattr(obj, field_name)
        if linked_obj is None:
            return "-"
        app_label = linked_obj._meta.app_label
        model_name = linked_obj._meta.model_name
        view_name = f"admin:{app_label}_{model_name}_change"
        # Add try-except block for cases where reverse fails (e.g., model not in admin)
        try:
            link_url = reverse(view_name, args=[linked_obj.pk])
            return format_html('<a href="{}">{}</a>', link_url, linked_obj)
        except Exception:
            # Fallback: Display object representation without a link
            logger.debug(f"Could not reverse admin URL for {app_label}.{model_name} with pk {linked_obj.pk}")
            return str(linked_obj)

    desc = field_name.replace("_", " ").title()
    try:
        _linkify.short_description = desc  # Sets column name
        _linkify.admin_order_field = field_name  # Allow sorting by this field
    except AttributeError:
        logger.warning(f"Could not set admin attributes on linkify function for {field_name}")
    return _linkify

linkify_gfk(field_name)

Converts a GenericForeignKey value into clickable links in the admin.

Parameters:

Name Type Description Default
field_name str

The name of the GenericForeignKey field on the model.

required

Returns:

Name Type Description
Callable

A function suitable for Django admin's list_display.

Source code in src/django_admin_magic/utils.py
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
def linkify_gfk(field_name):
    """
    Converts a GenericForeignKey value into clickable links in the admin.

    Args:
        field_name (str): The name of the GenericForeignKey field on the model.

    Returns:
        Callable: A function suitable for Django admin's list_display.

    """

    def _linkify_gfk(obj):
        linked_obj = getattr(obj, field_name)
        if linked_obj is None:
            return "-"

        # GFK target object could be anything, so we need its ContentType info
        try:
            # Ensure the linked object has a _meta attribute and pk
            if not hasattr(linked_obj, "_meta") or not hasattr(linked_obj, "pk") or linked_obj.pk is None:
                # Fallback if it's not a standard model instance or has no pk
                return str(linked_obj)

            # Get metadata directly from the linked object instance
            obj_id = linked_obj.pk
            app_label = linked_obj._meta.app_label
            model_name = linked_obj._meta.model_name

            view_name = f"admin:{app_label}_{model_name}_change"
            link_url = reverse(view_name, args=[obj_id])
            # Use a simplified representation, perhaps just the model name and PK
            display_text = f"{model_name.capitalize()} {obj_id}"
            return format_html('<a href="{}">{}</a>', link_url, display_text)
        except Exception as e:
            # Fallback if URL cannot be reversed or any other error occurs
            logger.debug(f"Could not reverse admin URL for GFK target {linked_obj}: {e}")
            return str(linked_obj)  # Display the object's string representation

    # Use the GFK field name for description and ordering
    desc = field_name.replace("_", " ").title()
    try:
        _linkify_gfk.short_description = desc
        _linkify_gfk.admin_order_field = field_name  # Attempt sorting - may depend on GFK setup
    except AttributeError:
        logger.warning(f"Could not set admin attributes on linkify_gfk function for {field_name}")
    return _linkify_gfk

linkify_m2m(field_name)

Render a ManyToMany field as a comma-separated list of linkified related objects.

  • Respects configured limit via app settings (M2M_LIST_MAX_ITEMS)
  • Uses configured display attribute (M2M_LIST_DISPLAY_ATTR), defaulting to str
  • Falls back gracefully if admin URL cannot be reversed
  • Clips with an ellipsis when more than the configured limit
Source code in src/django_admin_magic/utils.py
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
def linkify_m2m(field_name):
    """
    Render a ManyToMany field as a comma-separated list of linkified related objects.

    - Respects configured limit via app settings (M2M_LIST_MAX_ITEMS)
    - Uses configured display attribute (M2M_LIST_DISPLAY_ATTR), defaulting to __str__
    - Falls back gracefully if admin URL cannot be reversed
    - Clips with an ellipsis when more than the configured limit
    """

    def _resolve_display_text(related_obj):
        display_attr = getattr(app_settings, "M2M_LIST_DISPLAY_ATTR", "__str__")
        if display_attr == "__str__":
            return str(related_obj)
        # Support dotted path attributes, e.g., "profile.name"
        try:
            value = related_obj
            for part in str(display_attr).split("."):
                value = getattr(value, part)
            return str(value)
        except Exception:
            return str(related_obj)

    def _maybe_link(related_obj, text):
        try:
            if not hasattr(related_obj, "_meta") or getattr(related_obj, "pk", None) is None:
                return text
            app_label = related_obj._meta.app_label
            model_name = related_obj._meta.model_name
            view_name = f"admin:{app_label}_{model_name}_change"
            link_url = reverse(view_name, args=[related_obj.pk])
            return format_html('<a href="{}">{}</a>', link_url, text)
        except Exception:
            return text

    def _linkify_m2m(obj):
        try:
            manager = getattr(obj, field_name)
        except Exception:
            return "-"

        try:
            queryset = manager.all()
        except Exception:
            return "-"

        max_items = getattr(app_settings, "M2M_LIST_MAX_ITEMS", 10) or 10
        items = list(queryset[: max_items + 1])
        has_more = len(items) > max_items
        items = items[:max_items]

        if not items:
            return "-"

        display_nodes = [_maybe_link(related, _resolve_display_text(related)) for related in items]

        # Join with comma+space safely
        rendered = (
            display_nodes[0]
            if len(display_nodes) == 1
            else format_html_join(
                ", ",
                "{}",
                ((node,) for node in display_nodes),
            )
        )

        if has_more:
            rendered = format_html("{}{}", rendered, " ...")
        return rendered

    desc = field_name.replace("_", " ").title()
    try:
        _linkify_m2m.short_description = desc
        # M2M not sortable by default
    except AttributeError:
        logger.warning(f"Could not set admin attributes on linkify_m2m for {field_name}")
    return _linkify_m2m

reorder_list_display_to_avoid_linkify_first(list_display)

Reorder list_display to ensure the first field is not a linkify function. Moves all leading linkify functions after the first non-linkify field.

Source code in src/django_admin_magic/utils.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def reorder_list_display_to_avoid_linkify_first(list_display):
    """
    Reorder list_display to ensure the first field is not a linkify function.
    Moves all leading linkify functions after the first non-linkify field.
    """
    if not list_display or len(list_display) < 2:
        return list_display

    # Find the index of the first non-linkify field
    first_non_linkify_index = None
    for i, field in enumerate(list_display):
        if not is_linkify_function(field):
            first_non_linkify_index = i
            break

    if first_non_linkify_index is None or first_non_linkify_index == 0:
        # No non-linkify field found, or already starts with non-linkify
        return list_display

    # Move all leading linkify functions (from start up to first non-linkify) after the first non-linkify
    leading_linkify = list_display[:first_non_linkify_index]
    rest = list_display[first_non_linkify_index:]
    reordered = [rest[0]] + leading_linkify + rest[1:]
    logger.debug(f"Reordered list_display to avoid linkify field being first: {reordered}")
    return reordered