Skip to content

Commit fb43ef3

Browse files
committed
✨(backend) manage reconciliation requests for user accounts
For now, the reconciliation requests are imported through CSV in the Django admin, which sends confirmation email to both addresses. When both are checked, the actual reconciliation is processed, in a threefold process (update document acess, update invitations, update user status.)
1 parent 08fb191 commit fb43ef3

File tree

8 files changed

+654
-4
lines changed

8 files changed

+654
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to
99
### Added
1010

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

1314
### Changed
1415

src/backend/core/admin.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
"""Admin classes and registrations for core app."""
22

3-
from django.contrib import admin
3+
from django.contrib import admin, messages
44
from django.contrib.auth import admin as auth_admin
5+
from django.shortcuts import redirect
56
from django.utils.translation import gettext_lazy as _
67

78
from treebeard.admin import TreeAdmin
89

9-
from . import models
10+
from core import models
11+
from core.tasks.user_reconciliation import user_reconciliation_csv_import_job
1012

1113

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

106108

109+
@admin.register(models.UserReconciliationCsvImport)
110+
class UserReconciliationCsvImportAdmin(admin.ModelAdmin):
111+
"""Admin class for UserReconciliationCsvImport model."""
112+
113+
list_display = ("id", "created_at", "status")
114+
115+
def save_model(self, request, obj, form, change):
116+
"""Override save_model to trigger the import task on creation."""
117+
super().save_model(request, obj, form, change)
118+
119+
if not change:
120+
user_reconciliation_csv_import_job.delay(obj.pk)
121+
messages.success(request, _("Import job created and queued."))
122+
return redirect("..")
123+
124+
125+
@admin.action(description=_("Process selected user reconciliations"))
126+
def process_reconciliation(modeladmin, request, queryset):
127+
"""
128+
Admin action to process selected user reconciliations.
129+
The action will process only entries that are ready and have both emails checked.
130+
131+
Its action is threefold:
132+
- Transfer document accesses from inactive to active user, updating roles as needed.
133+
- Activate the active user and deactivate the inactive user.
134+
"""
135+
processable_entries = queryset.filter(
136+
status="ready", active_email_checked=True, inactive_email_checked=True
137+
)
138+
139+
# Prepare the bulk operations
140+
updated_documentaccess = []
141+
removed_documentaccess = []
142+
update_users_active_status = []
143+
144+
for entry in processable_entries:
145+
new_updated_documentaccess, new_removed_documentaccess = (
146+
entry.process_documentaccess_reconciliation()
147+
)
148+
updated_documentaccess += new_updated_documentaccess
149+
removed_documentaccess += new_removed_documentaccess
150+
151+
entry.active_user.is_active = True
152+
entry.inactive_user.is_active = False
153+
update_users_active_status.append(entry.active_user)
154+
update_users_active_status.append(entry.inactive_user)
155+
156+
# Actually perform the bulk operations
157+
models.DocumentAccess.objects.bulk_update(updated_documentaccess, ["user", "role"])
158+
159+
if removed_documentaccess:
160+
ids_to_delete = [rd.id for rd in removed_documentaccess]
161+
models.DocumentAccess.objects.filter(id__in=ids_to_delete).delete()
162+
163+
models.User.objects.bulk_update(update_users_active_status, ["is_active"])
164+
165+
166+
@admin.register(models.UserReconciliation)
167+
class UserReconciliationAdmin(admin.ModelAdmin):
168+
"""Admin class for UserReconciliation model."""
169+
170+
list_display = ["id", "created_at", "status"]
171+
actions = [process_reconciliation]
172+
173+
107174
@admin.register(models.Template)
108175
class TemplateAdmin(admin.ModelAdmin):
109176
"""Template admin interface declaration."""
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# Generated by Django 5.2.9 on 2025-12-15 19:09
2+
3+
import uuid
4+
5+
import django.db.models.deletion
6+
from django.conf import settings
7+
from django.db import migrations, models
8+
9+
10+
class Migration(migrations.Migration):
11+
dependencies = [
12+
("core", "0027_auto_20251120_0956"),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name="UserReconciliationCsvImport",
18+
fields=[
19+
(
20+
"id",
21+
models.UUIDField(
22+
default=uuid.uuid4,
23+
editable=False,
24+
help_text="primary key for the record as UUID",
25+
primary_key=True,
26+
serialize=False,
27+
verbose_name="id",
28+
),
29+
),
30+
(
31+
"created_at",
32+
models.DateTimeField(
33+
auto_now_add=True,
34+
help_text="date and time at which a record was created",
35+
verbose_name="created on",
36+
),
37+
),
38+
(
39+
"updated_at",
40+
models.DateTimeField(
41+
auto_now=True,
42+
help_text="date and time at which a record was last updated",
43+
verbose_name="updated on",
44+
),
45+
),
46+
("file", models.FileField(upload_to="imports/")),
47+
(
48+
"status",
49+
models.CharField(
50+
choices=[
51+
("pending", "Pending"),
52+
("running", "Running"),
53+
("done", "Done"),
54+
("error", "Error"),
55+
],
56+
default="pending",
57+
max_length=20,
58+
),
59+
),
60+
("logs", models.TextField(blank=True)),
61+
],
62+
options={
63+
"verbose_name": "user reconciliation CSV import",
64+
"verbose_name_plural": "user reconciliation CSV imports",
65+
},
66+
),
67+
migrations.CreateModel(
68+
name="UserReconciliation",
69+
fields=[
70+
(
71+
"id",
72+
models.UUIDField(
73+
default=uuid.uuid4,
74+
editable=False,
75+
help_text="primary key for the record as UUID",
76+
primary_key=True,
77+
serialize=False,
78+
verbose_name="id",
79+
),
80+
),
81+
(
82+
"created_at",
83+
models.DateTimeField(
84+
auto_now_add=True,
85+
help_text="date and time at which a record was created",
86+
verbose_name="created on",
87+
),
88+
),
89+
(
90+
"updated_at",
91+
models.DateTimeField(
92+
auto_now=True,
93+
help_text="date and time at which a record was last updated",
94+
verbose_name="updated on",
95+
),
96+
),
97+
(
98+
"active_email",
99+
models.EmailField(
100+
max_length=254, verbose_name="Active email address"
101+
),
102+
),
103+
(
104+
"inactive_email",
105+
models.EmailField(
106+
max_length=254, verbose_name="Email address to deactivate"
107+
),
108+
),
109+
("active_email_checked", models.BooleanField(default=False)),
110+
("inactive_email_checked", models.BooleanField(default=False)),
111+
(
112+
"status",
113+
models.CharField(
114+
choices=[
115+
("pending", "Pending"),
116+
("ready", "Ready"),
117+
("done", "Done"),
118+
("error", "Error"),
119+
],
120+
default="pending",
121+
max_length=20,
122+
),
123+
),
124+
("logs", models.TextField(blank=True)),
125+
(
126+
"active_user",
127+
models.ForeignKey(
128+
blank=True,
129+
null=True,
130+
on_delete=django.db.models.deletion.CASCADE,
131+
related_name="active_user",
132+
to=settings.AUTH_USER_MODEL,
133+
),
134+
),
135+
(
136+
"inactive_user",
137+
models.ForeignKey(
138+
blank=True,
139+
null=True,
140+
on_delete=django.db.models.deletion.CASCADE,
141+
related_name="inactive_user",
142+
to=settings.AUTH_USER_MODEL,
143+
),
144+
),
145+
],
146+
options={
147+
"verbose_name": "user reconciliation",
148+
"verbose_name_plural": "user reconciliations",
149+
},
150+
),
151+
]

0 commit comments

Comments
 (0)