🧑‍💻(backend) add email filter to target subset of users for merge cmd

Add an email substring filter to the user merge management command,
allowing selection of a subset of users concerned by the merge process.

This enables safer incremental execution of the command by testing it
on a controlled group of users before applying it globally.

The goal is to validate behavior and ensure the merge process does not
introduce unexpected side effects or inconsistencies at scale.
This commit is contained in:
lebaudantoine
2026-06-02 18:58:06 +02:00
committed by aleb_the_flash
parent 29b0a6fcb4
commit 85eff8afaf
2 changed files with 44 additions and 2 deletions
@@ -36,17 +36,28 @@ class Command(BaseCommand):
action="store_true",
help="Simulate the merge without writing any changes to the database.",
)
parser.add_argument(
"--email-filter",
type=str,
default=None,
help="Only merge users whose email contains this string (e.g. '@example.com').",
)
def handle(self, *args, **options):
"""Execute the management command."""
dry_run = options["dry_run"]
email_filter = options["email_filter"]
if dry_run:
self.stdout.write("[DRY-RUN] No changes will be written.\n")
users_qs = User.objects.all()
if email_filter:
users_qs = users_qs.filter(email__icontains=email_filter)
self.stdout.write(f"[INFO] Filtering emails containing '{email_filter}'.\n")
duplicate_emails = (
User.objects.all()
.exclude(email__isnull=True)
users_qs.exclude(email__isnull=True)
.exclude(email="")
.values("email")
.annotate(cnt=Count("id"))
@@ -427,3 +427,34 @@ def test_merge_is_atomic_rolls_back_all_on_any_failure(mock_reassign_files):
for f in files:
f.refresh_from_db()
assert f.creator == user1
# ── Email filter ───────────────────────────────────────────────────────────────
def test_merge_email_filter_only_merges_matching_emails():
"""Command should only merge users whose email matches the filter."""
UserFactory(email="user1@example.com")
UserFactory(email="user1@example.com")
other1 = UserFactory(email="user1@other.com")
other2 = UserFactory(email="user1@other.com")
call_command("merge_duplicate_users", email_filter="@example.com")
assert User.objects.filter(email="user1@example.com").count() == 1
assert User.objects.filter(id=other1.id).exists()
assert User.objects.filter(id=other2.id).exists()
def test_merge_email_filter_no_match_does_nothing():
"""Command should do nothing when the email filter matches no users."""
UserFactory(email="user1@example.com")
UserFactory(email="user1@example.com")
call_command("merge_duplicate_users", email_filter="@nomatch.com")
assert User.objects.filter(email="user1@example.com").count() == 2
def test_merge_email_filter_is_case_insensitive():
"""Command should match emails case-insensitively when filtering."""
UserFactory(email="user1@Example.com")
UserFactory(email="user1@Example.com")
call_command("merge_duplicate_users", email_filter="@example.com")
assert User.objects.filter(email="user1@Example.com").count() == 1