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 @@ -10,6 +10,7 @@ and this project adheres to

- ✨ Add comments feature to the editor #1330
- ✨(backend) Comments on text editor #1330
- ✨(backend) manage reconciliation requests for user accounts #1708

### Changed

Expand Down
88 changes: 86 additions & 2 deletions src/backend/core/admin.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
"""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.forms import ModelForm
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 +107,87 @@ class UserAdmin(auth_admin.UserAdmin):
search_fields = ("id", "sub", "admin_email", "email", "full_name")


class UserReconciliationCsvImportForm(ModelForm):
class Meta:
model = models.UserReconciliationCsvImport
fields = ("file",)


@admin.register(models.UserReconciliationCsvImport)
class UserReconciliationCsvImportAdmin(admin.ModelAdmin):
list_display = ("id", "created_at", "status")

def save_model(self, request, obj, form, change):
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.
- Transfer invitations 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 = []
updated_invitations = []
removed_invitations = []
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

new_updated_invitations, new_removed_invitations = (
entry.process_invitation_reconciliation()
)
updated_invitations += new_updated_invitations
removed_invitations += new_removed_invitations

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 len(removed_documentaccess):
ids_to_delete = [rd.id for rd in removed_documentaccess]
models.DocumentAccess.objects.filter(id__in=ids_to_delete).delete()

models.Invitation.objects.bulk_update(updated_invitations, ["email", "role"])

if len(removed_invitations):
ids_to_delete = [ri.id for ri in removed_invitations]
models.Invitation.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):
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,149 @@
# Generated by Django 5.2.8 on 2025-12-01 15:01

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


class Migration(migrations.Migration):

dependencies = [
("core", "0026_comments"),
]

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={
"abstract": False,
},
),
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={
"abstract": False,
},
),
]
Loading