plain.models 0.45.0__py3-none-any.whl → 0.46.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain/models/CHANGELOG.md +13 -0
- plain/models/backends/base/validation.py +1 -1
- plain/models/backends/mysql/base.py +0 -1
- plain/models/backends/mysql/validation.py +16 -16
- plain/models/backends/postgresql/base.py +0 -1
- plain/models/backends/postgresql/operations.py +1 -2
- plain/models/backends/sqlite3/base.py +0 -1
- plain/models/base.py +206 -234
- plain/models/config.py +1 -9
- plain/models/fields/__init__.py +117 -227
- plain/models/fields/json.py +22 -22
- plain/models/fields/mixins.py +11 -10
- plain/models/fields/related.py +131 -119
- plain/models/migrations/state.py +1 -1
- plain/models/preflight.py +105 -98
- {plain_models-0.45.0.dist-info → plain_models-0.46.0.dist-info}/METADATA +1 -1
- {plain_models-0.45.0.dist-info → plain_models-0.46.0.dist-info}/RECORD +20 -20
- {plain_models-0.45.0.dist-info → plain_models-0.46.0.dist-info}/WHEEL +0 -0
- {plain_models-0.45.0.dist-info → plain_models-0.46.0.dist-info}/entry_points.txt +0 -0
- {plain_models-0.45.0.dist-info → plain_models-0.46.0.dist-info}/licenses/LICENSE +0 -0
plain/models/fields/json.py
CHANGED
@@ -23,7 +23,7 @@ class JSONField(CheckFieldDefaultMixin, Field):
|
|
23
23
|
default_error_messages = {
|
24
24
|
"invalid": "Value must be valid JSON.",
|
25
25
|
}
|
26
|
-
|
26
|
+
_default_fix = ("dict", "{}")
|
27
27
|
|
28
28
|
def __init__(
|
29
29
|
self,
|
@@ -40,31 +40,31 @@ class JSONField(CheckFieldDefaultMixin, Field):
|
|
40
40
|
self.decoder = decoder
|
41
41
|
super().__init__(**kwargs)
|
42
42
|
|
43
|
-
def
|
44
|
-
errors = super().
|
45
|
-
|
46
|
-
errors.extend(self._check_supported(database))
|
43
|
+
def preflight(self, **kwargs):
|
44
|
+
errors = super().preflight(**kwargs)
|
45
|
+
errors.extend(self._check_supported())
|
47
46
|
return errors
|
48
47
|
|
49
|
-
def _check_supported(self
|
48
|
+
def _check_supported(self):
|
50
49
|
errors = []
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
50
|
+
|
51
|
+
if (
|
52
|
+
self.model._meta.required_db_vendor
|
53
|
+
and self.model._meta.required_db_vendor != db_connection.vendor
|
54
|
+
):
|
55
|
+
return errors
|
56
|
+
|
57
|
+
if not (
|
58
|
+
"supports_json_field" in self.model._meta.required_db_features
|
59
|
+
or db_connection.features.supports_json_field
|
60
|
+
):
|
61
|
+
errors.append(
|
62
|
+
preflight.PreflightResult(
|
63
|
+
fix=f"{db_connection.display_name} does not support JSONFields. Consider using a TextField with JSON serialization or upgrade to a database that supports JSON fields.",
|
64
|
+
obj=self.model,
|
65
|
+
id="fields.json_field_unsupported",
|
67
66
|
)
|
67
|
+
)
|
68
68
|
return errors
|
69
69
|
|
70
70
|
def deconstruct(self):
|
plain/models/fields/mixins.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
from plain import
|
1
|
+
from plain.preflight import PreflightResult
|
2
2
|
|
3
3
|
NOT_PROVIDED = object()
|
4
4
|
|
@@ -29,7 +29,7 @@ class FieldCacheMixin:
|
|
29
29
|
|
30
30
|
|
31
31
|
class CheckFieldDefaultMixin:
|
32
|
-
|
32
|
+
_default_fix = ("<valid default>", "<invalid default>")
|
33
33
|
|
34
34
|
def _check_default(self):
|
35
35
|
if (
|
@@ -38,21 +38,22 @@ class CheckFieldDefaultMixin:
|
|
38
38
|
and not callable(self.default)
|
39
39
|
):
|
40
40
|
return [
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
41
|
+
PreflightResult(
|
42
|
+
fix=(
|
43
|
+
f"{self.__class__.__name__} default should be a callable instead of an instance "
|
44
|
+
"so that it's not shared between all field instances. "
|
45
45
|
"Use a callable instead, e.g., use `{}` instead of "
|
46
|
-
"`{}`.".format(*self.
|
46
|
+
"`{}`.".format(*self._default_fix)
|
47
47
|
),
|
48
48
|
obj=self,
|
49
|
-
id="fields.
|
49
|
+
id="fields.invalid_choice_mixin_default",
|
50
|
+
warning=True,
|
50
51
|
)
|
51
52
|
]
|
52
53
|
else:
|
53
54
|
return []
|
54
55
|
|
55
|
-
def
|
56
|
-
errors = super().
|
56
|
+
def preflight(self, **kwargs):
|
57
|
+
errors = super().preflight(**kwargs)
|
57
58
|
errors.extend(self._check_default())
|
58
59
|
return errors
|
plain/models/fields/related.py
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
from functools import cached_property, partial
|
2
2
|
|
3
|
-
from plain import exceptions
|
3
|
+
from plain import exceptions
|
4
4
|
from plain.models.constants import LOOKUP_SEP
|
5
5
|
from plain.models.deletion import SET_DEFAULT, SET_NULL
|
6
6
|
from plain.models.query_utils import PathInfo, Q
|
7
7
|
from plain.models.utils import make_model_tuple
|
8
|
+
from plain.preflight import PreflightResult
|
8
9
|
from plain.runtime import SettingsReference
|
9
10
|
|
10
11
|
from ..registry import models_registry
|
@@ -106,9 +107,9 @@ class RelatedField(FieldCacheMixin, Field):
|
|
106
107
|
models_registry.check_ready()
|
107
108
|
return self.remote_field.model
|
108
109
|
|
109
|
-
def
|
110
|
+
def preflight(self, **kwargs):
|
110
111
|
return [
|
111
|
-
*super().
|
112
|
+
*super().preflight(**kwargs),
|
112
113
|
*self._check_related_name_is_valid(),
|
113
114
|
*self._check_related_query_name_is_valid(),
|
114
115
|
*self._check_relation_model_exists(),
|
@@ -126,11 +127,10 @@ class RelatedField(FieldCacheMixin, Field):
|
|
126
127
|
)
|
127
128
|
if not is_valid_id:
|
128
129
|
return [
|
129
|
-
|
130
|
-
f"The name '{self.remote_field.related_name}' is invalid related_name for field {self.model._meta.object_name}.{self.name}",
|
131
|
-
hint="Related name must be a valid Python identifier.",
|
130
|
+
PreflightResult(
|
131
|
+
fix=f"The name '{self.remote_field.related_name}' is invalid related_name for field {self.model._meta.object_name}.{self.name}. Related name must be a valid Python identifier.",
|
132
132
|
obj=self,
|
133
|
-
id="fields.
|
133
|
+
id="fields.invalid_related_name",
|
134
134
|
)
|
135
135
|
]
|
136
136
|
return []
|
@@ -142,26 +142,26 @@ class RelatedField(FieldCacheMixin, Field):
|
|
142
142
|
errors = []
|
143
143
|
if rel_query_name.endswith("_"):
|
144
144
|
errors.append(
|
145
|
-
|
146
|
-
|
147
|
-
|
145
|
+
PreflightResult(
|
146
|
+
fix=(
|
147
|
+
f"Reverse query name '{rel_query_name}' must not end with an underscore. "
|
148
148
|
"Add or change a related_name or related_query_name "
|
149
149
|
"argument for this field."
|
150
150
|
),
|
151
151
|
obj=self,
|
152
|
-
id="fields.
|
152
|
+
id="fields.related_field_accessor_clash",
|
153
153
|
)
|
154
154
|
)
|
155
155
|
if LOOKUP_SEP in rel_query_name:
|
156
156
|
errors.append(
|
157
|
-
|
158
|
-
|
159
|
-
|
157
|
+
PreflightResult(
|
158
|
+
fix=(
|
159
|
+
f"Reverse query name '{rel_query_name}' must not contain '{LOOKUP_SEP}'. "
|
160
160
|
"Add or change a related_name or related_query_name "
|
161
161
|
"argument for this field."
|
162
162
|
),
|
163
163
|
obj=self,
|
164
|
-
id="fields.
|
164
|
+
id="fields.related_field_query_name_clash",
|
165
165
|
)
|
166
166
|
)
|
167
167
|
return errors
|
@@ -178,11 +178,13 @@ class RelatedField(FieldCacheMixin, Field):
|
|
178
178
|
)
|
179
179
|
if rel_is_missing and rel_is_string:
|
180
180
|
return [
|
181
|
-
|
182
|
-
|
183
|
-
|
181
|
+
PreflightResult(
|
182
|
+
fix=(
|
183
|
+
f"Field defines a relation with model '{model_name}', which is either "
|
184
|
+
"not installed, or is abstract. Ensure the model is installed and not abstract."
|
185
|
+
),
|
184
186
|
obj=self,
|
185
|
-
id="fields.
|
187
|
+
id="fields.related_model_not_installed",
|
186
188
|
)
|
187
189
|
]
|
188
190
|
return []
|
@@ -230,29 +232,29 @@ class RelatedField(FieldCacheMixin, Field):
|
|
230
232
|
clash_name = f"{rel_opts.label}.{clash_field.name}"
|
231
233
|
if not rel_is_hidden and clash_field.name == rel_name:
|
232
234
|
errors.append(
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
235
|
+
PreflightResult(
|
236
|
+
fix=(
|
237
|
+
f"Reverse accessor '{rel_opts.object_name}.{rel_name}' "
|
238
|
+
f"for '{field_name}' clashes with field name "
|
239
|
+
f"'{clash_name}'. "
|
238
240
|
f"Rename field '{clash_name}', or add/change a related_name "
|
239
241
|
f"argument to the definition for field '{field_name}'."
|
240
242
|
),
|
241
243
|
obj=self,
|
242
|
-
id="fields.
|
244
|
+
id="fields.related_accessor_clash_field",
|
243
245
|
)
|
244
246
|
)
|
245
247
|
|
246
248
|
if clash_field.name == rel_query_name:
|
247
249
|
errors.append(
|
248
|
-
|
249
|
-
|
250
|
-
|
250
|
+
PreflightResult(
|
251
|
+
fix=(
|
252
|
+
f"Reverse query name for '{field_name}' clashes with field name '{clash_name}'. "
|
251
253
|
f"Rename field '{clash_name}', or add/change a related_name "
|
252
254
|
f"argument to the definition for field '{field_name}'."
|
253
255
|
),
|
254
256
|
obj=self,
|
255
|
-
id="fields.
|
257
|
+
id="fields.related_accessor_clash_manager",
|
256
258
|
)
|
257
259
|
)
|
258
260
|
|
@@ -267,30 +269,30 @@ class RelatedField(FieldCacheMixin, Field):
|
|
267
269
|
)
|
268
270
|
if not rel_is_hidden and clash_field.get_accessor_name() == rel_name:
|
269
271
|
errors.append(
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
272
|
+
PreflightResult(
|
273
|
+
fix=(
|
274
|
+
f"Reverse accessor '{rel_opts.object_name}.{rel_name}' "
|
275
|
+
f"for '{field_name}' clashes with reverse accessor for "
|
276
|
+
f"'{clash_name}'. "
|
275
277
|
"Add or change a related_name argument "
|
276
278
|
f"to the definition for '{field_name}' or '{clash_name}'."
|
277
279
|
),
|
278
280
|
obj=self,
|
279
|
-
id="fields.
|
281
|
+
id="fields.related_name_clash",
|
280
282
|
)
|
281
283
|
)
|
282
284
|
|
283
285
|
if clash_field.get_accessor_name() == rel_query_name:
|
284
286
|
errors.append(
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
287
|
+
PreflightResult(
|
288
|
+
fix=(
|
289
|
+
f"Reverse query name for '{field_name}' clashes with reverse query name "
|
290
|
+
f"for '{clash_name}'. "
|
289
291
|
"Add or change a related_name argument "
|
290
292
|
f"to the definition for '{field_name}' or '{clash_name}'."
|
291
293
|
),
|
292
294
|
obj=self,
|
293
|
-
id="fields.
|
295
|
+
id="fields.related_query_name_clash",
|
294
296
|
)
|
295
297
|
)
|
296
298
|
|
@@ -600,9 +602,9 @@ class ForeignKey(RelatedField):
|
|
600
602
|
self.remote_field.limit_choices_to
|
601
603
|
)
|
602
604
|
|
603
|
-
def
|
605
|
+
def preflight(self, **kwargs):
|
604
606
|
return [
|
605
|
-
*super().
|
607
|
+
*super().preflight(**kwargs),
|
606
608
|
*self._check_on_delete(),
|
607
609
|
]
|
608
610
|
|
@@ -610,23 +612,24 @@ class ForeignKey(RelatedField):
|
|
610
612
|
on_delete = getattr(self.remote_field, "on_delete", None)
|
611
613
|
if on_delete == SET_NULL and not self.allow_null:
|
612
614
|
return [
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
"Set allow_null=True argument on the field, or change the on_delete "
|
617
|
-
"rule."
|
615
|
+
PreflightResult(
|
616
|
+
fix=(
|
617
|
+
"Field specifies on_delete=SET_NULL, but cannot be null. "
|
618
|
+
"Set allow_null=True argument on the field, or change the on_delete rule."
|
618
619
|
),
|
619
620
|
obj=self,
|
620
|
-
id="fields.
|
621
|
+
id="fields.foreign_key_null_constraint_violation",
|
621
622
|
)
|
622
623
|
]
|
623
624
|
elif on_delete == SET_DEFAULT and not self.has_default():
|
624
625
|
return [
|
625
|
-
|
626
|
-
|
627
|
-
|
626
|
+
PreflightResult(
|
627
|
+
fix=(
|
628
|
+
"Field specifies on_delete=SET_DEFAULT, but has no default value. "
|
629
|
+
"Set a default value, or change the on_delete rule."
|
630
|
+
),
|
628
631
|
obj=self,
|
629
|
-
id="fields.
|
632
|
+
id="fields.foreign_key_set_default_no_default",
|
630
633
|
)
|
631
634
|
]
|
632
635
|
else:
|
@@ -834,9 +837,9 @@ class ManyToManyField(RelatedField):
|
|
834
837
|
**kwargs,
|
835
838
|
)
|
836
839
|
|
837
|
-
def
|
840
|
+
def preflight(self, **kwargs):
|
838
841
|
return [
|
839
|
-
*super().
|
842
|
+
*super().preflight(**kwargs),
|
840
843
|
*self._check_relationship_model(**kwargs),
|
841
844
|
*self._check_ignored_options(**kwargs),
|
842
845
|
*self._check_table_uniqueness(**kwargs),
|
@@ -847,36 +850,43 @@ class ManyToManyField(RelatedField):
|
|
847
850
|
|
848
851
|
if self.has_null_arg:
|
849
852
|
warnings.append(
|
850
|
-
|
851
|
-
"null has no effect on ManyToManyField.",
|
853
|
+
PreflightResult(
|
854
|
+
fix="The 'null' option has no effect on ManyToManyField. Remove the 'null' argument.",
|
852
855
|
obj=self,
|
853
|
-
id="fields.
|
856
|
+
id="fields.m2m_null_has_no_effect",
|
857
|
+
warning=True,
|
854
858
|
)
|
855
859
|
)
|
856
860
|
|
857
861
|
if self._validators:
|
858
862
|
warnings.append(
|
859
|
-
|
860
|
-
"ManyToManyField does not support validators.",
|
863
|
+
PreflightResult(
|
864
|
+
fix="ManyToManyField does not support validators. Remove validators from this field.",
|
861
865
|
obj=self,
|
862
|
-
id="fields.
|
866
|
+
id="fields.m2m_validators_not_supported",
|
867
|
+
warning=True,
|
863
868
|
)
|
864
869
|
)
|
865
870
|
if self.remote_field.symmetrical and self._related_name:
|
866
871
|
warnings.append(
|
867
|
-
|
868
|
-
|
869
|
-
|
872
|
+
PreflightResult(
|
873
|
+
fix=(
|
874
|
+
"The 'related_name' argument has no effect on ManyToManyField "
|
875
|
+
'with a symmetrical relationship, e.g. to "self". '
|
876
|
+
"Remove the 'related_name' argument or set symmetrical=False."
|
877
|
+
),
|
870
878
|
obj=self,
|
871
|
-
id="fields.
|
879
|
+
id="fields.m2m_related_name_no_effect_symmetrical",
|
880
|
+
warning=True,
|
872
881
|
)
|
873
882
|
)
|
874
883
|
if self.db_comment:
|
875
884
|
warnings.append(
|
876
|
-
|
877
|
-
"db_comment has no effect on ManyToManyField.",
|
885
|
+
PreflightResult(
|
886
|
+
fix="The 'db_comment' option has no effect on ManyToManyField. Remove the 'db_comment' argument.",
|
878
887
|
obj=self,
|
879
|
-
id="fields.
|
888
|
+
id="fields.m2m_db_comment_has_no_effect",
|
889
|
+
warning=True,
|
880
890
|
)
|
881
891
|
)
|
882
892
|
|
@@ -893,11 +903,14 @@ class ManyToManyField(RelatedField):
|
|
893
903
|
if self.remote_field.through not in self.opts.models_registry.get_models():
|
894
904
|
# The relationship model is not installed.
|
895
905
|
errors.append(
|
896
|
-
|
897
|
-
|
898
|
-
|
906
|
+
PreflightResult(
|
907
|
+
fix=(
|
908
|
+
"Field specifies a many-to-many relation through model "
|
909
|
+
f"'{qualified_model_name}', which has not been installed. "
|
910
|
+
"Ensure the through model is properly defined and installed."
|
911
|
+
),
|
899
912
|
obj=self,
|
900
|
-
id="fields.
|
913
|
+
id="fields.m2m_through_model_not_installed",
|
901
914
|
)
|
902
915
|
)
|
903
916
|
|
@@ -925,18 +938,16 @@ class ManyToManyField(RelatedField):
|
|
925
938
|
|
926
939
|
if seen_self > 2 and not self.remote_field.through_fields:
|
927
940
|
errors.append(
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
"through_fields keyword argument.",
|
934
|
-
hint=(
|
941
|
+
PreflightResult(
|
942
|
+
fix=(
|
943
|
+
"The model is used as an intermediate model by "
|
944
|
+
f"'{self}', but it has more than two foreign keys "
|
945
|
+
f"to '{from_model_name}', which is ambiguous. "
|
935
946
|
"Use through_fields to specify which two foreign keys "
|
936
947
|
"Plain should use."
|
937
948
|
),
|
938
949
|
obj=self.remote_field.through,
|
939
|
-
id="fields.
|
950
|
+
id="fields.m2m_through_model_ambiguous_fks",
|
940
951
|
)
|
941
952
|
)
|
942
953
|
|
@@ -953,47 +964,48 @@ class ManyToManyField(RelatedField):
|
|
953
964
|
|
954
965
|
if seen_from > 1 and not self.remote_field.through_fields:
|
955
966
|
errors.append(
|
956
|
-
|
957
|
-
(
|
967
|
+
PreflightResult(
|
968
|
+
fix=(
|
958
969
|
"The model is used as an intermediate model by "
|
959
970
|
f"'{self}', but it has more than one foreign key "
|
960
971
|
f"from '{from_model_name}', which is ambiguous. You must specify "
|
961
972
|
"which foreign key Plain should use via the "
|
962
|
-
"through_fields keyword argument."
|
963
|
-
),
|
964
|
-
hint=(
|
973
|
+
"through_fields keyword argument. "
|
965
974
|
"If you want to create a recursive relationship, "
|
966
975
|
f'use ManyToManyField("{RECURSIVE_RELATIONSHIP_CONSTANT}", through="{relationship_model_name}").'
|
967
976
|
),
|
968
977
|
obj=self,
|
969
|
-
id="fields.
|
978
|
+
id="fields.m2m_through_model_invalid_recursive_from",
|
970
979
|
)
|
971
980
|
)
|
972
981
|
|
973
982
|
if seen_to > 1 and not self.remote_field.through_fields:
|
974
983
|
errors.append(
|
975
|
-
|
976
|
-
|
977
|
-
|
978
|
-
|
979
|
-
|
980
|
-
|
981
|
-
|
984
|
+
PreflightResult(
|
985
|
+
fix=(
|
986
|
+
"The model is used as an intermediate model by "
|
987
|
+
f"'{self}', but it has more than one foreign key "
|
988
|
+
f"to '{to_model_name}', which is ambiguous. You must specify "
|
989
|
+
"which foreign key Plain should use via the "
|
990
|
+
"through_fields keyword argument. "
|
982
991
|
"If you want to create a recursive relationship, "
|
983
992
|
f'use ManyToManyField("{RECURSIVE_RELATIONSHIP_CONSTANT}", through="{relationship_model_name}").'
|
984
993
|
),
|
985
994
|
obj=self,
|
986
|
-
id="fields.
|
995
|
+
id="fields.m2m_through_model_invalid_recursive_to",
|
987
996
|
)
|
988
997
|
)
|
989
998
|
|
990
999
|
if seen_from == 0 or seen_to == 0:
|
991
1000
|
errors.append(
|
992
|
-
|
993
|
-
|
994
|
-
|
1001
|
+
PreflightResult(
|
1002
|
+
fix=(
|
1003
|
+
"The model is used as an intermediate model by "
|
1004
|
+
f"'{self}', but it does not have a foreign key to '{from_model_name}' or '{to_model_name}'. "
|
1005
|
+
"Add the required foreign keys to the through model."
|
1006
|
+
),
|
995
1007
|
obj=self.remote_field.through,
|
996
|
-
id="fields.
|
1008
|
+
id="fields.m2m_through_model_missing_fk",
|
997
1009
|
)
|
998
1010
|
)
|
999
1011
|
|
@@ -1007,16 +1019,16 @@ class ManyToManyField(RelatedField):
|
|
1007
1019
|
and self.remote_field.through_fields[1]
|
1008
1020
|
):
|
1009
1021
|
errors.append(
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
1022
|
+
PreflightResult(
|
1023
|
+
fix=(
|
1024
|
+
"Field specifies 'through_fields' but does not provide "
|
1025
|
+
"the names of the two link fields that should be used "
|
1026
|
+
f"for the relation through model '{qualified_model_name}'. "
|
1015
1027
|
"Make sure you specify 'through_fields' as "
|
1016
|
-
"through_fields=('field1', 'field2')"
|
1028
|
+
"through_fields=('field1', 'field2')."
|
1017
1029
|
),
|
1018
1030
|
obj=self,
|
1019
|
-
id="fields.
|
1031
|
+
id="fields.m2m_through_fields_wrong_length",
|
1020
1032
|
)
|
1021
1033
|
)
|
1022
1034
|
|
@@ -1051,7 +1063,7 @@ class ManyToManyField(RelatedField):
|
|
1051
1063
|
):
|
1052
1064
|
possible_field_names.append(f.name)
|
1053
1065
|
if possible_field_names:
|
1054
|
-
|
1066
|
+
fix = (
|
1055
1067
|
"Did you mean one of the following foreign keys to '{}': "
|
1056
1068
|
"{}?".format(
|
1057
1069
|
related_model._meta.object_name,
|
@@ -1059,17 +1071,16 @@ class ManyToManyField(RelatedField):
|
|
1059
1071
|
)
|
1060
1072
|
)
|
1061
1073
|
else:
|
1062
|
-
|
1074
|
+
fix = ""
|
1063
1075
|
|
1064
1076
|
try:
|
1065
1077
|
field = through._meta.get_field(field_name)
|
1066
1078
|
except exceptions.FieldDoesNotExist:
|
1067
1079
|
errors.append(
|
1068
|
-
|
1069
|
-
f"The intermediary model '{qualified_model_name}' has no field '{field_name}'.",
|
1070
|
-
hint=hint,
|
1080
|
+
PreflightResult(
|
1081
|
+
fix=f"The intermediary model '{qualified_model_name}' has no field '{field_name}'. {fix}",
|
1071
1082
|
obj=self,
|
1072
|
-
id="fields.
|
1083
|
+
id="fields.m2m_through_field_not_found",
|
1073
1084
|
)
|
1074
1085
|
)
|
1075
1086
|
else:
|
@@ -1079,11 +1090,10 @@ class ManyToManyField(RelatedField):
|
|
1079
1090
|
== related_model
|
1080
1091
|
):
|
1081
1092
|
errors.append(
|
1082
|
-
|
1083
|
-
f"'{through._meta.object_name}.{field_name}' is not a foreign key to '{related_model._meta.object_name}'.",
|
1084
|
-
hint=hint,
|
1093
|
+
PreflightResult(
|
1094
|
+
fix=f"'{through._meta.object_name}.{field_name}' is not a foreign key to '{related_model._meta.object_name}'. {fix}",
|
1085
1095
|
obj=self,
|
1086
|
-
id="fields.
|
1096
|
+
id="fields.m2m_through_field_not_fk_to_model",
|
1087
1097
|
)
|
1088
1098
|
)
|
1089
1099
|
|
@@ -1108,12 +1118,14 @@ class ManyToManyField(RelatedField):
|
|
1108
1118
|
):
|
1109
1119
|
clashing_obj = model._meta.label
|
1110
1120
|
return [
|
1111
|
-
|
1112
|
-
|
1113
|
-
|
1121
|
+
PreflightResult(
|
1122
|
+
fix=(
|
1123
|
+
f"The field's intermediary table '{m2m_db_table}' clashes with the "
|
1124
|
+
f"table name of '{clashing_obj}'. "
|
1125
|
+
"Change the through model's db_table or use a different model."
|
1126
|
+
),
|
1114
1127
|
obj=self,
|
1115
|
-
|
1116
|
-
id="fields.E340",
|
1128
|
+
id="fields.m2m_table_name_clash",
|
1117
1129
|
)
|
1118
1130
|
]
|
1119
1131
|
return []
|
plain/models/migrations/state.py
CHANGED
@@ -521,7 +521,7 @@ class StateModelsRegistry(ModelsRegistry):
|
|
521
521
|
from plain.models.preflight import _check_lazy_references
|
522
522
|
|
523
523
|
if errors := _check_lazy_references(self, packages_registry):
|
524
|
-
raise ValueError("\n".join(error.
|
524
|
+
raise ValueError("\n".join(error.message for error in errors))
|
525
525
|
|
526
526
|
@contextmanager
|
527
527
|
def bulk_update(self):
|