Ticket #9475: smart_m2mfield.py

File smart_m2mfield.py, 6.1 KB (added by Antti Kaihola, 14 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")
Back to Top