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

charettes / django-polymodels / 14162717205

31 Mar 2025 02:19AM UTC coverage: 99.112%. Remained the same
14162717205

push

github

charettes
Drop support for Django < 4.2 and Python < 3.9.

128 of 137 branches covered (93.43%)

335 of 338 relevant lines covered (99.11%)

4.96 hits per line

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

99.05
/polymodels/fields.py
1
from django import forms
5✔
2
from django.apps import apps
5✔
3
from django.core import checks
5✔
4
from django.db.models import ForeignKey, Q
5✔
5
from django.db.models.fields import NOT_PROVIDED
5✔
6
from django.db.models.fields.related import (
5✔
7
    RelatedField,
8
    lazy_related_operation,
9
)
10
from django.utils.deconstruct import deconstructible
5✔
11
from django.utils.functional import LazyObject, empty
5✔
12
from django.utils.translation import gettext_lazy as _
5✔
13

14
from .models import BasePolymorphicModel
5✔
15
from .utils import get_content_type
5✔
16

17

18
class LimitChoicesToSubclasses:
5✔
19
    def __init__(self, field, limit_choices_to):
5✔
20
        self.field = field
5✔
21
        self.limit_choices_to = limit_choices_to
5✔
22

23
    @property
5✔
24
    def value(self):
5✔
25
        subclasses_lookup = self.field.polymorphic_type.subclasses_lookup("pk")
5✔
26
        limit_choices_to = self.limit_choices_to
5✔
27
        if limit_choices_to is None:
5✔
28
            limit_choices_to = subclasses_lookup.copy()
5✔
29
        elif isinstance(limit_choices_to, dict):
5✔
30
            limit_choices_to = dict(limit_choices_to, **subclasses_lookup)
5✔
31
        elif isinstance(limit_choices_to, Q):
5!
32
            limit_choices_to = limit_choices_to & Q(**subclasses_lookup)
5✔
33
        self.__dict__["value"] = limit_choices_to
5✔
34
        return limit_choices_to
5✔
35

36
    def __call__(self):
5✔
37
        return self.value
5✔
38

39

40
class LazyPolymorphicTypeQueryset(LazyObject):
5✔
41
    def __init__(self, remote_field, db):
5✔
42
        super().__init__()
5✔
43
        self.__dict__.update(remote_field=remote_field, db=db)
5✔
44

45
    def _setup(self):
5✔
46
        remote_field = self.__dict__.get("remote_field")
5✔
47
        db = self.__dict__.get("db")
5✔
48
        self._wrapped = remote_field.model._default_manager.using(db).complex_filter(
5✔
49
            remote_field.limit_choices_to()
50
        )
51

52
    def __getattr__(self, attr):
5✔
53
        # ModelChoiceField._set_queryset(queryset) calls queryset.all() on
54
        # Django 2.1+ in order to clear possible cached results.
55
        # Since no results might have been cached before _setup() is called
56
        # it's safe to keep deferring until something else is accessed.
57
        if attr == "all" and self._wrapped is empty:
5!
58
            return lambda: self
5✔
59
        return super().__getattr__(attr)
×
60

61

62
@deconstructible
5✔
63
class ContentTypeReference:
5✔
64
    def __init__(self, app_label, model_name):
5✔
65
        self.app_label = app_label
5✔
66
        self.model_name = model_name
5✔
67

68
    def __eq__(self, other):
5✔
69
        return isinstance(other, self.__class__) and (
5✔
70
            (self.app_label, self.model_name) == (other.app_label, other.model_name)
71
        )
72

73
    def __call__(self):
5✔
74
        model = apps.get_model(self.app_label, self.model_name)
5✔
75
        return get_content_type(model).pk
5✔
76

77
    def __repr__(self):
5✔
78
        return "ContentTypeReference(%r, %r)" % (self.app_label, self.model_name)
5✔
79

80

81
class PolymorphicTypeField(ForeignKey):
5✔
82
    default_error_messages = {
5✔
83
        "invalid": _("Specified model is not a subclass of %(model)s.")
84
    }
85
    description = _("Content type of a subclass of %(type)s")
5✔
86
    default_kwargs = {
5✔
87
        "to": "contenttypes.contenttype",
88
        "related_name": "+",
89
    }
