1+ from django .test import TestCase
2+ from importlib import import_module , reload
3+
4+ from projects .tests .factories import ProjectFactory
5+ from data_manager .models import View , FilterGroup , Filter
6+ from core .models import AsyncMigrationStatus
7+
8+
9+ class TestCleanupInconsistentFiltergroupMigration (TestCase ):
10+
11+ def setUp (self ):
12+ # Filter group 1
13+ self .filter_group_1 = FilterGroup .objects .create (conjunction = 'and' )
14+ self .filter_1 = Filter .objects .create (index = 0 , column = 'id' , type = 'number' , operator = 'eq' , value = 1 )
15+ self .filter_2 = Filter .objects .create (index = 1 , column = 'id' , type = 'number' , operator = 'eq' , value = 2 )
16+ self .filter_group_1 .filters .add (self .filter_1 )
17+ self .filter_group_1 .filters .add (self .filter_2 )
18+
19+ # Filter group 2
20+ self .filter_group_2 = FilterGroup .objects .create (conjunction = 'or' )
21+ self .filter_3 = Filter .objects .create (index = 0 , column = 'id' , type = 'number' , operator = 'eq' , value = 1 )
22+ self .filter_4 = Filter .objects .create (index = 1 , column = 'id' , type = 'number' , operator = 'eq' , value = 2 )
23+ self .filter_group_2 .filters .add (self .filter_3 )
24+ self .filter_group_2 .filters .add (self .filter_4 )
25+
26+
27+ # Project 1
28+ self .project_1 = ProjectFactory ()
29+ self .view_1 = View .objects .create (project = self .project_1 , filter_group = self .filter_group_1 )
30+ self .view_2 = View .objects .create (project = self .project_1 , filter_group = self .filter_group_2 )
31+
32+ # Project 2
33+ self .project_2 = ProjectFactory ()
34+ # These views don't have their own filter group.
35+ self .view_3 = View .objects .create (project = self .project_2 , filter_group = self .filter_group_1 )
36+ self .view_4 = View .objects .create (project = self .project_2 , filter_group = self .filter_group_2 )
37+
38+ # Project 3
39+ self .project_3 = ProjectFactory ()
40+ # This view doesn't have its own filter group.
41+ self .view_5 = View .objects .create (project = self .project_3 , filter_group = self .filter_group_1 )
42+
43+
44+ # Unaffected project, filters and views
45+ self .project_unaffected = ProjectFactory ()
46+ self .filter_group_unaffected = FilterGroup .objects .create (conjunction = 'and' )
47+ self .filter_unaffected = Filter .objects .create (index = 0 , column = 'id' , type = 'number' , operator = 'eq' , value = 1 )
48+ self .filter_group_unaffected .filters .add (self .filter_unaffected )
49+ self .view_unaffected = View .objects .create (project = self .project_unaffected , filter_group = self .filter_group_unaffected )
50+
51+ def _assert_equivalent_filter_group (self , filter_group_1 , filter_group_2 ):
52+ assert filter_group_1 .conjunction == filter_group_2 .conjunction
53+
54+ # Check both filter groups have same number of filters
55+ assert filter_group_1 .filters .count () == filter_group_2 .filters .count ()
56+
57+ # Get filters sorted by index to compare in order
58+ filters_1 = filter_group_1 .filters .order_by ('index' )
59+ filters_2 = filter_group_2 .filters .order_by ('index' )
60+
61+ # Compare each filter's attributes
62+ for f1 , f2 in zip (filters_1 , filters_2 ):
63+ assert f1 .column == f2 .column
64+ assert f1 .type == f2 .type
65+ assert f1 .operator == f2 .operator
66+ assert f1 .value == f2 .value
67+ assert f1 .index == f2 .index
68+
69+ def test_migration (self ):
70+ AsyncMigrationStatus .objects .all ().delete () # cleanup migrations run when creating test database
71+ assert AsyncMigrationStatus .objects .filter (name = '0013_cleanup_inconsistent_filtergroup_20250624_2119' ).count () == 0
72+
73+ # Assert initial state
74+ assert FilterGroup .objects .count () == 3
75+ assert Filter .objects .count () == 5
76+
77+ # Run migration
78+ module = import_module ('data_manager.migrations.0013_cleanup_inconsistent_filtergroup_20250624_2119' )
79+ reload (module )
80+ module .cleanup_inconsistent_filtergroup ()
81+
82+ migration = AsyncMigrationStatus .objects .get (name = '0013_cleanup_inconsistent_filtergroup_20250624_2119' )
83+ assert migration .status == AsyncMigrationStatus .STATUS_FINISHED
84+ assert set (migration .meta ['project_ids' ]) == set ([self .project_1 .id , self .project_2 .id , self .project_3 .id ])
85+
86+ # Assert final state
87+ assert FilterGroup .objects .count () == 6
88+ assert Filter .objects .count () == 11
89+
90+ # Assert filter groups are equivalent for views that shared filter_group_1
91+ self .view_1 .refresh_from_db ()
92+ self .view_2 .refresh_from_db ()
93+ self .view_3 .refresh_from_db ()
94+ self .view_4 .refresh_from_db ()
95+ self .view_5 .refresh_from_db ()
96+
97+ # View 1 keeps original filter group
98+ assert self .view_1 .filter_group == self .filter_group_1
99+ # View 2 keeps original filter group
100+ assert self .view_2 .filter_group == self .filter_group_2
101+ # View 3 gets new equivalent filter group
102+ assert self .view_3 .filter_group != self .filter_group_1
103+ self ._assert_equivalent_filter_group (self .view_3 .filter_group , self .filter_group_1 )
104+ # View 4 gets new equivalent filter group
105+ assert self .view_4 .filter_group != self .filter_group_2
106+ self ._assert_equivalent_filter_group (self .view_4 .filter_group , self .filter_group_2 )
107+ # View 5 gets new equivalent filter group
108+ assert self .view_5 .filter_group != self .filter_group_1
109+ self ._assert_equivalent_filter_group (self .view_5 .filter_group , self .filter_group_1 )
110+ # Assert unaffected view/filter group remains unchanged
111+ self .view_unaffected .refresh_from_db ()
112+ assert self .view_unaffected .filter_group == self .filter_group_unaffected
113+
114+
115+
116+
117+
0 commit comments