Normalize supervisor emails and add unique constraint for case-insensitive email uniqueness

This commit is contained in:
Ross
2025-11-03 10:30:45 +00:00
parent 7d079e0466
commit 66f9dadcc8
3 changed files with 80 additions and 2 deletions
@@ -0,0 +1,51 @@
from django.db import migrations
# no additional imports required
def normalize_and_dedupe(apps, schema_editor):
Supervisor = apps.get_model('generic', 'Supervisor')
UserProfile = apps.get_model('generic', 'UserProfile')
CidUser = apps.get_model('generic', 'CidUser')
# Build groups by normalized email without saving changes yet to avoid unique constraint errors
supervisors = list(Supervisor.objects.all())
groups = {}
for s in supervisors:
norm_email = (s.email or "").strip().lower()
norm_name = (s.name or "").strip()
groups.setdefault(norm_email, []).append((s, norm_name))
# For each group, pick a keeper (smallest PK), reassign FKs from duplicates to keeper, delete duplicates,
# then update the keeper's normalized email/name.
for norm_email, entries in groups.items():
# pick keeper by lowest id
entries_sorted = sorted(entries, key=lambda e: e[0].pk)
keeper_obj, keeper_name = entries_sorted[0]
# reassign duplicates
for dup_obj, dup_name in entries_sorted[1:]:
UserProfile.objects.filter(supervisor_id=dup_obj.pk).update(supervisor_id=keeper_obj.pk)
CidUser.objects.filter(supervisor_id=dup_obj.pk).update(supervisor_id=keeper_obj.pk)
dup_obj.delete()
# update keeper's email and name to normalized values if needed
changed = False
if norm_email and keeper_obj.email != norm_email:
keeper_obj.email = norm_email
changed = True
if keeper_name and keeper_obj.name != keeper_name:
keeper_obj.name = keeper_name
changed = True
if changed:
keeper_obj.save()
class Migration(migrations.Migration):
dependencies = [
('generic', '0027_alter_questionreview_status'),
]
operations = [
migrations.RunPython(normalize_and_dedupe, reverse_code=migrations.RunPython.noop),
]
@@ -0,0 +1,17 @@
from django.db import migrations
from django.db.models import UniqueConstraint
from django.db.models.functions import Lower
class Migration(migrations.Migration):
dependencies = [
('generic', '0028_normalize_supervisor_emails'),
]
operations = [
migrations.AddConstraint(
model_name='supervisor',
constraint=UniqueConstraint(Lower('email'), name='unique_supervisor_email_ci'),
),
]
+12 -2
View File
@@ -5,6 +5,8 @@ import os
from typing import Optional, Self, Tuple
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import models
from django.db.models import UniqueConstraint
from django.db.models.functions import Lower
from django.forms import ValidationError
from django.http import Http404, HttpRequest
from django.shortcuts import render
@@ -1859,8 +1861,16 @@ class Supervisor(models.Model):
return reverse("generic:supervisor_detail", kwargs={"pk": self.pk})
def clean(self):
self.email = self.email.strip()
self.name = self.name.strip()
if self.email:
# normalize email: strip and lowercase for consistency
self.email = self.email.strip().lower()
if self.name:
self.name = self.name.strip()
class Meta:
constraints = [
UniqueConstraint(Lower('email'), name='unique_supervisor_email_ci')
]
@receiver(post_save, sender=User)