• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

iplweb / bpp / 18634744198

19 Oct 2025 07:00PM UTC coverage: 31.618% (-29.9%) from 61.514%
18634744198

push

github

mpasternak
Merge branch 'release/v202510.1270'

657 of 9430 branches covered (6.97%)

Branch coverage included in aggregate %.

229 of 523 new or added lines in 42 files covered. (43.79%)

11303 existing lines in 316 files now uncovered.

14765 of 39346 relevant lines covered (37.53%)

0.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

30.89
src/dynamic_columns/models.py
1
"""Models for in-database representation of dynamic admin columns configuration."""
2

3
import re
1✔
4

5
from django.conf import settings
1✔
6
from django.contrib.admin import ModelAdmin as DjangoModelAdmin
1✔
7
from django.contrib.contenttypes.models import ContentType
1✔
8
from django.db import models
1✔
9
from django.db.models import Max
1✔
10
from django.utils.datastructures import OrderedSet
1✔
11
from django.utils.functional import cached_property
1✔
12
from django.utils.translation import gettext_lazy as _
1✔
13

14
from dynamic_columns.exceptions import CodeAccessNotAllowed
1✔
15
from dynamic_columns.util import qual, str_to_class
1✔
16

17

18
class ModelAdminManager(models.Manager):
1✔
19
    def db_repr(self, model_admin: DjangoModelAdmin) -> "ModelAdmin":
1✔
20
        """
21
        Get database representation of a Django's ModelAdmin -- return
22
        a class ``dynamic_columns.models.ModelAdmin``. This class basically
23
        consists of 2 elements:
24

25
        * full qualified class name of ``model_admin``,
26
        * django.contrib.contenttype.models.ContentType reference of model registered
27
          for that model_admin instance.
28

29
        :param model_admin: ``django.contrib.admin.admin.ModelAdmin`` instance.
30

31
        :return: ``dynamic_columns.models.ModelAdmin`` instance, created fresh or from
32
        database.
33
        """
UNCOV
34
        cname = qual(model_admin.__class__)
×
35

UNCOV
36
        found = False
×
UNCOV
37
        for path in getattr(settings, "DYNAMIC_COLUMNS_ALLOWED_IMPORT_PATHS", []):
×
UNCOV
38
            if cname.startswith(path):
×
UNCOV
39
                found = True
×
40

UNCOV
41
        if not found:
×
42
            raise CodeAccessNotAllowed(
×
43
                f"Please add {cname} to your project's settings.py if you want to "
44
                f"use DynamicColumnsMixin for your {model_admin} classes -- "
45
                f"add it to a list ``DYNAMIC_COLUMNS_ALLOWED_IMPORT_PATHS``. "
46
            )
47

UNCOV
48
        return self.get_or_create(
×
49
            class_name=cname,
50
            model_ref=ContentType.objects.get_for_model(model_admin.model),
51
        )[0]
52

53
    def enable(self, model_admin: DjangoModelAdmin) -> "ModelAdmin":
1✔
54
        """
55
        Enable dynamic columns -- create an in-database representation
56
        of Django's ModelAdmin instance, create in-database representation
57
        of columns: enabled by default, which can be moved or disabled by end-user
58
        (``model_admin.list_display_default``) and disabled by default, which
59
        can be later enabled by end-user (``model_admin.list_display_allowed``),
60
        keeping forbidden columns (``model_admin.list_display_forbidden``) away.
61

62
        :returns: dynamic_columns.models.ModelAdmin
63
        """
UNCOV
64
        obj = self.db_repr(model_admin)
×
65

66
        # If there is a ``list_display`` setting on ``model_admin``, treat it
67
        # as ``list_display_default``.
68
        #
69
        # Unless it was not changed from the default settings, which at
70
        #  the time of this writing, contains only "__str__".
71
        #
72
        # In this case, if ``list_display_always`` is declared, we will skip it,
73
        # but it is not declared - we will not.
74
        #
