Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to

## [Unreleased]

### Added

- ✨(backend) add a is_first_connection flag to the User model#1938

## [v4.7.0] - 2026-03-09

### Added
Expand Down
17 changes: 15 additions & 2 deletions src/backend/core/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,21 @@ class UserSerializer(serializers.ModelSerializer):

class Meta:
model = models.User
fields = ["id", "email", "full_name", "short_name", "language"]
read_only_fields = ["id", "email", "full_name", "short_name"]
fields = [
"id",
"email",
"full_name",
"short_name",
"language",
"is_first_connection",
]
read_only_fields = [
"id",
"email",
"full_name",
"short_name",
"is_first_connection",
]

def get_full_name(self, instance):
"""Return the full name of the user."""
Expand Down
20 changes: 20 additions & 0 deletions src/backend/core/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,25 @@ def get_me(self, request):
self.serializer_class(request.user, context=context).data
)

@drf.decorators.action(
detail=False,
methods=["post"],
url_path="onboarding-done",
permission_classes=[permissions.IsAuthenticated],
)
def onboarding_done(self, request):
"""
Allows the frontend to mark the first connection as done for the current user,
e.g. after showing an onboarding message.
"""
if request.user.is_first_connection:
request.user.is_first_connection = False
request.user.save(update_fields=["is_first_connection", "updated_at"])

return drf.response.Response(
{"detail": "Onboarding marked as done."}, status=status.HTTP_200_OK
)


class ReconciliationConfirmView(APIView):
"""API endpoint to confirm user reconciliation emails.
Expand Down Expand Up @@ -2224,6 +2243,7 @@ class DocumentAccessViewSet(
"user__full_name",
"user__email",
"user__language",
"user__is_first_connection",
"document__id",
"document__path",
"document__depth",
Expand Down
32 changes: 32 additions & 0 deletions src/backend/core/migrations/0030_user_is_first_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 5.2.11 on 2026-03-04 14:49

from django.db import migrations, models


def set_is_first_connection_false(apps, schema_editor):
"""Update all existing user.is_first_connection to False."""
user = apps.get_model("core", "User")

user.objects.update(is_first_connection=False)


class Migration(migrations.Migration):
dependencies = [
("core", "0029_userreconciliationcsvimport_userreconciliation"),
]

operations = [
migrations.AddField(
model_name="user",
name="is_first_connection",
field=models.BooleanField(
default=True,
help_text="Whether the user has completed the first connection process.",
verbose_name="first connection status",
),
),
migrations.RunPython(
set_is_first_connection_false,
reverse_code=migrations.RunPython.noop,
),
]
5 changes: 5 additions & 0 deletions src/backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
"Unselect this instead of deleting accounts."
),
)
is_first_connection = models.BooleanField(
_("first connection status"),
default=True,
help_text=_("Whether the user has completed the first connection process."),
)

objects = UserManager()

Expand Down
21 changes: 12 additions & 9 deletions src/backend/core/tests/documents/test_api_document_accesses.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,15 +245,18 @@ def test_api_document_accesses_list_authenticated_related_privileged(
"path": access.document.path,
"depth": access.document.depth,
},
"user": {
"id": str(access.user.id),
"email": access.user.email,
"language": access.user.language,
"full_name": access.user.full_name,
"short_name": access.user.short_name,
}
if access.user
else None,
"user": (
{
"id": str(access.user.id),
"email": access.user.email,
"language": access.user.language,
"full_name": access.user.full_name,
"short_name": access.user.short_name,
"is_first_connection": access.user.is_first_connection,
}
if access.user
else None
),
"max_ancestors_role": None,
"max_role": access.role,
"team": access.team,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import pytest

from core import models


@pytest.mark.django_db
def test_update_blank_title_migration(migrator):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import pycrdt
import pytest

from core import models


@pytest.mark.django_db
def test_populate_attachments_on_all_documents(migrator):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Module testing migration 0030 about adding is_first_connection to user model."""

from django.contrib.auth.hashers import make_password

import factory
import pytest

from core import models


@pytest.mark.django_db
def test_set_is_first_connection_false(migrator):
"""
Test that once the migration adding is_first_connection column to user model is applied
all existing user have the False value.
"""
old_state = migrator.apply_initial_migration(
("core", "0029_userreconciliationcsvimport_userreconciliation")
)
OldUser = old_state.apps.get_model("core", "User")

old_user1 = OldUser.objects.create(
email="email1@example.com", sub="user1", password=make_password("password")
)
old_user2 = OldUser.objects.create(
email="email2@example.com", sub="user2", password=make_password("password")
)

assert hasattr(old_user1, "is_first_connection") is False
assert hasattr(old_user2, "is_first_connection") is False

# # Apply the migration
new_state = migrator.apply_tested_migration(
("core", "0030_user_is_first_connection")
)

NewUser = new_state.apps.get_model("core", "User")

updated_user1 = NewUser.objects.get(id=old_user1.id)

assert updated_user1.is_first_connection is False

updated_user2 = NewUser.objects.get(id=old_user2.id)

assert updated_user2.is_first_connection is False

# create a new user after migration

new_user1 = NewUser.objects.create(
email="email3example.com", sub="user3", password=make_password("password")
)
assert new_user1.is_first_connection is True
29 changes: 29 additions & 0 deletions src/backend/core/tests/test_api_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ def test_api_users_retrieve_me_authenticated():
"full_name": user.full_name,
"language": user.language,
"short_name": user.short_name,
"is_first_connection": True,
}


Expand Down Expand Up @@ -489,9 +490,37 @@ def test_api_users_retrieve_me_authenticated_empty_name():
"full_name": "test_foo",
"language": user.language,
"short_name": "test_foo",
"is_first_connection": True,
}


def test_api_users_retrieve_me_onboarding():
"""
On first connection of a new user, the "is_first_connection" flag should be True.

The frontend can use this flag to trigger specific behavior for first time users,
e.g. showing an onboarding message, and update the flag to False after onboarding is done.
"""
user = factories.UserFactory()

client = APIClient()
client.force_login(user)

# First request: flag should be True
first_response = client.get("/api/v1.0/users/me/")
assert first_response.status_code == 200
assert first_response.json()["is_first_connection"] is True

update_response = client.post("/api/v1.0/users/onboarding-done/")

assert update_response.status_code == 200

# Second request: flag should be False
second_response = client.get("/api/v1.0/users/me/")
assert second_response.status_code == 200
assert second_response.json()["is_first_connection"] is False


def test_api_users_retrieve_anonymous():
"""Anonymous users should not be allowed to retrieve a user."""
client = APIClient()
Expand Down
Loading