Skip to content

Commit 05a3ee1

Browse files
committed
prevent the creation of embedded models
1 parent d85d5a5 commit 05a3ee1

File tree

16 files changed

+236
-23
lines changed

16 files changed

+236
-23
lines changed

django_mongodb_backend/fields/embedded_model.py

+11
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,18 @@ def __init__(self, embedded_model, *args, **kwargs):
1818
super().__init__(*args, **kwargs)
1919

2020
def check(self, **kwargs):
21+
from ..models import EmbeddedModel
22+
2123
errors = super().check(**kwargs)
24+
if not issubclass(self.embedded_model, EmbeddedModel):
25+
return [
26+
checks.Error(
27+
"Embedded models must be a subclass of "
28+
"django_mongodb_backend.models.EmbeddedModel.",
29+
obj=self,
30+
id="django_mongodb_backend.embedded_model.E002",
31+
)
32+
]
2233
for field in self.embedded_model._meta.fields:
2334
if field.remote_field:
2435
errors.append(

django_mongodb_backend/managers.py

+30
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,37 @@
1+
from django.db import NotSupportedError
12
from django.db.models.manager import BaseManager
23

34
from .queryset import MongoQuerySet
45

56

67
class MongoManager(BaseManager.from_queryset(MongoQuerySet)):
78
pass
9+
10+
11+
class EmbeddedModelManager(BaseManager):
12+
"""
13+
Prevent all queryset operations on embedded models since they don't have
14+
their own collection.
15+
16+
Raise a helpful error message for some basic QuerySet methods. Subclassing
17+
BaseManager means that other methods raise, e.g. AttributeError:
18+
'EmbeddedModelManager' object has no attribute 'update_or_create'".
19+
"""
20+
21+
def all(self):
22+
raise NotSupportedError("EmbeddedModels cannot be queried.")
23+
24+
def get(self, *args, **kwargs):
25+
raise NotSupportedError("EmbeddedModels cannot be queried.")
26+
27+
def filter(self, *args, **kwargs):
28+
raise NotSupportedError("EmbeddedModels cannot be queried.")
29+
30+
def create(self, **kwargs):
31+
raise NotSupportedError("EmbeddedModels cannot be created.")
32+
33+
def update(self, *args, **kwargs):
34+
raise NotSupportedError("EmbeddedModels cannot be updated.")
35+
36+
def delete(self):
37+
raise NotSupportedError("EmbeddedModels cannot be deleted.")

django_mongodb_backend/models.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from django.db import NotSupportedError, models
2+
3+
from .managers import EmbeddedModelManager
4+
5+
6+
class EmbeddedModel(models.Model):
7+
objects = EmbeddedModelManager()
8+
9+
class Meta:
10+
abstract = True
11+
12+
def delete(self, *args, **kwargs):
13+
raise NotSupportedError("EmbeddedModels cannot be deleted.")
14+
15+
def save(self, *args, **kwargs):
16+
raise NotSupportedError("EmbeddedModels cannot be saved.")

django_mongodb_backend/schema.py

+30
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@
1010
from .utils import OperationCollector
1111

1212

13+
def ignore_embedded_models(func):
14+
"""
15+
Make a SchemaEditor method a no-op if model is an EmbeddedModel (unless
16+
parent_model isn't None, in which case this is a valid recursive operation
17+
such as adding an index on an embedded model's field).
18+
"""
19+
20+
def wrapper(self, model, *args, **kwargs):
21+
parent_model = kwargs.get("parent_model")
22+
from .models import EmbeddedModel
23+
24+
if issubclass(model, EmbeddedModel) and parent_model is None:
25+
return
26+
func(self, model, *args, **kwargs)
27+
28+
return wrapper
29+
30+
1331
class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
1432
def get_collection(self, name):
1533
if self.collect_sql:
@@ -22,6 +40,7 @@ def get_database(self):
2240
return self.connection.get_database()
2341

2442
@wrap_database_errors
43+
@ignore_embedded_models
2544
def create_model(self, model):
2645
self.get_database().create_collection(model._meta.db_table)
2746
self._create_model_indexes(model)
@@ -75,13 +94,15 @@ def _create_model_indexes(self, model, column_prefix="", parent_model=None):
7594
for index in model._meta.indexes:
7695
self.add_index(model, index, column_prefix=column_prefix, parent_model=parent_model)
7796

97+
@ignore_embedded_models
7898
def delete_model(self, model):
7999
# Delete implicit M2m tables.
80100
for field in model._meta.local_many_to_many:
81101
if field.remote_field.through._meta.auto_created:
82102
self.delete_model(field.remote_field.through)
83103
self.get_collection(model._meta.db_table).drop()
84104

105+
@ignore_embedded_models
85106
def add_field(self, model, field):
86107
# Create implicit M2M tables.
87108
if field.many_to_many and field.remote_field.through._meta.auto_created:
@@ -103,6 +124,7 @@ def add_field(self, model, field):
103124
elif self._field_should_have_unique(field):
104125
self._add_field_unique(model, field)
105126

127+
@ignore_embedded_models
106128
def _alter_field(
107129
self,
108130
model,
@@ -149,6 +171,7 @@ def _alter_field(
149171
if not old_field_unique and new_field_unique:
150172
self._add_field_unique(model, new_field)
151173

174+
@ignore_embedded_models
152175
def remove_field(self, model, field):
153176
# Remove implicit M2M tables.
154177
if field.many_to_many and field.remote_field.through._meta.auto_created:
@@ -210,6 +233,7 @@ def _remove_model_indexes(self, model, column_prefix="", parent_model=None):
210233
for index in model._meta.indexes:
211234
self.remove_index(parent_model or model, index)
212235

236+
@ignore_embedded_models
213237
def alter_index_together(self, model, old_index_together, new_index_together, column_prefix=""):
214238
olds = {tuple(fields) for fields in old_index_together}
215239
news = {tuple(fields) for fields in new_index_together}
@@ -222,6 +246,7 @@ def alter_index_together(self, model, old_index_together, new_index_together, co
222246
for field_names in news.difference(olds):
223247
self._add_composed_index(model, field_names, column_prefix=column_prefix)
224248

249+
@ignore_embedded_models
225250
def alter_unique_together(
226251
self, model, old_unique_together, new_unique_together, column_prefix="", parent_model=None
227252
):
@@ -249,6 +274,7 @@ def alter_unique_together(
249274
model, constraint, parent_model=parent_model, column_prefix=column_prefix
250275
)
251276

277+
@ignore_embedded_models
252278
def add_index(
253279
self, model, index, *, field=None, unique=False, column_prefix="", parent_model=None
254280
):
@@ -302,6 +328,7 @@ def _add_field_index(self, model, field, *, column_prefix=""):
302328
index.name = self._create_index_name(model._meta.db_table, [column_prefix + field.column])
303329
self.add_index(model, index, field=field, column_prefix=column_prefix)
304330

331+
@ignore_embedded_models
305332
def remove_index(self, model, index):
306333
if index.contains_expressions:
307334
return
@@ -355,6 +382,7 @@ def _remove_field_index(self, model, field, column_prefix=""):
355382
)
356383
collection.drop_index(index_names[0])
357384

385+
@ignore_embedded_models
358386
def add_constraint(self, model, constraint, field=None, column_prefix="", parent_model=None):
359387
if isinstance(constraint, UniqueConstraint) and self._unique_supported(
360388
condition=constraint.condition,
@@ -384,6 +412,7 @@ def _add_field_unique(self, model, field, column_prefix=""):
384412
constraint = UniqueConstraint(fields=[field.name], name=name)
385413
self.add_constraint(model, constraint, field=field, column_prefix=column_prefix)
386414

415+
@ignore_embedded_models
387416
def remove_constraint(self, model, constraint):
388417
if isinstance(constraint, UniqueConstraint) and self._unique_supported(
389418
condition=constraint.condition,
@@ -417,6 +446,7 @@ def _remove_field_unique(self, model, field, column_prefix=""):
417446
)
418447
self.get_collection(model._meta.db_table).drop_index(constraint_names[0])
419448

449+
@ignore_embedded_models
420450
def alter_db_table(self, model, old_db_table, new_db_table):
421451
if old_db_table == new_db_table:
422452
return

docs/source/embedded-models.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ The basics
1111
Let's consider this example::
1212

1313
from django_mongodb_backend.fields import EmbeddedModelField
14+
from django_mongodb_backend.models import EmbeddedModel
1415

1516
class Customer(models.Model):
1617
name = models.CharField(...)
1718
address = EmbeddedModelField("Address")
1819
...
1920

20-
class Address(models.Model):
21+
class Address(EmbeddedModel):
2122
...
2223
city = models.CharField(...)
2324

docs/source/fields.rst

+8-4
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,11 @@ Stores a model of type ``embedded_model``.
222222

223223
This is a required argument.
224224

225-
Specifies the model class to embed. It can be either a concrete model
226-
class or a :ref:`lazy reference <lazy-relationships>` to a model class.
225+
Specifies the model class to embed. It must be a subclass of
226+
:class:`django_mongodb_backend.models.EmbeddedModel`.
227+
228+
It can be either a concrete model class or a :ref:`lazy reference
229+
<lazy-relationships>` to a model class.
227230

228231
The embedded model cannot have relational fields
229232
(:class:`~django.db.models.ForeignKey`,
@@ -234,11 +237,12 @@ Stores a model of type ``embedded_model``.
234237

235238
from django.db import models
236239
from django_mongodb_backend.fields import EmbeddedModelField
240+
from django_mongodb_backend.models import EmbeddedModel
237241

238-
class Address(models.Model):
242+
class Address(EmbeddedModel):
239243
...
240244

241-
class Author(models.Model):
245+
class Author(EmbeddedModel):
242246
address = EmbeddedModelField(Address)
243247

244248
class Book(models.Model):

docs/source/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ django-mongodb-backend 5.0.x documentation
88
fields
99
querysets
1010
forms
11+
models
1112
embedded-models
1213

1314
Indices and tables

docs/source/models.rst

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Model reference
2+
===============
3+
4+
.. module:: django_mongodb_backend.models
5+
6+
One MongoDB-specific model is available in ``django_mongodb_backend.models``.
7+
8+
.. class:: EmbeddedModel
9+
10+
An abstract model which all :doc:`embedded models <embedded-models>` must
11+
subclass.
12+
13+
Since these models are not stored in their own collection, they do not have
14+
any of the normal ``QuerySet`` methods (``all()``, ``filter()``, ``delete()``,
15+
etc.) You also cannot call ``Model.save()`` and ``delete()`` on them.

tests/model_fields_/models.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from django.db import models
44

55
from django_mongodb_backend.fields import ArrayField, EmbeddedModelField, ObjectIdField
6+
from django_mongodb_backend.models import EmbeddedModel
67

78

89
# ObjectIdField
@@ -98,19 +99,19 @@ class Holder(models.Model):
9899
data = EmbeddedModelField("Data", null=True, blank=True)
99100

100101

101-
class Data(models.Model):
102+
class Data(EmbeddedModel):
102103
integer = models.IntegerField(db_column="custom_column")
103104
auto_now = models.DateTimeField(auto_now=True)
104105
auto_now_add = models.DateTimeField(auto_now_add=True)
105106

106107

107-
class Address(models.Model):
108+
class Address(EmbeddedModel):
108109
city = models.CharField(max_length=20)
109110
state = models.CharField(max_length=2)
110111
zip_code = models.IntegerField(db_index=True)
111112

112113

113-
class Author(models.Model):
114+
class Author(EmbeddedModel):
114115
name = models.CharField(max_length=10)
115116
age = models.IntegerField()
116117
address = EmbeddedModelField(Address)

tests/model_fields_/test_embedded_model.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.test.utils import isolate_apps
55

66
from django_mongodb_backend.fields import EmbeddedModelField
7+
from django_mongodb_backend.models import EmbeddedModel
78

89
from .models import (
910
Address,
@@ -108,7 +109,7 @@ def test_nested(self):
108109
@isolate_apps("model_fields_")
109110
class CheckTests(SimpleTestCase):
110111
def test_no_relational_fields(self):
111-
class Target(models.Model):
112+
class Target(EmbeddedModel):
112113
key = models.ForeignKey("MyModel", models.CASCADE)
113114

114115
class MyModel(models.Model):
@@ -121,3 +122,19 @@ class MyModel(models.Model):
121122
self.assertEqual(
122123
msg, "Embedded models cannot have relational fields (Target.key is a ForeignKey)."
123124
)
125+
126+
def test_embedded_model_subclass(self):
127+
class Target(models.Model):
128+
pass
129+
130+
class MyModel(models.Model):
131+
field = EmbeddedModelField(Target)
132+
133+
errors = MyModel().check()
134+
self.assertEqual(len(errors), 1)
135+
self.assertEqual(errors[0].id, "django_mongodb_backend.embedded_model.E002")
136+
msg = errors[0].msg
137+
self.assertEqual(
138+
msg,
139+
"Embedded models must be a subclass of django_mongodb_backend.models.EmbeddedModel.",
140+
)

tests/model_forms_/models.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from django.db import models
22

33
from django_mongodb_backend.fields import EmbeddedModelField
4+
from django_mongodb_backend.models import EmbeddedModel
45

56

6-
class Address(models.Model):
7+
class Address(EmbeddedModel):
78
po_box = models.CharField(max_length=50, blank=True, verbose_name="PO Box")
89
city = models.CharField(max_length=20)
910
state = models.CharField(max_length=2)
@@ -15,8 +16,3 @@ class Author(models.Model):
1516
age = models.IntegerField()
1617
address = EmbeddedModelField(Address)
1718
billing_address = EmbeddedModelField(Address, blank=True, null=True)
18-
19-
20-
class Book(models.Model):
21-
name = models.CharField(max_length=100)
22-
author = EmbeddedModelField(Author)

tests/models_/__init__.py

Whitespace-only changes.

tests/models_/models.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django_mongodb_backend.models import EmbeddedModel
2+
3+
4+
class Embed(EmbeddedModel):
5+
pass

0 commit comments

Comments
 (0)