90

91
    def __init__(self, polymorphic_type, *args, **kwargs):
5✔
92
        self.polymorphic_type = polymorphic_type
5✔
93
        self.overriden_default = False
5✔
94
        for kwarg, value in self.default_kwargs.items():
5✔
95
            kwargs.setdefault(kwarg, value)
5✔
96
        kwargs["limit_choices_to"] = LimitChoicesToSubclasses(
5✔
97
            self, kwargs.pop("limit_choices_to", None)
98
        )
99
        super().__init__(*args, **kwargs)
5✔
100

101
    def contribute_to_class(self, cls, name):
5✔
102
        super().contribute_to_class(cls, name)
5✔
103
        polymorphic_type = self.polymorphic_type
5✔
104
        if isinstance(polymorphic_type, str) or polymorphic_type._meta.pk is None:
5✔
105

106
            def resolve_polymorphic_type(model, related_model, field):
5✔
107
                field.do_polymorphic_type(related_model)
5✔
108

109
            lazy_related_operation(
5✔
110
                resolve_polymorphic_type, cls, polymorphic_type, field=self
111
            )
112
        else:
113
            self.do_polymorphic_type(polymorphic_type)
5✔
114

115
    def do_polymorphic_type(self, polymorphic_type):
5✔
116
        if self.default is NOT_PROVIDED and not self.null:
5✔
117
            opts = polymorphic_type._meta
5✔
118
            self.default = ContentTypeReference(opts.app_label, opts.model_name)
5✔
119
            self.overriden_default = True
5✔
120
        self.polymorphic_type = polymorphic_type
5✔
121
        self.type = polymorphic_type.__name__
5✔
122
        self.error_messages["invalid"] = (
5✔
123
            "Specified content type is not of a subclass of %s."
124
            % polymorphic_type._meta.object_name
125
        )
126

127
    def check(self, **kwargs):
5✔
128
        errors = super().check(**kwargs)
5✔
129
        if isinstance(self.polymorphic_type, str):
5✔
130
            errors.append(
5✔
131
                checks.Error(
132
                    (
133
                        "Field defines a relation with model '%s', which "
134
                        "is either not installed, or is abstract."
135
                    )
136
                    % self.polymorphic_type,
137
                    id="fields.E300",
138
                )
139
            )
140
        elif not issubclass(self.polymorphic_type, BasePolymorphicModel):
5✔
141
            errors.append(
5✔
142
                checks.Error(
143
                    "The %s type is not a subclass of BasePolymorphicModel."
144
                    % self.polymorphic_type.__name__,
145
                    id="polymodels.E004",
146
                )
147
            )
148
        return errors
5✔
149

150
    def formfield(self, **kwargs):
5✔
151
        db = kwargs.pop("using", None)
5✔
152
        if isinstance(self.polymorphic_type, str):
5✔
153
            raise ValueError(
5✔
154
                "Cannot create form field for %r yet, because its related model %r has not been loaded yet"
155
                % (self.name, self.polymorphic_type)
156
            )
157
        defaults = {
5✔
158
            "form_class": forms.ModelChoiceField,
159
            "queryset": LazyPolymorphicTypeQueryset(self.remote_field, db),
160
            "to_field_name": self.remote_field.field_name,
161
        }
162
        defaults.update(kwargs)
5✔
163
        return super(RelatedField, self).formfield(**defaults)
5✔
164

165
    def deconstruct(self):
5✔
166
        name, path, args, kwargs = super().deconstruct()
5✔
167
        opts = getattr(self.polymorphic_type, "_meta", None)
5✔
168
        kwargs["polymorphic_type"] = (
5✔
169
            "%s.%s" % (opts.app_label, opts.object_name)
170
            if opts
171
            else self.polymorphic_type
172
        )
173
        for kwarg, value in list(kwargs.items()):
5✔
174
            if self.default_kwargs.get(kwarg) == value:
5✔
175
                kwargs.pop(kwarg)
5✔
176
        if self.overriden_default:
5✔
177
            kwargs.pop("default")
5✔
178
        kwargs.pop("limit_choices_to", None)
5✔
179
        return name, path, args, kwargs
5✔
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