test_state.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. from django.apps.registry import Apps
  2. from django.db import models
  3. from django.db.migrations.state import ProjectState, ModelState, InvalidBasesError
  4. from django.test import TestCase
  5. from .models import (FoodManager, FoodQuerySet, ModelWithCustomBase,
  6. NoMigrationFoodManager)
  7. class StateTests(TestCase):
  8. """
  9. Tests state construction, rendering and modification by operations.
  10. """
  11. def test_create(self):
  12. """
  13. Tests making a ProjectState from an Apps
  14. """
  15. new_apps = Apps(["migrations"])
  16. class Author(models.Model):
  17. name = models.CharField(max_length=255)
  18. bio = models.TextField()
  19. age = models.IntegerField(blank=True, null=True)
  20. class Meta:
  21. app_label = "migrations"
  22. apps = new_apps
  23. unique_together = ["name", "bio"]
  24. index_together = ["bio", "age"]
  25. class AuthorProxy(Author):
  26. class Meta:
  27. app_label = "migrations"
  28. apps = new_apps
  29. proxy = True
  30. ordering = ["name"]
  31. class SubAuthor(Author):
  32. width = models.FloatField(null=True)
  33. class Meta:
  34. app_label = "migrations"
  35. apps = new_apps
  36. class Book(models.Model):
  37. title = models.CharField(max_length=1000)
  38. author = models.ForeignKey(Author)
  39. contributors = models.ManyToManyField(Author)
  40. class Meta:
  41. app_label = "migrations"
  42. apps = new_apps
  43. verbose_name = "tome"
  44. db_table = "test_tome"
  45. class Food(models.Model):
  46. food_mgr = FoodManager('a', 'b')
  47. food_qs = FoodQuerySet.as_manager()
  48. food_no_mgr = NoMigrationFoodManager('x', 'y')
  49. class Meta:
  50. app_label = "migrations"
  51. apps = new_apps
  52. class FoodNoManagers(models.Model):
  53. class Meta:
  54. app_label = "migrations"
  55. apps = new_apps
  56. class FoodNoDefaultManager(models.Model):
  57. food_no_mgr = NoMigrationFoodManager('x', 'y')
  58. food_mgr = FoodManager('a', 'b')
  59. food_qs = FoodQuerySet.as_manager()
  60. class Meta:
  61. app_label = "migrations"
  62. apps = new_apps
  63. mgr1 = FoodManager('a', 'b')
  64. mgr2 = FoodManager('x', 'y', c=3, d=4)
  65. class FoodOrderedManagers(models.Model):
  66. # The managers on this model should be orderd by their creation
  67. # counter and not by the order in model body
  68. food_no_mgr = NoMigrationFoodManager('x', 'y')
  69. food_mgr2 = mgr2
  70. food_mgr1 = mgr1
  71. class Meta:
  72. app_label = "migrations"
  73. apps = new_apps
  74. project_state = ProjectState.from_apps(new_apps)
  75. author_state = project_state.models['migrations', 'author']
  76. author_proxy_state = project_state.models['migrations', 'authorproxy']
  77. sub_author_state = project_state.models['migrations', 'subauthor']
  78. book_state = project_state.models['migrations', 'book']
  79. food_state = project_state.models['migrations', 'food']
  80. food_no_managers_state = project_state.models['migrations', 'foodnomanagers']
  81. food_no_default_manager_state = project_state.models['migrations', 'foodnodefaultmanager']
  82. food_order_manager_state = project_state.models['migrations', 'foodorderedmanagers']
  83. self.assertEqual(author_state.app_label, "migrations")
  84. self.assertEqual(author_state.name, "Author")
  85. self.assertEqual([x for x, y in author_state.fields], ["id", "name", "bio", "age"])
  86. self.assertEqual(author_state.fields[1][1].max_length, 255)
  87. self.assertEqual(author_state.fields[2][1].null, False)
  88. self.assertEqual(author_state.fields[3][1].null, True)
  89. self.assertEqual(author_state.options, {"unique_together": {("name", "bio")}, "index_together": {("bio", "age")}})
  90. self.assertEqual(author_state.bases, (models.Model, ))
  91. self.assertEqual(book_state.app_label, "migrations")
  92. self.assertEqual(book_state.name, "Book")
  93. self.assertEqual([x for x, y in book_state.fields], ["id", "title", "author", "contributors"])
  94. self.assertEqual(book_state.fields[1][1].max_length, 1000)
  95. self.assertEqual(book_state.fields[2][1].null, False)
  96. self.assertEqual(book_state.fields[3][1].__class__.__name__, "ManyToManyField")
  97. self.assertEqual(book_state.options, {"verbose_name": "tome", "db_table": "test_tome"})
  98. self.assertEqual(book_state.bases, (models.Model, ))
  99. self.assertEqual(author_proxy_state.app_label, "migrations")
  100. self.assertEqual(author_proxy_state.name, "AuthorProxy")
  101. self.assertEqual(author_proxy_state.fields, [])
  102. self.assertEqual(author_proxy_state.options, {"proxy": True, "ordering": ["name"]})
  103. self.assertEqual(author_proxy_state.bases, ("migrations.author", ))
  104. self.assertEqual(sub_author_state.app_label, "migrations")
  105. self.assertEqual(sub_author_state.name, "SubAuthor")
  106. self.assertEqual(len(sub_author_state.fields), 2)
  107. self.assertEqual(sub_author_state.bases, ("migrations.author", ))
  108. # The default manager is used in migrations
  109. self.assertEqual([name for name, mgr in food_state.managers], ['food_mgr'])
  110. self.assertEqual(food_state.managers[0][1].args, ('a', 'b', 1, 2))
  111. # No explicit managers defined. Migrations will fall back to the default
  112. self.assertEqual(food_no_managers_state.managers, [])
  113. # food_mgr is used in migration but isn't the default mgr, hence add the
  114. # default
  115. self.assertEqual([name for name, mgr in food_no_default_manager_state.managers],
  116. ['food_no_mgr', 'food_mgr'])
  117. self.assertEqual(food_no_default_manager_state.managers[0][1].__class__, models.Manager)
  118. self.assertIsInstance(food_no_default_manager_state.managers[1][1], FoodManager)
  119. self.assertEqual([name for name, mgr in food_order_manager_state.managers],
  120. ['food_mgr1', 'food_mgr2'])
  121. self.assertEqual([mgr.args for name, mgr in food_order_manager_state.managers],
  122. [('a', 'b', 1, 2), ('x', 'y', 3, 4)])
  123. def test_render(self):
  124. """
  125. Tests rendering a ProjectState into an Apps.
  126. """
  127. project_state = ProjectState()
  128. project_state.add_model(ModelState(
  129. app_label="migrations",
  130. name="Tag",
  131. fields=[
  132. ("id", models.AutoField(primary_key=True)),
  133. ("name", models.CharField(max_length=100)),
  134. ("hidden", models.BooleanField()),
  135. ],
  136. ))
  137. project_state.add_model(ModelState(
  138. app_label="migrations",
  139. name="SubTag",
  140. fields=[
  141. ('tag_ptr', models.OneToOneField(
  142. auto_created=True,
  143. primary_key=True,
  144. to_field='id',
  145. serialize=False,
  146. to='migrations.Tag',
  147. )),
  148. ("awesome", models.BooleanField()),
  149. ],
  150. bases=("migrations.Tag",),
  151. ))
  152. base_mgr = models.Manager()
  153. mgr1 = FoodManager('a', 'b')
  154. mgr2 = FoodManager('x', 'y', c=3, d=4)
  155. project_state.add_model(ModelState(
  156. app_label="migrations",
  157. name="Food",
  158. fields=[
  159. ("id", models.AutoField(primary_key=True)),
  160. ],
  161. managers=[
  162. # The ordering we really want is objects, mgr1, mgr2
  163. ('default', base_mgr),
  164. ('food_mgr2', mgr2),
  165. ('food_mgr1', mgr1),
  166. ]
  167. ))
  168. new_apps = project_state.apps
  169. self.assertEqual(new_apps.get_model("migrations", "Tag")._meta.get_field("name").max_length, 100)
  170. self.assertEqual(new_apps.get_model("migrations", "Tag")._meta.get_field("hidden").null, False)
  171. self.assertEqual(len(new_apps.get_model("migrations", "SubTag")._meta.local_fields), 2)
  172. Food = new_apps.get_model("migrations", "Food")
  173. managers = sorted(Food._meta.managers)
  174. self.assertEqual([mgr.name for _, mgr, _ in managers],
  175. ['default', 'food_mgr1', 'food_mgr2'])
  176. self.assertEqual([mgr.__class__ for _, mgr, _ in managers],
  177. [models.Manager, FoodManager, FoodManager])
  178. self.assertIs(managers[0][1], Food._default_manager)
  179. def test_render_model_inheritance(self):
  180. class Book(models.Model):
  181. title = models.CharField(max_length=1000)
  182. class Meta:
  183. app_label = "migrations"
  184. apps = Apps()
  185. class Novel(Book):
  186. class Meta:
  187. app_label = "migrations"
  188. apps = Apps()
  189. # First, test rendering individually
  190. apps = Apps(["migrations"])
  191. # We shouldn't be able to render yet
  192. ms = ModelState.from_model(Novel)
  193. with self.assertRaises(InvalidBasesError):
  194. ms.render(apps)
  195. # Once the parent model is in the app registry, it should be fine
  196. ModelState.from_model(Book).render(apps)
  197. ModelState.from_model(Novel).render(apps)
  198. def test_render_model_with_multiple_inheritance(self):
  199. class Foo(models.Model):
  200. class Meta:
  201. app_label = "migrations"
  202. apps = Apps()
  203. class Bar(models.Model):
  204. class Meta:
  205. app_label = "migrations"
  206. apps = Apps()
  207. class FooBar(Foo, Bar):
  208. class Meta:
  209. app_label = "migrations"
  210. apps = Apps()
  211. class AbstractSubFooBar(FooBar):
  212. class Meta:
  213. abstract = True
  214. apps = Apps()
  215. class SubFooBar(AbstractSubFooBar):
  216. class Meta:
  217. app_label = "migrations"
  218. apps = Apps()
  219. apps = Apps(["migrations"])
  220. # We shouldn't be able to render yet
  221. ms = ModelState.from_model(FooBar)
  222. with self.assertRaises(InvalidBasesError):
  223. ms.render(apps)
  224. # Once the parent models are in the app registry, it should be fine
  225. ModelState.from_model(Foo).render(apps)
  226. self.assertSequenceEqual(ModelState.from_model(Foo).bases, [models.Model])
  227. ModelState.from_model(Bar).render(apps)
  228. self.assertSequenceEqual(ModelState.from_model(Bar).bases, [models.Model])
  229. ModelState.from_model(FooBar).render(apps)
  230. self.assertSequenceEqual(ModelState.from_model(FooBar).bases, ['migrations.foo', 'migrations.bar'])
  231. ModelState.from_model(SubFooBar).render(apps)
  232. self.assertSequenceEqual(ModelState.from_model(SubFooBar).bases, ['migrations.foobar'])
  233. def test_render_project_dependencies(self):
  234. """
  235. Tests that the ProjectState render method correctly renders models
  236. to account for inter-model base dependencies.
  237. """
  238. new_apps = Apps()
  239. class A(models.Model):
  240. class Meta:
  241. app_label = "migrations"
  242. apps = new_apps
  243. class B(A):
  244. class Meta:
  245. app_label = "migrations"
  246. apps = new_apps
  247. class C(B):
  248. class Meta:
  249. app_label = "migrations"
  250. apps = new_apps
  251. class D(A):
  252. class Meta:
  253. app_label = "migrations"
  254. apps = new_apps
  255. class E(B):
  256. class Meta:
  257. app_label = "migrations"
  258. apps = new_apps
  259. proxy = True
  260. class F(D):
  261. class Meta:
  262. app_label = "migrations"
  263. apps = new_apps
  264. proxy = True
  265. # Make a ProjectState and render it
  266. project_state = ProjectState()
  267. project_state.add_model(ModelState.from_model(A))
  268. project_state.add_model(ModelState.from_model(B))
  269. project_state.add_model(ModelState.from_model(C))
  270. project_state.add_model(ModelState.from_model(D))
  271. project_state.add_model(ModelState.from_model(E))
  272. project_state.add_model(ModelState.from_model(F))
  273. final_apps = project_state.apps
  274. self.assertEqual(len(final_apps.get_models()), 6)
  275. # Now make an invalid ProjectState and make sure it fails
  276. project_state = ProjectState()
  277. project_state.add_model(ModelState.from_model(A))
  278. project_state.add_model(ModelState.from_model(B))
  279. project_state.add_model(ModelState.from_model(C))
  280. project_state.add_model(ModelState.from_model(F))
  281. with self.assertRaises(InvalidBasesError):
  282. project_state.apps
  283. def test_render_unique_app_labels(self):
  284. """
  285. Tests that the ProjectState render method doesn't raise an
  286. ImproperlyConfigured exception about unique labels if two dotted app
  287. names have the same last part.
  288. """
  289. class A(models.Model):
  290. class Meta:
  291. app_label = "django.contrib.auth"
  292. class B(models.Model):
  293. class Meta:
  294. app_label = "vendor.auth"
  295. # Make a ProjectState and render it
  296. project_state = ProjectState()
  297. project_state.add_model(ModelState.from_model(A))
  298. project_state.add_model(ModelState.from_model(B))
  299. self.assertEqual(len(project_state.apps.get_models()), 2)
  300. def test_equality(self):
  301. """
  302. Tests that == and != are implemented correctly.
  303. """
  304. # Test two things that should be equal
  305. project_state = ProjectState()
  306. project_state.add_model(ModelState(
  307. "migrations",
  308. "Tag",
  309. [
  310. ("id", models.AutoField(primary_key=True)),
  311. ("name", models.CharField(max_length=100)),
  312. ("hidden", models.BooleanField()),
  313. ],
  314. {},
  315. None,
  316. ))
  317. other_state = project_state.clone()
  318. self.assertEqual(project_state, project_state)
  319. self.assertEqual(project_state, other_state)
  320. self.assertEqual(project_state != project_state, False)
  321. self.assertEqual(project_state != other_state, False)
  322. # Make a very small change (max_len 99) and see if that affects it
  323. project_state = ProjectState()
  324. project_state.add_model(ModelState(
  325. "migrations",
  326. "Tag",
  327. [
  328. ("id", models.AutoField(primary_key=True)),
  329. ("name", models.CharField(max_length=99)),
  330. ("hidden", models.BooleanField()),
  331. ],
  332. {},
  333. None,
  334. ))
  335. self.assertNotEqual(project_state, other_state)
  336. self.assertEqual(project_state == other_state, False)
  337. def test_dangling_references_throw_error(self):
  338. new_apps = Apps()
  339. class Author(models.Model):
  340. name = models.TextField()
  341. class Meta:
  342. app_label = "migrations"
  343. apps = new_apps
  344. class Book(models.Model):
  345. author = models.ForeignKey(Author)
  346. class Meta:
  347. app_label = "migrations"
  348. apps = new_apps
  349. class Magazine(models.Model):
  350. authors = models.ManyToManyField(Author)
  351. class Meta:
  352. app_label = "migrations"
  353. apps = new_apps
  354. # Make a valid ProjectState and render it
  355. project_state = ProjectState()
  356. project_state.add_model(ModelState.from_model(Author))
  357. project_state.add_model(ModelState.from_model(Book))
  358. project_state.add_model(ModelState.from_model(Magazine))
  359. self.assertEqual(len(project_state.apps.get_models()), 3)
  360. # now make an invalid one with a ForeignKey
  361. project_state = ProjectState()
  362. project_state.add_model(ModelState.from_model(Book))
  363. with self.assertRaises(ValueError):
  364. project_state.apps
  365. # and another with ManyToManyField
  366. project_state = ProjectState()
  367. project_state.add_model(ModelState.from_model(Magazine))
  368. with self.assertRaises(ValueError):
  369. project_state.apps
  370. def test_real_apps(self):
  371. """
  372. Tests that including real apps can resolve dangling FK errors.
  373. This test relies on the fact that contenttypes is always loaded.
  374. """
  375. new_apps = Apps()
  376. class TestModel(models.Model):
  377. ct = models.ForeignKey("contenttypes.ContentType")
  378. class Meta:
  379. app_label = "migrations"
  380. apps = new_apps
  381. # If we just stick it into an empty state it should fail
  382. project_state = ProjectState()
  383. project_state.add_model(ModelState.from_model(TestModel))
  384. with self.assertRaises(ValueError):
  385. project_state.apps
  386. # If we include the real app it should succeed
  387. project_state = ProjectState(real_apps=["contenttypes"])
  388. project_state.add_model(ModelState.from_model(TestModel))
  389. rendered_state = project_state.apps
  390. self.assertEqual(
  391. len([x for x in rendered_state.get_models() if x._meta.app_label == "migrations"]),
  392. 1,
  393. )
  394. def test_ignore_order_wrt(self):
  395. """
  396. Makes sure ProjectState doesn't include OrderWrt fields when
  397. making from existing models.
  398. """
  399. new_apps = Apps()
  400. class Author(models.Model):
  401. name = models.TextField()
  402. class Meta:
  403. app_label = "migrations"
  404. apps = new_apps
  405. class Book(models.Model):
  406. author = models.ForeignKey(Author)
  407. class Meta:
  408. app_label = "migrations"
  409. apps = new_apps
  410. order_with_respect_to = "author"
  411. # Make a valid ProjectState and render it
  412. project_state = ProjectState()
  413. project_state.add_model(ModelState.from_model(Author))
  414. project_state.add_model(ModelState.from_model(Book))
  415. self.assertEqual(
  416. [name for name, field in project_state.models["migrations", "book"].fields],
  417. ["id", "author"],
  418. )
  419. class ModelStateTests(TestCase):
  420. def test_custom_model_base(self):
  421. state = ModelState.from_model(ModelWithCustomBase)
  422. self.assertEqual(state.bases, (models.Model,))
  423. def test_bound_field_sanity_check(self):
  424. field = models.CharField(max_length=1)
  425. field.model = models.Model
  426. with self.assertRaisesMessage(ValueError,
  427. 'ModelState.fields cannot be bound to a model - "field" is.'):
  428. ModelState('app', 'Model', [('field', field)])
  429. def test_fields_immutability(self):
  430. """
  431. Tests that rendering a model state doesn't alter its internal fields.
  432. """
  433. apps = Apps()
  434. field = models.CharField(max_length=1)
  435. state = ModelState('app', 'Model', [('name', field)])
  436. Model = state.render(apps)
  437. self.assertNotEqual(Model._meta.get_field('name'), field)
  438. def test_repr(self):
  439. field = models.CharField(max_length=1)
  440. state = ModelState('app', 'Model', [('name', field)], bases=['app.A', 'app.B', 'app.C'])
  441. self.assertEqual(repr(state), "<ModelState: 'app.Model'>")
  442. project_state = ProjectState()
  443. project_state.add_model(state)
  444. with self.assertRaisesMessage(InvalidBasesError, "Cannot resolve bases for [<ModelState: 'app.Model'>]"):
  445. project_state.apps