✨(backend) add a is_first_connection flag to the User model
Backend part of #1796. This changes allows to display an onboarding modal the first time that the get_me() API view is called. I originally tried to check if `User.last_login` was `None`, but it is updated as soon as the user is logged, so I chose to create a flag on the model.
This commit is contained in:
@@ -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."""
|
||||
|
||||
@@ -318,6 +318,25 @@ class UserViewSet(
|
||||
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.
|
||||
@@ -2224,6 +2243,7 @@ class DocumentAccessViewSet(
|
||||
"user__full_name",
|
||||
"user__email",
|
||||
"user__language",
|
||||
"user__is_first_connection",
|
||||
"document__id",
|
||||
"document__path",
|
||||
"document__depth",
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import pytest
|
||||
|
||||
from core import models
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_blank_title_migration(migrator):
|
||||
|
||||
-2
@@ -7,8 +7,6 @@ from django.core.files.storage import default_storage
|
||||
import pycrdt
|
||||
import pytest
|
||||
|
||||
from core import models
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_populate_attachments_on_all_documents(migrator):
|
||||
|
||||
@@ -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="[email protected]", sub="user1", password=make_password("password")
|
||||
)
|
||||
old_user2 = OldUser.objects.create(
|
||||
email="[email protected]", 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
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user