75
        # This way we can still support the "old" ``list_display`` parameter, but avoid
76
        # displaying double columns in case it is not actually being used, in favor
77
        # of the new ``list_display_default`` and ``list_display_always``.
78
        #
79

UNCOV
80
        list_display = getattr(model_admin, "list_display", [])
×
UNCOV
81
        if list_display == DjangoModelAdmin.list_display:
×
82
            # Looks like ``list_display`` was not changed from default setting.
83
            # Is there ``list_display_always`` declared? If yes, empty
84
            # ``list_display`` variable
UNCOV
85
            if getattr(model_admin, "list_display_always", []):
×
UNCOV
86
                list_display = []
×
87

88
        # Sources of column names. ``list_display``, handled in a way described
89
        # above, ``list_display_default`` -- columns visible by default,
90
        # and ``list_display_allowed`` -- columns not visible by default, but
91
        # can be enabled later:
92

UNCOV
93
        column_sources = [
×
94
            # (column_source, column_enabled_defualt_value)
95
            (list_display, True),
96
            (getattr(model_admin, "list_display_default", []), True),
97
            (getattr(model_admin, "list_display_allowed", []), False),
98
        ]
99

100
        # Did you know, that instead of typing all the column names by yourself,
101
        # you could use a magic string "__all__"? This way you can get all the
102
        # columns in the model. But, could it be too broad? It could be. This is
103
        # why there is a setting ``list_display_forbidden`` which can exclude
104
        # some columns on a per-model basis, and there is also a configuration setting
105
        # ``DYNAMIC_COLUMNS_FORBIDDEN_COLUMN_NAMES``. Both those variables can
106
        # contain a list of regex that will be matched against column names.
107

UNCOV
108
        forbidden_columns_patterns = (
×
109
            # ``list_display_always`` are forbidden in the database - they are declared
110
            # in the code, they cannot be moved:
111
            getattr(model_admin, "list_display_forbidden", [])
112
            + getattr(settings, "DYNAMIC_COLUMNS_FORBIDDEN_COLUMN_NAMES", [])
113
        )
114

UNCOV
115
        list_display_always = getattr(model_admin, "list_display_always", [])
×
116

UNCOV
117
        def column_allowed(field_name):
×
UNCOV
118
            if field_name in list_display_always:
×
UNCOV
119
                return False
×
120

UNCOV
121
            for elem in forbidden_columns_patterns:
×
UNCOV
122
                if re.match(elem, field_name):
×
UNCOV
123
                    return False
×
124

UNCOV
125
            return True
×
126

UNCOV
127
        all_columns = set()
×
128

UNCOV
129
        db_max = ModelAdminColumn.objects.all().aggregate(max_cnt=Max("ordering"))
×
UNCOV
130
        cnt = (db_max["max_cnt"] or 0) + 1
×
131

UNCOV
132
        for column_source, default_value in column_sources:
×
UNCOV
133
            if column_source == "__all__":
×
134
                # Discover "all" columns
UNCOV
135
                columns = [
×
136
                    field.name
137
                    for field in model_admin.model._meta.fields
138
                    if column_allowed(field.name)
139
                ]
140
            else:
141
                # Got an exact list of column names in the source code:
UNCOV
142
                columns = column_source
×
143

UNCOV
144
            for column in [col for col in columns if column_allowed(col)]:
×
UNCOV
145
                all_columns.add(column)
×
UNCOV
146
                cnt += 1
×
UNCOV
147
                obj.modeladmincolumn_set.get_or_create(
×
148
                    col_name=column,
149
                    defaults={"ordering": cnt, "enabled": default_value},
150
                )
151

152
        # Remove stale columns from the database
UNCOV
153
        obj.modeladmincolumn_set.exclude(col_name__in=all_columns).delete()
×
154

UNCOV
155
        return obj
×
156

157

