test_models.py 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019
  1. import unittest
  2. from django.conf import settings
  3. from django.core.checks import Error, Warning
  4. from django.core.checks.model_checks import _check_lazy_references
  5. from django.core.exceptions import ImproperlyConfigured
  6. from django.db import connection, connections, models
  7. from django.db.models.signals import post_init
  8. from django.test import SimpleTestCase
  9. from django.test.utils import isolate_apps, override_settings
  10. def get_max_column_name_length():
  11. allowed_len = None
  12. db_alias = None
  13. for db in settings.DATABASES:
  14. connection = connections[db]
  15. max_name_length = connection.ops.max_name_length()
  16. if max_name_length is not None and not connection.features.truncates_names:
  17. if allowed_len is None or max_name_length < allowed_len:
  18. allowed_len = max_name_length
  19. db_alias = db
  20. return (allowed_len, db_alias)
  21. @isolate_apps('invalid_models_tests')
  22. class IndexTogetherTests(SimpleTestCase):
  23. def test_non_iterable(self):
  24. class Model(models.Model):
  25. class Meta:
  26. index_together = 42
  27. self.assertEqual(Model.check(), [
  28. Error(
  29. "'index_together' must be a list or tuple.",
  30. obj=Model,
  31. id='models.E008',
  32. ),
  33. ])
  34. def test_non_list(self):
  35. class Model(models.Model):
  36. class Meta:
  37. index_together = 'not-a-list'
  38. self.assertEqual(Model.check(), [
  39. Error(
  40. "'index_together' must be a list or tuple.",
  41. obj=Model,
  42. id='models.E008',
  43. ),
  44. ])
  45. def test_list_containing_non_iterable(self):
  46. class Model(models.Model):
  47. class Meta:
  48. index_together = [('a', 'b'), 42]
  49. self.assertEqual(Model.check(), [
  50. Error(
  51. "All 'index_together' elements must be lists or tuples.",
  52. obj=Model,
  53. id='models.E009',
  54. ),
  55. ])
  56. def test_pointing_to_missing_field(self):
  57. class Model(models.Model):
  58. class Meta:
  59. index_together = [['missing_field']]
  60. self.assertEqual(Model.check(), [
  61. Error(
  62. "'index_together' refers to the nonexistent field 'missing_field'.",
  63. obj=Model,
  64. id='models.E012',
  65. ),
  66. ])
  67. def test_pointing_to_non_local_field(self):
  68. class Foo(models.Model):
  69. field1 = models.IntegerField()
  70. class Bar(Foo):
  71. field2 = models.IntegerField()
  72. class Meta:
  73. index_together = [['field2', 'field1']]
  74. self.assertEqual(Bar.check(), [
  75. Error(
  76. "'index_together' refers to field 'field1' which is not "
  77. "local to model 'Bar'.",
  78. hint='This issue may be caused by multi-table inheritance.',
  79. obj=Bar,
  80. id='models.E016',
  81. ),
  82. ])
  83. def test_pointing_to_m2m_field(self):
  84. class Model(models.Model):
  85. m2m = models.ManyToManyField('self')
  86. class Meta:
  87. index_together = [['m2m']]
  88. self.assertEqual(Model.check(), [
  89. Error(
  90. "'index_together' refers to a ManyToManyField 'm2m', but "
  91. "ManyToManyFields are not permitted in 'index_together'.",
  92. obj=Model,
  93. id='models.E013',
  94. ),
  95. ])
  96. # unique_together tests are very similar to index_together tests.
  97. @isolate_apps('invalid_models_tests')
  98. class UniqueTogetherTests(SimpleTestCase):
  99. def test_non_iterable(self):
  100. class Model(models.Model):
  101. class Meta:
  102. unique_together = 42
  103. self.assertEqual(Model.check(), [
  104. Error(
  105. "'unique_together' must be a list or tuple.",
  106. obj=Model,
  107. id='models.E010',
  108. ),
  109. ])
  110. def test_list_containing_non_iterable(self):
  111. class Model(models.Model):
  112. one = models.IntegerField()
  113. two = models.IntegerField()
  114. class Meta:
  115. unique_together = [('a', 'b'), 42]
  116. self.assertEqual(Model.check(), [
  117. Error(
  118. "All 'unique_together' elements must be lists or tuples.",
  119. obj=Model,
  120. id='models.E011',
  121. ),
  122. ])
  123. def test_non_list(self):
  124. class Model(models.Model):
  125. class Meta:
  126. unique_together = 'not-a-list'
  127. self.assertEqual(Model.check(), [
  128. Error(
  129. "'unique_together' must be a list or tuple.",
  130. obj=Model,
  131. id='models.E010',
  132. ),
  133. ])
  134. def test_valid_model(self):
  135. class Model(models.Model):
  136. one = models.IntegerField()
  137. two = models.IntegerField()
  138. class Meta:
  139. # unique_together can be a simple tuple
  140. unique_together = ('one', 'two')
  141. self.assertEqual(Model.check(), [])
  142. def test_pointing_to_missing_field(self):
  143. class Model(models.Model):
  144. class Meta:
  145. unique_together = [['missing_field']]
  146. self.assertEqual(Model.check(), [
  147. Error(
  148. "'unique_together' refers to the nonexistent field 'missing_field'.",
  149. obj=Model,
  150. id='models.E012',
  151. ),
  152. ])
  153. def test_pointing_to_m2m(self):
  154. class Model(models.Model):
  155. m2m = models.ManyToManyField('self')
  156. class Meta:
  157. unique_together = [['m2m']]
  158. self.assertEqual(Model.check(), [
  159. Error(
  160. "'unique_together' refers to a ManyToManyField 'm2m', but "
  161. "ManyToManyFields are not permitted in 'unique_together'.",
  162. obj=Model,
  163. id='models.E013',
  164. ),
  165. ])
  166. @isolate_apps('invalid_models_tests')
  167. class IndexesTests(SimpleTestCase):
  168. def test_pointing_to_missing_field(self):
  169. class Model(models.Model):
  170. class Meta:
  171. indexes = [models.Index(fields=['missing_field'], name='name')]
  172. self.assertEqual(Model.check(), [
  173. Error(
  174. "'indexes' refers to the nonexistent field 'missing_field'.",
  175. obj=Model,
  176. id='models.E012',
  177. ),
  178. ])
  179. def test_pointing_to_m2m_field(self):
  180. class Model(models.Model):
  181. m2m = models.ManyToManyField('self')
  182. class Meta:
  183. indexes = [models.Index(fields=['m2m'], name='name')]
  184. self.assertEqual(Model.check(), [
  185. Error(
  186. "'indexes' refers to a ManyToManyField 'm2m', but "
  187. "ManyToManyFields are not permitted in 'indexes'.",
  188. obj=Model,
  189. id='models.E013',
  190. ),
  191. ])
  192. def test_pointing_to_non_local_field(self):
  193. class Foo(models.Model):
  194. field1 = models.IntegerField()
  195. class Bar(Foo):
  196. field2 = models.IntegerField()
  197. class Meta:
  198. indexes = [models.Index(fields=['field2', 'field1'], name='name')]
  199. self.assertEqual(Bar.check(), [
  200. Error(
  201. "'indexes' refers to field 'field1' which is not local to "
  202. "model 'Bar'.",
  203. hint='This issue may be caused by multi-table inheritance.',
  204. obj=Bar,
  205. id='models.E016',
  206. ),
  207. ])
  208. @isolate_apps('invalid_models_tests')
  209. class FieldNamesTests(SimpleTestCase):
  210. def test_ending_with_underscore(self):
  211. class Model(models.Model):
  212. field_ = models.CharField(max_length=10)
  213. m2m_ = models.ManyToManyField('self')
  214. self.assertEqual(Model.check(), [
  215. Error(
  216. 'Field names must not end with an underscore.',
  217. obj=Model._meta.get_field('field_'),
  218. id='fields.E001',
  219. ),
  220. Error(
  221. 'Field names must not end with an underscore.',
  222. obj=Model._meta.get_field('m2m_'),
  223. id='fields.E001',
  224. ),
  225. ])
  226. max_column_name_length, column_limit_db_alias = get_max_column_name_length()
  227. @unittest.skipIf(max_column_name_length is None, "The database doesn't have a column name length limit.")
  228. def test_M2M_long_column_name(self):
  229. """
  230. #13711 -- Model check for long M2M column names when database has
  231. column name length limits.
  232. """
  233. allowed_len, db_alias = get_max_column_name_length()
  234. # A model with very long name which will be used to set relations to.
  235. class VeryLongModelNamezzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz(models.Model):
  236. title = models.CharField(max_length=11)
  237. # Main model for which checks will be performed.
  238. class ModelWithLongField(models.Model):
  239. m2m_field = models.ManyToManyField(
  240. VeryLongModelNamezzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,
  241. related_name='rn1',
  242. )
  243. m2m_field2 = models.ManyToManyField(
  244. VeryLongModelNamezzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,
  245. related_name='rn2', through='m2msimple',
  246. )
  247. m2m_field3 = models.ManyToManyField(
  248. VeryLongModelNamezzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,
  249. related_name='rn3',
  250. through='m2mcomplex',
  251. )
  252. fk = models.ForeignKey(
  253. VeryLongModelNamezzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,
  254. models.CASCADE,
  255. related_name='rn4',
  256. )
  257. # Models used for setting `through` in M2M field.
  258. class m2msimple(models.Model):
  259. id2 = models.ForeignKey(ModelWithLongField, models.CASCADE)
  260. class m2mcomplex(models.Model):
  261. id2 = models.ForeignKey(ModelWithLongField, models.CASCADE)
  262. long_field_name = 'a' * (self.max_column_name_length + 1)
  263. models.ForeignKey(
  264. VeryLongModelNamezzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,
  265. models.CASCADE,
  266. ).contribute_to_class(m2msimple, long_field_name)
  267. models.ForeignKey(
  268. VeryLongModelNamezzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,
  269. models.CASCADE,
  270. db_column=long_field_name
  271. ).contribute_to_class(m2mcomplex, long_field_name)
  272. errors = ModelWithLongField.check()
  273. # First error because of M2M field set on the model with long name.
  274. m2m_long_name = "verylongmodelnamezzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz_id"
  275. if self.max_column_name_length > len(m2m_long_name):
  276. # Some databases support names longer than the test name.
  277. expected = []
  278. else:
  279. expected = [
  280. Error(
  281. 'Autogenerated column name too long for M2M field "%s". '
  282. 'Maximum length is "%s" for database "%s".'
  283. % (m2m_long_name, self.max_column_name_length, self.column_limit_db_alias),
  284. hint="Use 'through' to create a separate model for "
  285. "M2M and then set column_name using 'db_column'.",
  286. obj=ModelWithLongField,
  287. id='models.E019',
  288. )
  289. ]
  290. # Second error because the FK specified in the `through` model
  291. # `m2msimple` has auto-generated name longer than allowed.
  292. # There will be no check errors in the other M2M because it
  293. # specifies db_column for the FK in `through` model even if the actual
  294. # name is longer than the limits of the database.
  295. expected.append(
  296. Error(
  297. 'Autogenerated column name too long for M2M field "%s_id". '
  298. 'Maximum length is "%s" for database "%s".'
  299. % (long_field_name, self.max_column_name_length, self.column_limit_db_alias),
  300. hint="Use 'through' to create a separate model for "
  301. "M2M and then set column_name using 'db_column'.",
  302. obj=ModelWithLongField,
  303. id='models.E019',
  304. )
  305. )
  306. self.assertEqual(errors, expected)
  307. @unittest.skipIf(max_column_name_length is None, "The database doesn't have a column name length limit.")
  308. def test_local_field_long_column_name(self):
  309. """
  310. #13711 -- Model check for long column names
  311. when database does not support long names.
  312. """
  313. allowed_len, db_alias = get_max_column_name_length()
  314. class ModelWithLongField(models.Model):
  315. title = models.CharField(max_length=11)
  316. long_field_name = 'a' * (self.max_column_name_length + 1)
  317. long_field_name2 = 'b' * (self.max_column_name_length + 1)
  318. models.CharField(max_length=11).contribute_to_class(ModelWithLongField, long_field_name)
  319. models.CharField(max_length=11, db_column='vlmn').contribute_to_class(ModelWithLongField, long_field_name2)
  320. self.assertEqual(ModelWithLongField.check(), [
  321. Error(
  322. 'Autogenerated column name too long for field "%s". '
  323. 'Maximum length is "%s" for database "%s".'
  324. % (long_field_name, self.max_column_name_length, self.column_limit_db_alias),
  325. hint="Set the column name manually using 'db_column'.",
  326. obj=ModelWithLongField,
  327. id='models.E018',
  328. )
  329. ])
  330. def test_including_separator(self):
  331. class Model(models.Model):
  332. some__field = models.IntegerField()
  333. self.assertEqual(Model.check(), [
  334. Error(
  335. 'Field names must not contain "__".',
  336. obj=Model._meta.get_field('some__field'),
  337. id='fields.E002',
  338. )
  339. ])
  340. def test_pk(self):
  341. class Model(models.Model):
  342. pk = models.IntegerField()
  343. self.assertEqual(Model.check(), [
  344. Error(
  345. "'pk' is a reserved word that cannot be used as a field name.",
  346. obj=Model._meta.get_field('pk'),
  347. id='fields.E003',
  348. )
  349. ])
  350. @isolate_apps('invalid_models_tests')
  351. class ShadowingFieldsTests(SimpleTestCase):
  352. def test_field_name_clash_with_child_accessor(self):
  353. class Parent(models.Model):
  354. pass
  355. class Child(Parent):
  356. child = models.CharField(max_length=100)
  357. self.assertEqual(Child.check(), [
  358. Error(
  359. "The field 'child' clashes with the field "
  360. "'child' from model 'invalid_models_tests.parent'.",
  361. obj=Child._meta.get_field('child'),
  362. id='models.E006',
  363. )
  364. ])
  365. def test_multiinheritance_clash(self):
  366. class Mother(models.Model):
  367. clash = models.IntegerField()
  368. class Father(models.Model):
  369. clash = models.IntegerField()
  370. class Child(Mother, Father):
  371. # Here we have two clashed: id (automatic field) and clash, because
  372. # both parents define these fields.
  373. pass
  374. self.assertEqual(Child.check(), [
  375. Error(
  376. "The field 'id' from parent model "
  377. "'invalid_models_tests.mother' clashes with the field 'id' "
  378. "from parent model 'invalid_models_tests.father'.",
  379. obj=Child,
  380. id='models.E005',
  381. ),
  382. Error(
  383. "The field 'clash' from parent model "
  384. "'invalid_models_tests.mother' clashes with the field 'clash' "
  385. "from parent model 'invalid_models_tests.father'.",
  386. obj=Child,
  387. id='models.E005',
  388. )
  389. ])
  390. def test_inheritance_clash(self):
  391. class Parent(models.Model):
  392. f_id = models.IntegerField()
  393. class Target(models.Model):
  394. # This field doesn't result in a clash.
  395. f_id = models.IntegerField()
  396. class Child(Parent):
  397. # This field clashes with parent "f_id" field.
  398. f = models.ForeignKey(Target, models.CASCADE)
  399. self.assertEqual(Child.check(), [
  400. Error(
  401. "The field 'f' clashes with the field 'f_id' "
  402. "from model 'invalid_models_tests.parent'.",
  403. obj=Child._meta.get_field('f'),
  404. id='models.E006',
  405. )
  406. ])
  407. def test_multigeneration_inheritance(self):
  408. class GrandParent(models.Model):
  409. clash = models.IntegerField()
  410. class Parent(GrandParent):
  411. pass
  412. class Child(Parent):
  413. pass
  414. class GrandChild(Child):
  415. clash = models.IntegerField()
  416. self.assertEqual(GrandChild.check(), [
  417. Error(
  418. "The field 'clash' clashes with the field 'clash' "
  419. "from model 'invalid_models_tests.grandparent'.",
  420. obj=GrandChild._meta.get_field('clash'),
  421. id='models.E006',
  422. )
  423. ])
  424. def test_id_clash(self):
  425. class Target(models.Model):
  426. pass
  427. class Model(models.Model):
  428. fk = models.ForeignKey(Target, models.CASCADE)
  429. fk_id = models.IntegerField()
  430. self.assertEqual(Model.check(), [
  431. Error(
  432. "The field 'fk_id' clashes with the field 'fk' from model "
  433. "'invalid_models_tests.model'.",
  434. obj=Model._meta.get_field('fk_id'),
  435. id='models.E006',
  436. )
  437. ])
  438. @isolate_apps('invalid_models_tests')
  439. class OtherModelTests(SimpleTestCase):
  440. def test_unique_primary_key(self):
  441. invalid_id = models.IntegerField(primary_key=False)
  442. class Model(models.Model):
  443. id = invalid_id
  444. self.assertEqual(Model.check(), [
  445. Error(
  446. "'id' can only be used as a field name if the field also sets "
  447. "'primary_key=True'.",
  448. obj=Model,
  449. id='models.E004',
  450. ),
  451. ])
  452. def test_ordering_non_iterable(self):
  453. class Model(models.Model):
  454. class Meta:
  455. ordering = 'missing_field'
  456. self.assertEqual(Model.check(), [
  457. Error(
  458. "'ordering' must be a tuple or list "
  459. "(even if you want to order by only one field).",
  460. obj=Model,
  461. id='models.E014',
  462. ),
  463. ])
  464. def test_just_ordering_no_errors(self):
  465. class Model(models.Model):
  466. order = models.PositiveIntegerField()
  467. class Meta:
  468. ordering = ['order']
  469. self.assertEqual(Model.check(), [])
  470. def test_just_order_with_respect_to_no_errors(self):
  471. class Question(models.Model):
  472. pass
  473. class Answer(models.Model):
  474. question = models.ForeignKey(Question, models.CASCADE)
  475. class Meta:
  476. order_with_respect_to = 'question'
  477. self.assertEqual(Answer.check(), [])
  478. def test_ordering_with_order_with_respect_to(self):
  479. class Question(models.Model):
  480. pass
  481. class Answer(models.Model):
  482. question = models.ForeignKey(Question, models.CASCADE)
  483. order = models.IntegerField()
  484. class Meta:
  485. order_with_respect_to = 'question'
  486. ordering = ['order']
  487. self.assertEqual(Answer.check(), [
  488. Error(
  489. "'ordering' and 'order_with_respect_to' cannot be used together.",
  490. obj=Answer,
  491. id='models.E021',
  492. ),
  493. ])
  494. def test_non_valid(self):
  495. class RelationModel(models.Model):
  496. pass
  497. class Model(models.Model):
  498. relation = models.ManyToManyField(RelationModel)
  499. class Meta:
  500. ordering = ['relation']
  501. self.assertEqual(Model.check(), [
  502. Error(
  503. "'ordering' refers to the nonexistent field 'relation'.",
  504. obj=Model,
  505. id='models.E015',
  506. ),
  507. ])
  508. def test_ordering_pointing_to_missing_field(self):
  509. class Model(models.Model):
  510. class Meta:
  511. ordering = ('missing_field',)
  512. self.assertEqual(Model.check(), [
  513. Error(
  514. "'ordering' refers to the nonexistent field 'missing_field'.",
  515. obj=Model,
  516. id='models.E015',
  517. )
  518. ])
  519. def test_ordering_pointing_to_missing_foreignkey_field(self):
  520. class Model(models.Model):
  521. missing_fk_field = models.IntegerField()
  522. class Meta:
  523. ordering = ('missing_fk_field_id',)
  524. self.assertEqual(Model.check(), [
  525. Error(
  526. "'ordering' refers to the nonexistent field 'missing_fk_field_id'.",
  527. obj=Model,
  528. id='models.E015',
  529. )
  530. ])
  531. def test_ordering_pointing_to_foreignkey_field(self):
  532. class Parent(models.Model):
  533. pass
  534. class Child(models.Model):
  535. parent = models.ForeignKey(Parent, models.CASCADE)
  536. class Meta:
  537. ordering = ('parent_id',)
  538. self.assertFalse(Child.check())
  539. def test_name_beginning_with_underscore(self):
  540. class _Model(models.Model):
  541. pass
  542. self.assertEqual(_Model.check(), [
  543. Error(
  544. "The model name '_Model' cannot start or end with an underscore "
  545. "as it collides with the query lookup syntax.",
  546. obj=_Model,
  547. id='models.E023',
  548. )
  549. ])
  550. def test_name_ending_with_underscore(self):
  551. class Model_(models.Model):
  552. pass
  553. self.assertEqual(Model_.check(), [
  554. Error(
  555. "The model name 'Model_' cannot start or end with an underscore "
  556. "as it collides with the query lookup syntax.",
  557. obj=Model_,
  558. id='models.E023',
  559. )
  560. ])
  561. def test_name_contains_double_underscores(self):
  562. class Test__Model(models.Model):
  563. pass
  564. self.assertEqual(Test__Model.check(), [
  565. Error(
  566. "The model name 'Test__Model' cannot contain double underscores "
  567. "as it collides with the query lookup syntax.",
  568. obj=Test__Model,
  569. id='models.E024',
  570. )
  571. ])
  572. def test_property_and_related_field_accessor_clash(self):
  573. class Model(models.Model):
  574. fk = models.ForeignKey('self', models.CASCADE)
  575. @property
  576. def fk_id(self):
  577. pass
  578. self.assertEqual(Model.check(), [
  579. Error(
  580. "The property 'fk_id' clashes with a related field accessor.",
  581. obj=Model,
  582. id='models.E025',
  583. )
  584. ])
  585. def test_single_primary_key(self):
  586. class Model(models.Model):
  587. foo = models.IntegerField(primary_key=True)
  588. bar = models.IntegerField(primary_key=True)
  589. self.assertEqual(Model.check(), [
  590. Error(
  591. "The model cannot have more than one field with 'primary_key=True'.",
  592. obj=Model,
  593. id='models.E026',
  594. )
  595. ])
  596. @override_settings(TEST_SWAPPED_MODEL_BAD_VALUE='not-a-model')
  597. def test_swappable_missing_app_name(self):
  598. class Model(models.Model):
  599. class Meta:
  600. swappable = 'TEST_SWAPPED_MODEL_BAD_VALUE'
  601. self.assertEqual(Model.check(), [
  602. Error(
  603. "'TEST_SWAPPED_MODEL_BAD_VALUE' is not of the form 'app_label.app_name'.",
  604. id='models.E001',
  605. ),
  606. ])
  607. @override_settings(TEST_SWAPPED_MODEL_BAD_MODEL='not_an_app.Target')
  608. def test_swappable_missing_app(self):
  609. class Model(models.Model):
  610. class Meta:
  611. swappable = 'TEST_SWAPPED_MODEL_BAD_MODEL'
  612. self.assertEqual(Model.check(), [
  613. Error(
  614. "'TEST_SWAPPED_MODEL_BAD_MODEL' references 'not_an_app.Target', "
  615. 'which has not been installed, or is abstract.',
  616. id='models.E002',
  617. ),
  618. ])
  619. def test_two_m2m_through_same_relationship(self):
  620. class Person(models.Model):
  621. pass
  622. class Group(models.Model):
  623. primary = models.ManyToManyField(Person, through='Membership', related_name='primary')
  624. secondary = models.ManyToManyField(Person, through='Membership', related_name='secondary')
  625. class Membership(models.Model):
  626. person = models.ForeignKey(Person, models.CASCADE)
  627. group = models.ForeignKey(Group, models.CASCADE)
  628. self.assertEqual(Group.check(), [
  629. Error(
  630. "The model has two identical many-to-many relations through "
  631. "the intermediate model 'invalid_models_tests.Membership'.",
  632. obj=Group,
  633. id='models.E003',
  634. )
  635. ])
  636. def test_two_m2m_through_same_model_with_different_through_fields(self):
  637. class Country(models.Model):
  638. pass
  639. class ShippingMethod(models.Model):
  640. to_countries = models.ManyToManyField(
  641. Country, through='ShippingMethodPrice',
  642. through_fields=('method', 'to_country'),
  643. )
  644. from_countries = models.ManyToManyField(
  645. Country, through='ShippingMethodPrice',
  646. through_fields=('method', 'from_country'),
  647. related_name='+',
  648. )
  649. class ShippingMethodPrice(models.Model):
  650. method = models.ForeignKey(ShippingMethod, models.CASCADE)
  651. to_country = models.ForeignKey(Country, models.CASCADE)
  652. from_country = models.ForeignKey(Country, models.CASCADE)
  653. self.assertEqual(ShippingMethod.check(), [])
  654. def test_missing_parent_link(self):
  655. msg = 'Add parent_link=True to invalid_models_tests.ParkingLot.parent.'
  656. with self.assertRaisesMessage(ImproperlyConfigured, msg):
  657. class Place(models.Model):
  658. pass
  659. class ParkingLot(Place):
  660. parent = models.OneToOneField(Place, models.CASCADE)
  661. def test_m2m_table_name_clash(self):
  662. class Foo(models.Model):
  663. bar = models.ManyToManyField('Bar', db_table='myapp_bar')
  664. class Meta:
  665. db_table = 'myapp_foo'
  666. class Bar(models.Model):
  667. class Meta:
  668. db_table = 'myapp_bar'
  669. self.assertEqual(Foo.check(), [
  670. Error(
  671. "The field's intermediary table 'myapp_bar' clashes with the "
  672. "table name of 'invalid_models_tests.Bar'.",
  673. obj=Foo._meta.get_field('bar'),
  674. id='fields.E340',
  675. )
  676. ])
  677. def test_m2m_field_table_name_clash(self):
  678. class Foo(models.Model):
  679. pass
  680. class Bar(models.Model):
  681. foos = models.ManyToManyField(Foo, db_table='clash')
  682. class Baz(models.Model):
  683. foos = models.ManyToManyField(Foo, db_table='clash')
  684. self.assertEqual(Bar.check() + Baz.check(), [
  685. Error(
  686. "The field's intermediary table 'clash' clashes with the "
  687. "table name of 'invalid_models_tests.Baz.foos'.",
  688. obj=Bar._meta.get_field('foos'),
  689. id='fields.E340',
  690. ),
  691. Error(
  692. "The field's intermediary table 'clash' clashes with the "
  693. "table name of 'invalid_models_tests.Bar.foos'.",
  694. obj=Baz._meta.get_field('foos'),
  695. id='fields.E340',
  696. )
  697. ])
  698. def test_m2m_autogenerated_table_name_clash(self):
  699. class Foo(models.Model):
  700. class Meta:
  701. db_table = 'bar_foos'
  702. class Bar(models.Model):
  703. # The autogenerated `db_table` will be bar_foos.
  704. foos = models.ManyToManyField(Foo)
  705. class Meta:
  706. db_table = 'bar'
  707. self.assertEqual(Bar.check(), [
  708. Error(
  709. "The field's intermediary table 'bar_foos' clashes with the "
  710. "table name of 'invalid_models_tests.Foo'.",
  711. obj=Bar._meta.get_field('foos'),
  712. id='fields.E340',
  713. )
  714. ])
  715. def test_m2m_unmanaged_shadow_models_not_checked(self):
  716. class A1(models.Model):
  717. pass
  718. class C1(models.Model):
  719. mm_a = models.ManyToManyField(A1, db_table='d1')
  720. # Unmanaged models that shadow the above models. Reused table names
  721. # shouldn't be flagged by any checks.
  722. class A2(models.Model):
  723. class Meta:
  724. managed = False
  725. class C2(models.Model):
  726. mm_a = models.ManyToManyField(A2, through='Intermediate')
  727. class Meta:
  728. managed = False
  729. class Intermediate(models.Model):
  730. a2 = models.ForeignKey(A2, models.CASCADE, db_column='a1_id')
  731. c2 = models.ForeignKey(C2, models.CASCADE, db_column='c1_id')
  732. class Meta:
  733. db_table = 'd1'
  734. managed = False
  735. self.assertEqual(C1.check(), [])
  736. self.assertEqual(C2.check(), [])
  737. def test_m2m_to_concrete_and_proxy_allowed(self):
  738. class A(models.Model):
  739. pass
  740. class Through(models.Model):
  741. a = models.ForeignKey('A', models.CASCADE)
  742. c = models.ForeignKey('C', models.CASCADE)
  743. class ThroughProxy(Through):
  744. class Meta:
  745. proxy = True
  746. class C(models.Model):
  747. mm_a = models.ManyToManyField(A, through=Through)
  748. mm_aproxy = models.ManyToManyField(A, through=ThroughProxy, related_name='proxied_m2m')
  749. self.assertEqual(C.check(), [])
  750. @isolate_apps('django.contrib.auth', kwarg_name='apps')
  751. def test_lazy_reference_checks(self, apps):
  752. class DummyModel(models.Model):
  753. author = models.ForeignKey('Author', models.CASCADE)
  754. class Meta:
  755. app_label = 'invalid_models_tests'
  756. class DummyClass:
  757. def __call__(self, **kwargs):
  758. pass
  759. def dummy_method(self):
  760. pass
  761. def dummy_function(*args, **kwargs):
  762. pass
  763. apps.lazy_model_operation(dummy_function, ('auth', 'imaginarymodel'))
  764. apps.lazy_model_operation(dummy_function, ('fanciful_app', 'imaginarymodel'))
  765. post_init.connect(dummy_function, sender='missing-app.Model', apps=apps)
  766. post_init.connect(DummyClass(), sender='missing-app.Model', apps=apps)
  767. post_init.connect(DummyClass().dummy_method, sender='missing-app.Model', apps=apps)
  768. self.assertEqual(_check_lazy_references(apps), [
  769. Error(
  770. "%r contains a lazy reference to auth.imaginarymodel, "
  771. "but app 'auth' doesn't provide model 'imaginarymodel'." % dummy_function,
  772. obj=dummy_function,
  773. id='models.E022',
  774. ),
  775. Error(
  776. "%r contains a lazy reference to fanciful_app.imaginarymodel, "
  777. "but app 'fanciful_app' isn't installed." % dummy_function,
  778. obj=dummy_function,
  779. id='models.E022',
  780. ),
  781. Error(
  782. "An instance of class 'DummyClass' was connected to "
  783. "the 'post_init' signal with a lazy reference to the sender "
  784. "'missing-app.model', but app 'missing-app' isn't installed.",
  785. hint=None,
  786. obj='invalid_models_tests.test_models',
  787. id='signals.E001',
  788. ),
  789. Error(
  790. "Bound method 'DummyClass.dummy_method' was connected to the "
  791. "'post_init' signal with a lazy reference to the sender "
  792. "'missing-app.model', but app 'missing-app' isn't installed.",
  793. hint=None,
  794. obj='invalid_models_tests.test_models',
  795. id='signals.E001',
  796. ),
  797. Error(
  798. "The field invalid_models_tests.DummyModel.author was declared "
  799. "with a lazy reference to 'invalid_models_tests.author', but app "
  800. "'invalid_models_tests' isn't installed.",
  801. hint=None,
  802. obj=DummyModel.author.field,
  803. id='fields.E307',
  804. ),
  805. Error(
  806. "The function 'dummy_function' was connected to the 'post_init' "
  807. "signal with a lazy reference to the sender "
  808. "'missing-app.model', but app 'missing-app' isn't installed.",
  809. hint=None,
  810. obj='invalid_models_tests.test_models',
  811. id='signals.E001',
  812. ),
  813. ])
  814. @isolate_apps('invalid_models_tests')
  815. class ConstraintsTests(SimpleTestCase):
  816. def test_check_constraints(self):
  817. class Model(models.Model):
  818. age = models.IntegerField()
  819. class Meta:
  820. constraints = [models.CheckConstraint(check=models.Q(age__gte=18), name='is_adult')]
  821. errors = Model.check()
  822. warn = Warning(
  823. '%s does not support check constraints.' % connection.display_name,
  824. hint=(
  825. "A constraint won't be created. Silence this warning if you "
  826. "don't care about it."
  827. ),
  828. obj=Model,
  829. id='models.W027',
  830. )
  831. expected = [] if connection.features.supports_table_check_constraints else [warn, warn]
  832. self.assertCountEqual(errors, expected)