Bin
2025-12-17 2e6c955be321cefd7e0c4a3031eab805e0a5a303
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# Generated by Django 4.2.16 on 2024-11-07 10:31
 
from django.db import migrations
import logging
 
from core.models import AsyncMigrationStatus
from core.redis import start_job_async_or_sync
from django.db.models import Count, Min
from projects.models import ProjectMember
 
logger = logging.getLogger(__name__)
migration_name = '0028_auto_20241107_1031'
 
 
def forward_migration(migration_name):
 
    logger.info(f'Starting async migration {migration_name}')
    migration = AsyncMigrationStatus.objects.create(
        name=migration_name,
        status=AsyncMigrationStatus.STATUS_STARTED,
    )
 
    try:
        # Get projects with duplicates
        projects_with_duplicates = (
            ProjectMember.objects
            .values('project_id', 'user_id')
            .annotate(entry_count=Count('id'))
            .filter(entry_count__gt=1)
            .values_list('project_id', flat=True)
            .distinct()
        )
 
        for project_id in projects_with_duplicates:
            # Remove duplicates for each project
            duplicates = (
                ProjectMember.objects
                .filter(project_id=project_id)
                .values('user_id')
                .annotate(count=Count('id'), min_id=Min('id'))
                .filter(count__gt=1)
            )
            total_deleted = 0
            for dup in duplicates:
                user_id = dup['user_id']
                min_id = dup['min_id']
                entries_to_delete = (
                    ProjectMember.objects
                    .filter(user_id=user_id, project_id=project_id)
                    .exclude(id=min_id)
                )
                deleted_count, _ = entries_to_delete.delete()
                total_deleted += deleted_count
            logger.info(f'Deleted {total_deleted} duplicate ProjectMember entries for project ID {project_id}.')
 
    except Exception as e:
        migration.status = AsyncMigrationStatus.STATUS_ERROR
        migration.save()
        logger.error(f'Async migration {migration_name} failed: {e}')
        raise
 
    migration.status = AsyncMigrationStatus.STATUS_FINISHED
    migration.save()
    logger.info(f'Async migration {migration_name} complete')
 
 
def forwards(apps, schema_editor):
    # Dispatch migration to workers without passing unpicklable objects
    start_job_async_or_sync(forward_migration, migration_name=migration_name)
 
 
def backwards(apps, schema_editor):
    pass
 
 
class Migration(migrations.Migration):
    atomic = False
 
    dependencies = [
        ("projects", "0027_project_custom_task_lock_ttl"),
    ]
 
    operations = [
        migrations.RunPython(forwards, backwards),
    ]