158
class ModelAdmin(models.Model):
1✔
159
    """
160
    In-database representation of a Django's ModelAdmin.
161

162
    Consists of 2 parameters actually.
163

164
    ``class_name`` is the class name of a Django's ModelAdmin,
165

166
    ``model_ref`` is a django.contrib.contenttypes.models.ContentType reference
167
    to a content type that is registered for that admin.
168

169
    In Django you can register a single ModelAdmin class for different models.
170
    This is no different here. You can create ModelAdmins with the same ``class_name``
171
    but for different ``model_ref``s and have different columns visible for every
172
    single one.
173
    """
174

175
    class_name = models.TextField()
1✔
176

177
    model_ref = models.ForeignKey(ContentType, on_delete=models.CASCADE)
1✔
178

179
    objects = ModelAdminManager()
1✔
180

181
    class Meta:
1✔
182
        unique_together = [("class_name", "model_ref")]
1✔
183
        ordering = ("class_name",)
1✔
184
        verbose_name = _("Model admin")
1✔
185
        verbose_name_plural = _("Model admins")
1✔
186

187
    def __str__(self):
1✔
188
        return self.class_name
×
189

190
    @cached_property
1✔
191
    def class_ref(self):
1✔
192
        """
193
        This function returns a reference to Django's ModelAdmin class, preferably
194
        the one from your project's code. But as the database could get modified
195
        in an unsafe manner, we will check if the module path for that ModelAdmin
196
        is defined in settings.py's ``DYNAMIC_COLUMNS_ALLOWED_IMPORT_PATHS``.
197
        """
198
        found = False
×
199
        for path in getattr(settings, "DYNAMIC_COLUMNS_ALLOWED_IMPORT_PATHS", []):
×
200
            if self.class_name.startswith(path):
×
201
                found = True
×
202

203
        if not found:
×
204
            raise CodeAccessNotAllowed(
×
205
                f"Path {self.class_name} not found in settings.DYNAMIC_COLUMNS_ALLOWED_IMPORT_PATHS"
206
            )
207

208
        return str_to_class(self.class_name)
×
209

210
    def get_list_display(self, model_admin, request):
1✔
211
        """This function returns the list of columns, in a specific order
212
        that should be displayed in a Django ModelAdmin's change list.
213
        """
UNCOV
214
        ret = OrderedSet()
×
215

UNCOV
216
        column_sources = [
×
217
            getattr(model_admin, "list_display_always", []),
218
            ModelAdmin.objects.db_repr(model_admin)
219
            .modeladmincolumn_set.filter(enabled=True)
220
            .values_list("col_name", flat=True),
221
        ]
222

UNCOV
223
        for column_source in column_sources:
×
UNCOV
224
            [ret.add(c) for c in column_source]
×
225

UNCOV
226
        return ret
×
227

228

229
class ModelAdminColumn(models.Model):
1✔
230
    """This is an in-database column representation of a given
231
    in-database ModelAdmin instance.
232

233
    It can be enabled or disabled -- visible or invisible.
234

235
    It also has an order in which it is displayed.
236
    """
237

238
    parent = models.ForeignKey(
1✔
239
        ModelAdmin, on_delete=models.CASCADE, verbose_name=_("Parent")
240
    )
241

242
    col_name = models.CharField(max_length=255, verbose_name=_("Column name"))
1✔
243

244
    enabled = models.BooleanField(default=True, verbose_name=_("Enabled"))
1✔
245
    ordering = models.PositiveSmallIntegerField(verbose_name=_("Ordering"))
1✔
246

247
    def __str__(self):
1✔
UNCOV
248
        ret = _("Column") + f' "{self.col_name}"'
×
249

UNCOV
250
        if self.parent_id:
×
UNCOV
251
            ret += _(" of model ") + f'"{self.parent.class_name}"'
×
252

UNCOV
253
        return ret
×
254

255
    class Meta:
1✔
256
        unique_together = [("parent", "col_name")]
1✔
257
        ordering = ("parent", "ordering")
1✔
258
        verbose_name = _("Model admin column")
1✔
259
        verbose_name_plural = _("Model admin columns")
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc