1
+ from itertools import chain
2
+
3
+ from django .core .exceptions import FieldError
1
4
from django .db import NotSupportedError , models
2
5
3
- from .managers import EmbeddedModelManager
6
+ from .managers import EmbeddedModelManager , MultiMongoManager
4
7
5
8
6
9
class EmbeddedModel (models .Model ):
@@ -14,3 +17,136 @@ def delete(self, *args, **kwargs):
14
17
15
18
def save (self , * args , ** kwargs ):
16
19
raise NotSupportedError ("EmbeddedModels cannot be saved." )
20
+
21
+
22
+ class ModelBaseOverride (models .base .ModelBase ):
23
+ __excluded_fieldnames = ("_t" , "id" )
24
+
25
+ def __new__ (cls , name , bases , attrs , ** kwargs ):
26
+ """An override to the ModelBase which inspects inherited Model
27
+ definitions and passes down the field names and table reference
28
+ from parent to child model.
29
+ ** REMAINING TODO
30
+ - Handle Index Creation
31
+ - Tests
32
+ """
33
+ parents = [b for b in bases if isinstance (b , models .base .ModelBase )]
34
+
35
+ # if no ModelBase instances found, this is the first inherited MultiModel
36
+ if not parents :
37
+ return super ().__new__ (cls , name , bases , attrs , ** kwargs )
38
+
39
+ # Recursively fetch all fields of a class.
40
+ # Only conclude the loop when we get the MultiModel class
41
+ # We cannot explicitly pass a reference to the MultiModel class
42
+ # because this builds a circluar dependency
43
+ holder = bases
44
+ traverse = holder [0 ]
45
+ if traverse .__name__ != "MultiModel" and hasattr (traverse , "_meta" ):
46
+ while traverse and traverse .__name__ != "MultiModel" and hasattr (traverse , "_meta" ):
47
+ traverse = traverse ._meta ._bases [0 ] if traverse ._meta ._bases else None
48
+ holder = (traverse ,)
49
+
50
+ parent_fields = []
51
+
52
+ # Set up managed + default db if not set
53
+ if hasattr (parents [0 ], "_meta" ) and parents [0 ].__name__ != "MultiModel" :
54
+ if not attrs .get ("Meta" ):
55
+
56
+ class Meta :
57
+ db_table = parents [0 ]._meta .db_table
58
+ managed = False
59
+
60
+ attrs ["Meta" ] = Meta ()
61
+
62
+ elif meta := attrs .get ("Meta" ):
63
+ if not getattr (meta , "db_table" , None ):
64
+ meta .db_table = parents [0 ]._meta .db_table
65
+ if not getattr (meta , "managed" , None ):
66
+ meta .managed = False
67
+ parent_fields = set (parents [0 ]._meta .local_fields + parents [0 ]._meta .local_many_to_many )
68
+
69
+ # The parent class will not be passed to the __new__ construction
70
+ # because we will leverage Django's multi-table inheritance
71
+ # which would lead to more complications on field resolution
72
+ new_attrs = {** attrs }
73
+
74
+ for field in parent_fields :
75
+ if not models .base ._has_contribute_to_class (field ):
76
+ if field .name in new_attrs :
77
+ raise FieldError (
78
+ f"Local field { field .name !r} in class { name !r} clashes with field of "
79
+ f"the same name from base class { parents [0 ].__name__ !r} ."
80
+ )
81
+ new_attrs [field .name ] = field
82
+
83
+ # Construct new class without passing the parent reference, but adding
84
+ # every new (derived) attribute to the django class
85
+ new_cls = super ().__new__ (cls , name , holder , new_attrs , ** kwargs )
86
+
87
+ new_fields = chain (
88
+ new_cls ._meta .local_fields ,
89
+ new_cls ._meta .local_many_to_many ,
90
+ new_cls ._meta .private_fields ,
91
+ )
92
+ field_names = {f .name for f in new_fields }
93
+
94
+ for field in parent_fields :
95
+ if field .primary_key or field .name in ModelBaseOverride .__excluded_fieldnames :
96
+ continue
97
+ if models .base ._has_contribute_to_class (field ):
98
+ if (
99
+ field .name in field_names
100
+ and field .name not in ModelBaseOverride .__excluded_fieldnames
101
+ ):
102
+ raise FieldError (
103
+ f"Local field { field .name !r} in class { name !r} clashes with field of "
104
+ f"the same name from base class { parents [0 ].__name__ !r} ."
105
+ )
106
+
107
+ # if not hasattr(new_cls, field.name):
108
+ new_cls .add_to_class (field .name , field )
109
+ # Add each value as a subclass to its parent MultiModel object
110
+ for _base in parents :
111
+ # equivalent of if _base is MultiModel
112
+ if hasattr (_base , "_subclasses" ):
113
+ _base ._subclasses .setdefault (_base , []).append (new_cls )
114
+
115
+ new_cls ._meta ._bases = parents
116
+ new_cls ._meta .parents = {}
117
+ return new_cls
118
+
119
+
120
+ class MultiModel (models .Model , metaclass = ModelBaseOverride ):
121
+ """Manager handles tracking all inherited subclasses to be used in the MultiMongoManager query"""
122
+
123
+ _subclasses = {}
124
+
125
+ def __init_subclass__ (cls , ** kwargs ):
126
+ super ().__init_subclass__ (** kwargs )
127
+ for _base in cls .__bases__ :
128
+ if issubclass (_base , MultiModel ):
129
+ MultiModel ._subclasses .setdefault (_base , []).append (cls )
130
+
131
+ # Get all the subclasses for my model
132
+ @classmethod
133
+ def subclasses (cls ):
134
+ stack = [cls ]
135
+ acc = set ()
136
+ while stack :
137
+ node = stack .pop ()
138
+ stack .extend (cls ._subclasses .get (node , []))
139
+ acc .add (node )
140
+ return [obj .__name__ for obj in acc ]
141
+
142
+ _t = models .CharField (max_length = 255 , editable = False )
143
+ objects = MultiMongoManager ()
144
+
145
+ # Save the classname as the _t before saving
146
+ def save (self , * args , ** kwargs ):
147
+ if not self ._t :
148
+ self ._t = self .__class__ .__name__
149
+ super ().save (* args , ** kwargs )
150
+
151
+ class Meta :
152
+ abstract = True
0 commit comments