Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to
### Added

- ✨(backend) allow to create a new user in a marketing system
- ✨(backend) manage reconciliation requests for user accounts #1708

### Changed

Expand Down
71 changes: 69 additions & 2 deletions src/backend/core/admin.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"""Admin classes and registrations for core app."""

from django.contrib import admin
from django.contrib import admin, messages
from django.contrib.auth import admin as auth_admin
from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _

from treebeard.admin import TreeAdmin

from . import models
from core import models
from core.tasks.user_reconciliation import user_reconciliation_csv_import_job


class TemplateAccessInline(admin.TabularInline):
Expand Down Expand Up @@ -104,6 +106,71 @@ class UserAdmin(auth_admin.UserAdmin):
search_fields = ("id", "sub", "admin_email", "email", "full_name")


@admin.register(models.UserReconciliationCsvImport)
class UserReconciliationCsvImportAdmin(admin.ModelAdmin):
"""Admin class for UserReconciliationCsvImport model."""

list_display = ("id", "created_at", "status")

def save_model(self, request, obj, form, change):
"""Override save_model to trigger the import task on creation."""
super().save_model(request, obj, form, change)

if not change:
user_reconciliation_csv_import_job.delay(obj.pk)
messages.success(request, _("Import job created and queued."))
return redirect("..")


@admin.action(description=_("Process selected user reconciliations"))
def process_reconciliation(_modeladmin, _request, queryset):
"""
Admin action to process selected user reconciliations.
The action will process only entries that are ready and have both emails checked.

Its action is threefold:
- Transfer document accesses from inactive to active user, updating roles as needed.
- Activate the active user and deactivate the inactive user.
"""
processable_entries = queryset.filter(
status="ready", active_email_checked=True, inactive_email_checked=True
)

# Prepare the bulk operations
updated_documentaccess = []
removed_documentaccess = []
update_users_active_status = []

for entry in processable_entries:
new_updated_documentaccess, new_removed_documentaccess = (
entry.process_documentaccess_reconciliation()
)
updated_documentaccess += new_updated_documentaccess
removed_documentaccess += new_removed_documentaccess

entry.active_user.is_active = True
entry.inactive_user.is_active = False
update_users_active_status.append(entry.active_user)
update_users_active_status.append(entry.inactive_user)

# Actually perform the bulk operations
models.DocumentAccess.objects.bulk_update(updated_documentaccess, ["user", "role"])

if removed_documentaccess:
ids_to_delete = [rd.id for rd in removed_documentaccess]
models.DocumentAccess.objects.filter(id__in=ids_to_delete).delete()

models.User.objects.bulk_update(update_users_active_status, ["is_active"])


@admin.register(models.UserReconciliation)
class UserReconciliationAdmin(admin.ModelAdmin):
"""Admin class for UserReconciliation model."""

list_display = ["id", "created_at", "status"]
actions = [process_reconciliation]


@admin.register(models.Template)
class TemplateAdmin(admin.ModelAdmin):
"""Template admin interface declaration."""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Generated by Django 5.2.9 on 2025-12-15 19:09

import uuid

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("core", "0027_auto_20251120_0956"),
]

operations = [
migrations.CreateModel(
name="UserReconciliationCsvImport",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
("file", models.FileField(upload_to="imports/")),
(
"status",
models.CharField(
choices=[
("pending", "Pending"),
("running", "Running"),
("done", "Done"),
("error", "Error"),
],
default="pending",
max_length=20,
),
),
("logs", models.TextField(blank=True)),
],
options={
"verbose_name": "user reconciliation CSV import",
"verbose_name_plural": "user reconciliation CSV imports",
},
),
migrations.CreateModel(
name="UserReconciliation",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
(
"active_email",
models.EmailField(
max_length=254, verbose_name="Active email address"
),
),
(
"inactive_email",
models.EmailField(
max_length=254, verbose_name="Email address to deactivate"
),
),
("active_email_checked", models.BooleanField(default=False)),
("inactive_email_checked", models.BooleanField(default=False)),
(
"status",
models.CharField(
choices=[
("pending", "Pending"),
("ready", "Ready"),
("done", "Done"),
("error", "Error"),
],
default="pending",
max_length=20,
),
),
("logs", models.TextField(blank=True)),
(
"active_user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="active_user",
to=settings.AUTH_USER_MODEL,
),
),
(
"inactive_user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="inactive_user",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "user reconciliation",
"verbose_name_plural": "user reconciliations",
},
),
]
Loading
Loading