Skip to content

Conversation

@Ash-Crow
Copy link
Collaborator

@Ash-Crow Ash-Crow commented Dec 8, 2025

Purpose

Fix #1616.

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.)

Proposal

  • New UserReconciliationCsvImport model to manage the import of reconciliation requests through a task (user_reconciliation_csv_import_job)
  • New UserReconciliation model to store the user reconciliation requests themselves (a row = a active_user/inactive_user pair)
    • On save, a confirmation email is sent to the users
  • A process_reconciliation admin action process the action on the requested entries, if both emails have been checked.
    • Bulk update the DocumentAccess items, while managing the case where both users have access to the document (keeping the higher role)
    • Bulk update the Invitation items, while managing the case where both users have invitations for the document (keeping the higher role)
    • Bulk update the is_active status on both users
  • Write unit tests

Demo page reconciliation success

In waiting for @rl-83 to provide the design:
image

@Ash-Crow Ash-Crow force-pushed the sbl/user-reconciliation branch 4 times, most recently from 4d4bd60 to ab55e01 Compare December 11, 2025 08:28
@Ash-Crow Ash-Crow requested review from lunika and removed request for lunika December 15, 2025 09:12
@Ash-Crow Ash-Crow force-pushed the sbl/user-reconciliation branch 11 times, most recently from 35c4d49 to 5090839 Compare December 15, 2025 21:49
@Ash-Crow Ash-Crow force-pushed the sbl/user-reconciliation branch 8 times, most recently from 47099e1 to 57050a1 Compare January 12, 2026 16:54
reader = csv.DictReader(f)
rec_entries_created = 0
for row in reader:
status = row["status"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm wrong, but this column status is useless, don't you think? You can accept only one value. Or perhaps I'm not understanding its purpose.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the status on the source Grist form, as the CSV generation exports the whole table, so the lines that were already processed are ignored.

Capture d’écran du 2026-01-20 15-14-29

That said, I just saw how I can export the unique ID Grist gives to each row, so I'll update the model to use that instead


self.logs += f"""Requested update for {len(updated_accesses)} DocumentAccess items
and deletion for {len(removed_accesses)} DocumentAccess items.\n"""
self.status = "done"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing the status can lead to an unwanted state if something wrong happens between when this function is executed and when the bulk update is made.

blank=True,
related_name="inactive_user",
)
active_confirmation_id = models.UUIDField(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a little bit long, but active_email_confirmation_id gives probably more information.

active_confirmation_id = models.UUIDField(
default=uuid.uuid4, unique=True, editable=False, null=True
)
inactive_confirmation_id = models.UUIDField(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

@AntoLC AntoLC added feature add a new feature backend labels Jan 15, 2026
@AntoLC AntoLC linked an issue Jan 15, 2026 that may be closed by this pull request
Comment on lines 529 to 534
You can submit another request with the valid email addresses.
"""
).format(email1=email_1, email2=email_2),
"link": f"{domain}/",
"link_label": str(_("Click here")),
"button_label": str(_("Make a new request")),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see there is a button to trigger a new request, is there a route for that ?

Comment on lines +252 to +257
class ReconciliationConfirmView(APIView):
"""API endpoint to confirm user reconciliation emails.

GET /user_reconciliations/{user_type}/{confirmation_id}/
Marks `active_email_checked` or `inactive_email_checked` to True.
"""
Copy link
Collaborator

@AntoLC AntoLC Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be nice to have a few tests about this part, to have like a blueprint on how to use it.

Comment on lines +340 to +342
class Meta:
verbose_name = _("user reconciliation")
verbose_name_plural = _("user reconciliations")
Copy link
Collaborator

@AntoLC AntoLC Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class Meta:
verbose_name = _("user reconciliation")
verbose_name_plural = _("user reconciliations")
class Meta:
db_table = "impress_user_reconciliation"
verbose_name = _("user reconciliation")
verbose_name_plural = _("user reconciliations")

By default the table will be created with core prefix otherwise.

Image
"""To confirm that you are the one who initiated the request
and that this email belongs to you:"""
),
"link": f"{domain}/user_reconciliations/{user_type}/{confirmation_id}/",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick, but could we change to hyphens ?

Suggested change
"link": f"{domain}/user_reconciliations/{user_type}/{confirmation_id}/",
"link": f"{domain}/user-reconciliations/{user_type}/{confirmation_id}/",
@AntoLC AntoLC force-pushed the sbl/user-reconciliation branch from 052b0b3 to d32ef62 Compare January 15, 2026 17:15
@lunika
Copy link
Member

lunika commented Jan 16, 2026

After discussion with @AntoLC some data migration are missing.
Other relations with the user exists in the database and this data will be lost if not transfered.
When a user visit a docs it doesn't have access (a public or connected one), an entry is added to the LinkTrace model. These docs will be shown to the user on its homepage. This table should also be migrated.
Same for the DocumentFavorite model. Should be migrated.

There are also relation with Thread, Comment and Reaction. But I don't know if they should be migrated. WDYT @virgile-dev @rl-83 ? A thread (the comment container) and comments should be migrated from the inactive user to the new one ?

Ash-Crow and others added 3 commits January 20, 2026 14:44
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.)
@Ash-Crow Ash-Crow force-pushed the sbl/user-reconciliation branch from d32ef62 to 032413a Compare January 20, 2026 13:45
@github-actions
Copy link

github-actions bot commented Jan 20, 2026

Size Change: +6.44 kB (+0.15%)

Total Size: 4.2 MB

Filename Size Change
apps/impress/out/_next/static/01de138d/_buildManifest.js 0 B -852 B (removed) 🏆
apps/impress/out/_next/static/bd86f955/_buildManifest.js 917 B +917 B (new file) 🆕
apps/impress/out/_next/static/chunks/1217.js 0 B -784 kB (removed) 🏆
apps/impress/out/_next/static/chunks/2798.js 780 kB +780 kB (new file) 🆕
apps/impress/out/_next/static/chunks/pages/_app.js 572 kB +4.8 kB (+0.85%)
apps/impress/out/_next/static/chunks/pages/user_reconciliations/active/[id].js 1.42 kB +1.42 kB (new file) 🆕
apps/impress/out/_next/static/chunks/pages/user_reconciliations/inactive/[id].js 1.43 kB +1.43 kB (new file) 🆕
apps/impress/out/user_reconciliations/active/[id]/index.html 1.25 kB +1.25 kB (new file) 🆕
apps/impress/out/user_reconciliations/inactive/[id]/index.html 1.25 kB +1.25 kB (new file) 🆕

compressed-size-action

This changes the way the reconciliation requests CSV imports are
processed and requires the CSV to provide a unique id for each
request.

Rows with errors are also now handled better so that they don't
fail the whole import.
This changes the way the reconciliation requests CSV imports are
processed and requires the CSV to provide a unique id for each
request.

Rows with errors are also now handled better so that they don't
fail the whole import.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

4 participants