File: fields.py

package info (click to toggle)
django-polymodels 1.8.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 208 kB
  • sloc: python: 1,373; makefile: 6; sh: 5
file content (179 lines) | stat: -rw-r--r-- 6,684 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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
91
92
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
118
119
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
from django import forms
from django.apps import apps
from django.core import checks
from django.db.models import ForeignKey, Q
from django.db.models.fields import NOT_PROVIDED
from django.db.models.fields.related import (
    RelatedField,
    lazy_related_operation,
)
from django.utils.deconstruct import deconstructible
from django.utils.functional import LazyObject, empty
from django.utils.translation import gettext_lazy as _

from .models import BasePolymorphicModel
from .utils import get_content_type


class LimitChoicesToSubclasses:
    def __init__(self, field, limit_choices_to):
        self.field = field
        self.limit_choices_to = limit_choices_to

    @property
    def value(self):
        subclasses_lookup = self.field.polymorphic_type.subclasses_lookup("pk")
        limit_choices_to = self.limit_choices_to
        if limit_choices_to is None:
            limit_choices_to = subclasses_lookup.copy()
        elif isinstance(limit_choices_to, dict):
            limit_choices_to = dict(limit_choices_to, **subclasses_lookup)
        elif isinstance(limit_choices_to, Q):
            limit_choices_to = limit_choices_to & Q(**subclasses_lookup)
        self.__dict__["value"] = limit_choices_to
        return limit_choices_to

    def __call__(self):
        return self.value


class LazyPolymorphicTypeQueryset(LazyObject):
    def __init__(self, remote_field, db):
        super().__init__()
        self.__dict__.update(remote_field=remote_field, db=db)

    def _setup(self):
        remote_field = self.__dict__.get("remote_field")
        db = self.__dict__.get("db")
        self._wrapped = remote_field.model._default_manager.using(db).complex_filter(
            remote_field.limit_choices_to()
        )

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


@deconstructible
class ContentTypeReference:
    def __init__(self, app_label, model_name):
        self.app_label = app_label
        self.model_name = model_name

    def __eq__(self, other):
        return isinstance(other, self.__class__) and (
            (self.app_label, self.model_name) == (other.app_label, other.model_name)
        )

    def __call__(self):
        model = apps.get_model(self.app_label, self.model_name)
        return get_content_type(model).pk

    def __repr__(self):
        return "ContentTypeReference(%r, %r)" % (self.app_label, self.model_name)


class PolymorphicTypeField(ForeignKey):
    default_error_messages = {
        "invalid": _("Specified model is not a subclass of %(model)s.")
    }
    description = _("Content type of a subclass of %(type)s")
    default_kwargs = {
        "to": "contenttypes.contenttype",
        "related_name": "+",
    }

    def __init__(self, polymorphic_type, *args, **kwargs):
        self.polymorphic_type = polymorphic_type
        self.overriden_default = False
        for kwarg, value in self.default_kwargs.items():
            kwargs.setdefault(kwarg, value)
        kwargs["limit_choices_to"] = LimitChoicesToSubclasses(
            self, kwargs.pop("limit_choices_to", None)
        )
        super().__init__(*args, **kwargs)

    def contribute_to_class(self, cls, name):
        super().contribute_to_class(cls, name)
        polymorphic_type = self.polymorphic_type
        if isinstance(polymorphic_type, str) or polymorphic_type._meta.pk is None:

            def resolve_polymorphic_type(model, related_model, field):
                field.do_polymorphic_type(related_model)

            lazy_related_operation(
                resolve_polymorphic_type, cls, polymorphic_type, field=self
            )
        else:
            self.do_polymorphic_type(polymorphic_type)

    def do_polymorphic_type(self, polymorphic_type):
        if self.default is NOT_PROVIDED and not self.null:
            opts = polymorphic_type._meta
            self.default = ContentTypeReference(opts.app_label, opts.model_name)
            self.overriden_default = True
        self.polymorphic_type = polymorphic_type
        self.type = polymorphic_type.__name__
        self.error_messages["invalid"] = (
            "Specified content type is not of a subclass of %s."
            % polymorphic_type._meta.object_name
        )

    def check(self, **kwargs):
        errors = super().check(**kwargs)
        if isinstance(self.polymorphic_type, str):
            errors.append(
                checks.Error(
                    (
                        "Field defines a relation with model '%s', which "
                        "is either not installed, or is abstract."
                    )
                    % self.polymorphic_type,
                    id="fields.E300",
                )
            )
        elif not issubclass(self.polymorphic_type, BasePolymorphicModel):
            errors.append(
                checks.Error(
                    "The %s type is not a subclass of BasePolymorphicModel."
                    % self.polymorphic_type.__name__,
                    id="polymodels.E004",
                )
            )
        return errors

    def formfield(self, **kwargs):
        db = kwargs.pop("using", None)
        if isinstance(self.polymorphic_type, str):
            raise ValueError(
                "Cannot create form field for %r yet, because its related model %r has not been loaded yet"
                % (self.name, self.polymorphic_type)
            )
        defaults = {
            "form_class": forms.ModelChoiceField,
            "queryset": LazyPolymorphicTypeQueryset(self.remote_field, db),
            "to_field_name": self.remote_field.field_name,
        }
        defaults.update(kwargs)
        return super(RelatedField, self).formfield(**defaults)

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        opts = getattr(self.polymorphic_type, "_meta", None)
        kwargs["polymorphic_type"] = (
            "%s.%s" % (opts.app_label, opts.object_name)
            if opts
            else self.polymorphic_type
        )
        for kwarg, value in list(kwargs.items()):
            if self.default_kwargs.get(kwarg) == value:
                kwargs.pop(kwarg)
        if self.overriden_default:
            kwargs.pop("default")
        kwargs.pop("limit_choices_to", None)
        return name, path, args, kwargs