diff --git a/app/migrations/0002_conference_unique_conference_id_app.py b/app/migrations/0002_conference_unique_conference_id_app.py new file mode 100644 index 0000000..3d687dd --- /dev/null +++ b/app/migrations/0002_conference_unique_conference_id_app.py @@ -0,0 +1,65 @@ +# Generated by Django 2.2.28 on 2026-04-09 13:49 + +from django.db import migrations +from django.db.models import Count, Min + + +def merge_duplicate_conferences(apps, schema_editor): + """ + Before adding the unique constraint, merge duplicate conferences + (same conference_id + app_id). Keep the oldest one, move all + related data to it, and delete the rest. + """ + from django.db import transaction + + Conference = apps.get_model('app', 'Conference') + GenericEvent = apps.get_model('app', 'GenericEvent') + Connection = apps.get_model('app', 'Connection') + Session = apps.get_model('app', 'Session') + Issue = apps.get_model('app', 'Issue') + Summary = apps.get_model('app', 'Summary') + + dupes = (Conference.objects + .values('conference_id', 'app_id') + .annotate(count=Count('id'), earliest=Min('created_at')) + .filter(count__gt=1)) + + for dupe in dupes: + with transaction.atomic(): + conferences = Conference.objects.filter( + conference_id=dupe['conference_id'], + app_id=dupe['app_id'], + ).order_by('created_at') + + keeper = conferences.first() + duplicates = conferences.exclude(pk=keeper.pk) + keeper_has_summary = Summary.objects.filter(conference=keeper).exists() + + for dup in duplicates: + GenericEvent.objects.filter(conference=dup).update(conference=keeper) + Connection.objects.filter(conference=dup).update(conference=keeper) + Session.objects.filter(conference=dup).update(conference=keeper) + Issue.objects.filter(conference=dup).update(conference=keeper) + + # Summary is OneToOne — keep the first one, delete the rest + if not keeper_has_summary: + Summary.objects.filter(conference=dup).update(conference=keeper) + keeper_has_summary = True + else: + Summary.objects.filter(conference=dup).delete() + + for participant in dup.participants.all(): + keeper.participants.add(participant) + + dup.delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0001_initial'), + ] + + operations = [ + migrations.RunPython(merge_duplicate_conferences, migrations.RunPython.noop), + ] diff --git a/app/migrations/0003_conference_unique_together.py b/app/migrations/0003_conference_unique_together.py new file mode 100644 index 0000000..52beaf3 --- /dev/null +++ b/app/migrations/0003_conference_unique_together.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.28 on 2026-04-09 13:49 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0002_conference_unique_conference_id_app'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='conference', + unique_together={('conference_id', 'app')}, + ), + ] diff --git a/app/models/conference.py b/app/models/conference.py index 245fb45..e52f69d 100644 --- a/app/models/conference.py +++ b/app/models/conference.py @@ -37,6 +37,7 @@ class Conference(BaseModel): class Meta: db_table = 'conference' + unique_together = (('conference_id', 'app'),) cache_keys = ( sorted(('id',)),