Fixed #24147 -- Prevented managers leaking model during migrations

Thanks Tim Graham for the review.
This commit is contained in:
Markus Holtermann 2015-01-13 18:18:23 +01:00
parent ec7ef5afbb
commit 88786afbff
2 changed files with 53 additions and 3 deletions

View File

@ -400,6 +400,19 @@ class ModelState(object):
field_class = import_string(path) field_class = import_string(path)
yield name, field_class(*args, **kwargs) yield name, field_class(*args, **kwargs)
def construct_managers(self):
"Deep-clone the managers using deconstruction"
# Sort all managers by their creation counter
sorted_managers = sorted(self.managers, key=lambda v: v[1].creation_counter)
for mgr_name, manager in sorted_managers:
as_manager, manager_path, qs_path, args, kwargs = manager.deconstruct()
if as_manager:
qs_class = import_string(qs_path)
yield mgr_name, qs_class.as_manager()
else:
manager_class = import_string(manager_path)
yield mgr_name, manager_class(*args, **kwargs)
def clone(self): def clone(self):
"Returns an exact copy of this ModelState" "Returns an exact copy of this ModelState"
return self.__class__( return self.__class__(
@ -408,7 +421,7 @@ class ModelState(object):
fields=list(self.construct_fields()), fields=list(self.construct_fields()),
options=dict(self.options), options=dict(self.options),
bases=self.bases, bases=self.bases,
managers=self.managers, managers=list(self.construct_managers()),
) )
def render(self, apps): def render(self, apps):
@ -431,8 +444,7 @@ class ModelState(object):
body['__module__'] = "__fake__" body['__module__'] = "__fake__"
# Restore managers # Restore managers
for mgr_name, manager in self.managers: body.update(self.construct_managers())
body[mgr_name] = manager
# Then, make a Model object (apps.register_model is called in __new__) # Then, make a Model object (apps.register_model is called in __new__)
return type( return type(

View File

@ -1,5 +1,6 @@
from django.apps.registry import Apps from django.apps.registry import Apps
from django.db import models from django.db import models
from django.db.migrations.operations import RemoveField
from django.db.migrations.state import ProjectState, ModelState, InvalidBasesError from django.db.migrations.state import ProjectState, ModelState, InvalidBasesError
from django.test import TestCase from django.test import TestCase
@ -505,6 +506,43 @@ class StateTests(TestCase):
["id", "author"], ["id", "author"],
) )
def test_manager_refer_correct_model_version(self):
"""
#24147 - Tests that managers refer to the correct version of a
historical model
"""
project_state = ProjectState()
project_state.add_model(ModelState(
app_label="migrations",
name="Tag",
fields=[
("id", models.AutoField(primary_key=True)),
("hidden", models.BooleanField()),
],
managers=[
('food_mgr', FoodManager('a', 'b')),
('food_qs', FoodQuerySet.as_manager()),
]
))
old_model = project_state.apps.get_model('migrations', 'tag')
new_state = project_state.clone()
operation = RemoveField("tag", "hidden")
operation.state_forwards("migrations", new_state)
new_model = new_state.apps.get_model('migrations', 'tag')
self.assertIsNot(old_model, new_model)
self.assertIs(old_model, old_model.food_mgr.model)
self.assertIs(old_model, old_model.food_qs.model)
self.assertIs(new_model, new_model.food_mgr.model)
self.assertIs(new_model, new_model.food_qs.model)
self.assertIsNot(old_model.food_mgr, new_model.food_mgr)
self.assertIsNot(old_model.food_qs, new_model.food_qs)
self.assertIsNot(old_model.food_mgr.model, new_model.food_mgr.model)
self.assertIsNot(old_model.food_qs.model, new_model.food_qs.model)
class ModelStateTests(TestCase): class ModelStateTests(TestCase):
def test_custom_model_base(self): def test_custom_model_base(self):