-
Notifications
You must be signed in to change notification settings - Fork 524
✨(backend) manage reconciliation requests for user accounts #1708
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Ash-Crow
wants to merge
14
commits into
main
Choose a base branch
from
sbl/user-reconciliation
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
7e1d811
✨(backend) manage reconciliation requests for user accounts
Ash-Crow a511180
✨(frontend) add email validation pages
AntoLC 032413a
fix
AntoLC af523e1
Merge branch 'main' into sbl/user-reconciliation
Ash-Crow dc8db60
✨(backend) reconciliation requests: use a source unique id
Ash-Crow b9e341b
Merge branch 'main' into sbl/user-reconciliation
Ash-Crow c121441
✨(backend) reconciliation requests: use a source unique id
Ash-Crow 97f02ff
Merge branch 'sbl/user-reconciliation' of github.com:suitenumerique/d…
Ash-Crow c65ab34
✨(backend) reconciliation requests: use the standard email template
Ash-Crow 313e14d
✨(backend) process reconciliation requests as transactions
Ash-Crow 61c81de
✨(backend) reconciliation requests update link traces and doc favorites
Ash-Crow b3e69ea
✨(backend) reconciliation requests update comments
Ash-Crow 2a8e51a
✨(backend) add unit tests for the api view for reconciliation requests
Ash-Crow 5a23ecf
fixup! ✨(frontend) add email validation pages
AntoLC File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| # User account reconciliation | ||
|
|
||
| It is possible to merge user accounts based on their email addresses. | ||
|
|
||
| Docs does not have an internal process to requests, but it allows the import of a CSV from an external form | ||
| (e.g. made with Grist) in the Django admin panel (in "Core" > "User reconciliation CSV imports" > "Add user reconciliation") | ||
|
|
||
| The CSV must contain the following mandatory columns: | ||
|
|
||
| - `active_email`: the email of the user that will remain active after the process. | ||
| - `inactive_email`: the email of the user(s) that will be merged into the active user. It is possible to indicate several emails, so the user only has to make one request even if they have more than two accounts. | ||
| - `id`: a unique row id, so that entries already processed in a previous import are ignored. | ||
|
|
||
| The following columns are optional: `active_email_checked` and `inactive_email_checked` (both must contain `0` (False) or `1` (True), and both default to False.) | ||
| If present, it allows to indicate that the source form has a way to validate that the user making the request actually controls the email addresses, skipping the need to send confirmation emails (cf. below) | ||
|
|
||
| Once the CSV file is processed, this will create entries in "Core" > "User reconciliations" and send verification emails to validate that the user making the request actually controls the email addresses (unless `active_email_checked` and `inactive_email_checked` were set to `1` in the CSV) | ||
|
|
||
| In "Core" > "User reconciliations", an admin can then select all rows they wish to process and check the action "Process selected user reconciliations". Only rows that have the status `ready` and for which both emails have been validated will be processed. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,6 +40,8 @@ | |
| from rest_framework import filters, status, viewsets | ||
| from rest_framework import response as drf_response | ||
| from rest_framework.permissions import AllowAny | ||
| from rest_framework.response import Response | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. line 41 the drf reponse module is imported, you can probably use it instead of making an other import ? |
||
| from rest_framework.views import APIView | ||
|
|
||
| from core import authentication, choices, enums, models | ||
| from core.api.filters import remove_accents | ||
|
|
@@ -249,6 +251,59 @@ def get_me(self, request): | |
| ) | ||
|
|
||
|
|
||
| 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. | ||
| """ | ||
Ash-Crow marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| permission_classes = [AllowAny] | ||
|
|
||
| def get(self, request, user_type, confirmation_id): | ||
| """ | ||
| Check the confirmation ID and mark the corresponding email as checked. | ||
| """ | ||
| try: | ||
| # validate UUID | ||
| uuid_obj = uuid.UUID(str(confirmation_id)) | ||
| except ValueError: | ||
| return Response( | ||
| {"detail": "Badly formatted confirmation id"}, | ||
| status=status.HTTP_400_BAD_REQUEST, | ||
| ) | ||
|
|
||
| if user_type not in ("active", "inactive"): | ||
| return Response( | ||
| {"detail": "Invalid user_type"}, status=status.HTTP_400_BAD_REQUEST | ||
| ) | ||
|
|
||
| lookup = ( | ||
| {"active_email_confirmation_id": uuid_obj} | ||
| if user_type == "active" | ||
| else {"inactive_email_confirmation_id": uuid_obj} | ||
| ) | ||
|
|
||
| try: | ||
| rec = models.UserReconciliation.objects.get(**lookup) | ||
| except models.UserReconciliation.DoesNotExist: | ||
| return Response( | ||
| {"detail": "Reconciliation entry not found"}, | ||
| status=status.HTTP_404_NOT_FOUND, | ||
| ) | ||
|
|
||
| field_name = ( | ||
| "active_email_checked" | ||
| if user_type == "active" | ||
| else "inactive_email_checked" | ||
| ) | ||
| if not getattr(rec, field_name): | ||
| setattr(rec, field_name, True) | ||
| rec.save() | ||
|
|
||
| return Response({"detail": "Confirmation received"}) | ||
|
|
||
|
|
||
| class ResourceAccessViewsetMixin: | ||
| """Mixin with methods common to all access viewsets.""" | ||
|
|
||
|
|
||
178 changes: 178 additions & 0 deletions
178
src/backend/core/migrations/0029_userreconciliationcsvimport_userreconciliation.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| # Generated by Django 5.2.10 on 2026-02-02 16:58 | ||
|
|
||
| import uuid | ||
|
|
||
| import django.db.models.deletion | ||
| from django.conf import settings | ||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
| dependencies = [ | ||
| ("core", "0028_remove_templateaccess_template_and_more"), | ||
| ] | ||
|
|
||
| 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/", verbose_name="CSV file"), | ||
| ), | ||
| ( | ||
| "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", | ||
| "db_table": "impress_user_reconciliation_csv_import", | ||
| }, | ||
| ), | ||
| 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)), | ||
| ( | ||
| "active_email_confirmation_id", | ||
| models.UUIDField( | ||
| default=uuid.uuid4, editable=False, null=True, unique=True | ||
| ), | ||
| ), | ||
| ( | ||
| "inactive_email_confirmation_id", | ||
| models.UUIDField( | ||
| default=uuid.uuid4, editable=False, null=True, unique=True | ||
| ), | ||
| ), | ||
| ( | ||
| "source_unique_id", | ||
| models.CharField( | ||
| blank=True, | ||
| max_length=100, | ||
| null=True, | ||
| verbose_name="Unique ID in the source file", | ||
| ), | ||
| ), | ||
| ( | ||
| "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", | ||
| "db_table": "impress_user_reconciliation", | ||
| "ordering": ["-created_at"], | ||
| }, | ||
| ), | ||
| ] |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be placed under "Unreleased"