TiledViz
Loading...
Searching...
No Matches
migration.py
1"""
2Migration utilities for TiledViz database
3Handles automatic migration of project owners from projects.id_users to project_members
4"""
5
6import logging
7from sqlalchemy import text
8from app import db
9import app.models as models
10
11
13 """
14 Migrate existing project owners from projects.id_users to project_members table.
15 This function is idempotent - it can be run multiple times safely.
16 """
17 try:
18 logging.info("Starting project owners migration...")
19
20 # Check if migration is needed - projects with id_users but no owner membership
21 projects_without_members = db.session.execute(text("""
22 SELECT p.id, p.id_users, p.name
23 FROM projects p
24 LEFT JOIN project_members pm ON p.id = pm.project_id AND pm.role_type = 'owner'
25 WHERE p.id_users IS NOT NULL
26 AND pm.project_id IS NULL
27 """)).fetchall()
28
29 # Check for projects without any owner (id_users = NULL)
30 projects_without_owners = db.session.execute(text("""
31 SELECT p.id, p.name
32 FROM projects p
33 LEFT JOIN project_members pm ON p.id = pm.project_id AND pm.role_type = 'owner'
34 WHERE p.id_users IS NULL
35 AND pm.project_id IS NULL
36 """)).fetchall()
37
38 total_projects_to_migrate = len(projects_without_members) + len(projects_without_owners)
39
40 if total_projects_to_migrate == 0:
41 logging.info("No projects need migration - all owners already migrated")
42 return True
43
44 logging.info(f"Found {len(projects_without_members)} projects with id_users to migrate")
45 logging.info(f"Found {len(projects_without_owners)} projects without any owner")
46 logging.info(f"Total projects to process: {total_projects_to_migrate}")
47
48 migrated_count = 0
49 orphaned_projects = []
50
51 # Migrate projects with id_users
52 for project_id, owner_id, project_name in projects_without_members:
53 try:
54 # Check if the owner user still exists
55 owner_user = db.session.query(models.Users).filter_by(id=owner_id).first()
56 if not owner_user:
57 logging.warning(f"Owner user {owner_id} not found for project {project_name} (ID: {project_id})")
58 orphaned_projects.append((project_id, project_name, f"User {owner_id} not found"))
59 continue
60
61 # Create owner membership record
62 owner_membership = models.ProjectMembers(
63 project_id=project_id,
64 user_id=owner_id,
65 role_type='owner',
66 id_users=owner_id, # Keep for backward compatibility
67 id_projects=project_id # Keep for backward compatibility
68 )
69
70 db.session.add(owner_membership)
71 migrated_count += 1
72
73 logging.info(f"Created owner membership for project '{project_name}' (ID: {project_id}) -> User '{owner_user.name}' (ID: {owner_id})")
74
75 except Exception as e:
76 logging.error(f"Error migrating project {project_id}: {str(e)}")
77 orphaned_projects.append((project_id, project_name, str(e)))
78 continue
79
80 # Handle projects without any owner (id_users = NULL)
81 for project_id, project_name in projects_without_owners:
82 try:
83 # Try to find the first admin user to assign as owner
84 first_admin = db.session.query(models.Users).filter_by(is_admin=True).first()
85
86 if first_admin:
87 # Assign first admin as owner
88 owner_membership = models.ProjectMembers(
89 project_id=project_id,
90 user_id=first_admin.id,
91 role_type='owner',
92 id_users=first_admin.id,
93 id_projects=project_id
94 )
95
96 db.session.add(owner_membership)
97 migrated_count += 1
98
99 # Update the project's id_users field
100 project = db.session.query(models.Projects).filter_by(id=project_id).first()
101 if project:
102 project.id_users = first_admin.id
103
104 logging.info(f"Assigned admin '{first_admin.name}' as owner for orphaned project '{project_name}' (ID: {project_id})")
105 else:
106 # No admin user found - mark as orphaned
107 orphaned_projects.append((project_id, project_name, "No admin user available"))
108 logging.warning(f"No admin user found to assign as owner for project '{project_name}' (ID: {project_id})")
109
110 except Exception as e:
111 logging.error(f"Error handling orphaned project {project_id}: {str(e)}")
112 orphaned_projects.append((project_id, project_name, str(e)))
113 continue
114
115 # Commit all changes
116 db.session.commit()
117 logging.info(f"Successfully migrated {migrated_count} project owners")
118
119 # Report orphaned projects
120 if orphaned_projects:
121 logging.warning(f"Found {len(orphaned_projects)} orphaned projects that could not be migrated:")
122 for project_id, project_name, reason in orphaned_projects:
123 logging.warning(f" - Project '{project_name}' (ID: {project_id}): {reason}")
124 logging.warning("These projects need manual intervention via the admin interface")
125
126 return True
127
128 except Exception as e:
129 logging.error(f"Migration failed: {str(e)}")
130 db.session.rollback()
131 return False
132
133
135 """
136 Check the current migration status and return statistics
137 """
138 try:
139 # Count projects with id_users but no owner in project_members
140 unmigrated = db.session.execute(text("""
141 SELECT COUNT(*)
142 FROM projects p
143 LEFT JOIN project_members pm ON p.id = pm.project_id AND pm.role_type = 'owner'
144 WHERE p.id_users IS NOT NULL
145 AND pm.project_id IS NULL
146 """)).scalar()
147
148 # Count total projects with owners
149 total_with_owners = db.session.execute(text("""
150 SELECT COUNT(*) FROM projects WHERE id_users IS NOT NULL
151 """)).scalar()
152
153 # Count migrated projects
154 migrated = total_with_owners - unmigrated
155
156 return {
157 'total_projects_with_owners': total_with_owners,
158 'migrated_projects': migrated,
159 'unmigrated_projects': unmigrated,
160 'migration_complete': unmigrated == 0
161 }
162
163 except Exception as e:
164 logging.error(f"Error checking migration status: {str(e)}")
165 return None
166
167
169 """
170 Rollback migration by removing owner memberships created by migration.
171 WARNING: This will remove ALL owner memberships, not just migrated ones.
172 """
173 try:
174 logging.warning("Starting migration rollback...")
175
176 # Count owner memberships
177 owner_count = db.session.query(models.ProjectMembers).filter_by(role_type='owner').count()
178
179 if owner_count == 0:
180 logging.info("No owner memberships to rollback")
181 return True
182
183 # Remove all owner memberships
184 db.session.query(models.ProjectMembers).filter_by(role_type='owner').delete()
185 db.session.commit()
186
187 logging.warning(f"Rolled back {owner_count} owner memberships")
188 return True
189
190 except Exception as e:
191 logging.error(f"Rollback failed: {str(e)}")
192 db.session.rollback()
193 return False
194
195
197 """
198 Get list of projects that don't have any owner
199 """
200 try:
201 # Projects with id_users but user doesn't exist
202 projects_with_invalid_owner = db.session.execute(text("""
203 SELECT p.id, p.name, p.id_users
204 FROM projects p
205 LEFT JOIN users u ON p.id_users = u.id
206 WHERE p.id_users IS NOT NULL
207 AND u.id IS NULL
208 """)).fetchall()
209
210 # Projects without any owner (id_users = NULL)
211 projects_without_owner = db.session.execute(text("""
212 SELECT p.id, p.name
213 FROM projects p
214 LEFT JOIN project_members pm ON p.id = pm.project_id AND pm.role_type = 'owner'
215 WHERE p.id_users IS NULL
216 AND pm.project_id IS NULL
217 """)).fetchall()
218
219 return {
220 'invalid_owner': projects_with_invalid_owner,
221 'no_owner': projects_without_owner,
222 'total_orphaned': len(projects_with_invalid_owner) + len(projects_without_owner)
223 }
224
225 except Exception as e:
226 logging.error(f"Error getting orphaned projects: {str(e)}")
227 return None
228
229
231 """
232 Validate that migration was successful by checking data consistency
233 """
234 try:
235 # Check for orphaned project_members (no corresponding project)
236 orphaned_members = db.session.execute(text("""
237 SELECT COUNT(*)
238 FROM project_members pm
239 LEFT JOIN projects p ON pm.project_id = p.id
240 WHERE p.id IS NULL
241 """)).scalar()
242
243 # Check for projects with id_users but no owner membership
244 projects_without_owner_membership = db.session.execute(text("""
245 SELECT COUNT(*)
246 FROM projects p
247 LEFT JOIN project_members pm ON p.id = pm.project_id AND pm.role_type = 'owner'
248 WHERE p.id_users IS NOT NULL
249 AND pm.project_id IS NULL
250 """)).scalar()
251
252 # Check for multiple owners of the same project
253 multiple_owners = db.session.execute(text("""
254 SELECT project_id, COUNT(*)
255 FROM project_members
256 WHERE role_type = 'owner'
257 GROUP BY project_id
258 HAVING COUNT(*) > 1
259 """)).fetchall()
260
261 validation_result = {
262 'orphaned_members': orphaned_members,
263 'projects_without_owner_membership': projects_without_owner_membership,
264 'projects_with_multiple_owners': len(multiple_owners),
265 'multiple_owners_details': multiple_owners,
266 'is_valid': orphaned_members == 0 and projects_without_owner_membership == 0 and len(multiple_owners) == 0
267 }
268
269 if validation_result['is_valid']:
270 logging.info("Migration validation passed")
271 else:
272 logging.warning(f"Migration validation issues: {validation_result}")
273
274 return validation_result
275
276 except Exception as e:
277 logging.error(f"Migration validation failed: {str(e)}")
278 return None
check_migration_status()
Definition migration.py:134
get_orphaned_projects()
Definition migration.py:196
migrate_project_owners()
Definition migration.py:12