Code

Ticket #9475: smart_m2mfield.py

File smart_m2mfield.py, 6.1 KB (added by akaihola, 4 years ago)

partial solution as a separate module for non-patched Django

Line 
1"""add(), create() etc. support for Django's intermediate ManyToMany models
2
3This module implements at least part of
4http://code.djangoproject.com/ticket/9475 without patching Django.
5
6It only detects which fields have default values, and doesn't try to
7be super smart beyond that.
8
9Usage: Instead of models.ManyToManyField, use
10       smart_m2mfield.SmartManyToManyField.
11"""
12
13from django.utils.functional import curry
14from django.db.models.fields import AutoField, NOT_PROVIDED
15from django.db.models.fields.related import (
16    ReverseManyRelatedObjectsDescriptor, ManyToManyField,
17    create_many_to_many_intermediary_model, add_lazy_relation,
18    create_many_related_manager)
19
20
21def has_defaults(field):
22    """Return False for through m2m with no defaults
23
24    If the many-to-many relation defined by the given field uses a
25    manually defined intermediary model, and the model doesn't define
26    defaults for all extra fields, this function returns False.
27    """
28    for through_field in field.rel.through._meta.fields:
29        if (not isinstance(through_field, AutoField)
30            and through_field.name not in (field.m2m_field_name(),
31                                           field.m2m_reverse_field_name())
32            and through_field.default == NOT_PROVIDED):
33            return False
34    return True
35
36
37class SmartReverseManyRelatedObjectsDescriptor(
38    ReverseManyRelatedObjectsDescriptor):
39    """Smarter reverse m2m related objects descriptor
40
41    This is a modified version of ReverseManyRelatedObjectsDescriptor
42    to support add(), create() etc. when ``through=`` is used but all
43    extra fields of the intermediary model have a default value
44    """
45
46    def __get__(self, instance, instance_type=None):
47        if instance is None:
48            return self
49
50        # Dynamically create a class that subclasses the related
51        # model's default manager.
52        rel_model = self.field.rel.to
53        superclass = rel_model._default_manager.__class__
54        RelatedManager = create_many_related_manager(
55            superclass, self.field.rel)
56
57        if (not self.field.rel.through._meta.auto_created and
58            has_defaults(self.field)):
59
60            def add(self, *objs):
61                self._add_items(
62                    self.source_field_name, self.target_field_name, *objs)
63                if self.symmetrical:
64                    self._add_items(
65                        self.target_field_name, self.source_field_name, *objs)
66            add.alters_data = True
67
68            def remove(self, *objs):
69                self._remove_items(
70                    self.source_field_name, self.target_field_name, *objs)
71                if self.symmetrical:
72                    self._remove_items(
73                        self.target_field_name, self.source_field_name, *objs)
74            remove.alters_data = True
75
76            RelatedManager.add = add
77            RelatedManager.remove = remove
78
79        manager = RelatedManager(
80            model=rel_model,
81            core_filters={'%s__pk' % self.field.related_query_name():
82                          instance._get_pk_val()},
83            instance=instance,
84            symmetrical=(self.field.rel.symmetrical and
85                         isinstance(instance, rel_model)),
86            source_field_name=self.field.m2m_field_name(),
87            target_field_name=self.field.m2m_reverse_field_name(),
88            reverse=False)
89
90        return manager
91
92    def __set__(self, instance, value):
93        if instance is None:
94            raise AttributeError("Manager must be accessed via instance")
95
96        if (not self.field.rel.through._meta.auto_created and
97            not has_defaults(self.field)):
98            opts = self.field.rel.through._meta
99            raise AttributeError(
100                "Cannot set values on a ManyToManyField which specifies an "
101                "intermediary model with fields which have no default value.  "
102                "Use %s.%s's Manager instead." % (
103                    opts.app_label, opts.object_name))
104
105        manager = self.__get__(instance)
106        manager.clear()
107        manager.add(*value)
108
109
110class SmartManyToManyField(ManyToManyField):
111
112    def contribute_to_class(self, cls, name):
113        """Modified version of ManyToManyField.contribute_to_class
114
115        Support support add(), create() etc. when ``through=`` is used
116        but all extra fields of the intermediary model have a default
117        value.
118        """
119
120        # To support multiple relations to self, it's useful to have a non-None
121        # related name on symmetrical relations for internal reasons. The
122        # concept doesn't make a lot of sense externally ("you want me to
123        # specify *what* on my non-reversible relation?!"), so we set it up
124        # automatically. The funky name reduces the chance of an accidental
125        # clash.
126        if self.rel.symmetrical and (
127            self.rel.to == "self" or self.rel.to == cls._meta.object_name):
128            self.rel.related_name = "%s_rel_+" % name
129
130        super(SmartManyToManyField, self).contribute_to_class(cls, name)
131
132        # The intermediate m2m model is not auto created if:
133        #  1) There is a manually specified intermediate, or
134        #  2) The class owning the m2m field is abstract.
135        if not self.rel.through and not cls._meta.abstract:
136            self.rel.through = create_many_to_many_intermediary_model(
137                self, cls)
138
139        # Add the descriptor for the m2m relation
140        setattr(cls, self.name, SmartReverseManyRelatedObjectsDescriptor(self))
141
142        # Set up the accessor for the m2m table name for the relation
143        self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta)
144
145        # Populate some necessary rel arguments so that cross-app relations
146        # work correctly.
147        if isinstance(self.rel.through, basestring):
148
149            def resolve_through_model(field, model, cls):
150                field.rel.through = model
151
152            add_lazy_relation(cls, self, self.rel.through,
153                              resolve_through_model)
154
155        if isinstance(self.rel.to, basestring):
156            target = self.rel.to
157        else:
158            target = self.rel.to._meta.db_table
159        cls._meta.duplicate_targets[self.column] = (target, "m2m")