(backend) persist user preferences for room defaults on the User model

Add attributes on the User model to persist per-user preferences for
the default link access level and the default room configuration.

The frontend will let users update these preferences and then reuse
them when generating a link through the webapp.

Persisting them on the backend (rather than in application memory
only) ensures the preferences survive across sessions and devices.
This commit is contained in:
lebaudantoine
2026-08-03 11:32:27 +02:00
parent ca56ae87c2
commit 2e509eff28
7 changed files with 185 additions and 1 deletions
+20 -1
View File
@@ -31,9 +31,28 @@ class UserSerializer(serializers.ModelSerializer):
class Meta:
model = models.User
fields = ["id", "email", "full_name", "short_name", "timezone", "language"]
fields = [
"id",
"email",
"full_name",
"short_name",
"timezone",
"language",
"default_room_access_level",
"default_room_configuration",
]
read_only_fields = ["id", "email", "full_name", "short_name"]
def validate_default_room_configuration(self, value):
"""Validate the default room configuration against the RoomConfiguration schema."""
if value is None or value == {}:
return value
try:
RoomConfiguration.model_validate(value)
except PydanticValidationError as e:
raise serializers.ValidationError(e.errors()) from e
return value
class UserLightSerializer(serializers.ModelSerializer):
"""Serialize users with limited fields."""
@@ -0,0 +1,23 @@
# Generated by Django 5.2.14 on 2026-08-03 13:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0021_recording_external_process_id_alter_recording_status'),
]
operations = [
migrations.AddField(
model_name='user',
name='default_room_access_level',
field=models.CharField(blank=True, choices=[('public', 'Public Access'), ('trusted', 'Trusted Access'), ('restricted', 'Restricted Access')], help_text='Access level applied by default to new rooms created by this user. When empty, the instance default is used.', max_length=50, null=True, verbose_name='default room access level'),
),
migrations.AddField(
model_name='user',
name='default_room_configuration',
field=models.JSONField(blank=True, default=dict, help_text='Configurations applied by default to new rooms created by this user.', verbose_name='default room configuration'),
),
]
+19
View File
@@ -189,6 +189,25 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
default=settings.TIME_ZONE,
help_text=_("The timezone in which the user wants to see times."),
)
default_room_access_level = models.CharField(
max_length=50,
choices=RoomAccessLevel.choices,
blank=True,
null=True,
verbose_name=_("default room access level"),
help_text=_(
"Access level applied by default to new rooms created by this user. "
"When empty, the instance default is used."
),
)
default_room_configuration = models.JSONField(
blank=True,
default=dict,
verbose_name=_("default room configuration"),
help_text=_(
"Configurations applied by default to new rooms created by this user."
),
)
is_device = models.BooleanField(
_("device"),
default=False,
@@ -453,6 +453,8 @@ def test_api_rooms_retrieve_administrators(
{
"id": str(other_user_access.id),
"user": {
"default_room_access_level": None,
"default_room_configuration": {},
"id": str(other_user_access.user.id),
"email": other_user_access.user.email,
"full_name": other_user_access.user.full_name,
@@ -466,6 +468,8 @@ def test_api_rooms_retrieve_administrators(
{
"id": str(user_access.id),
"user": {
"default_room_access_level": None,
"default_room_configuration": {},
"id": str(user_access.user.id),
"email": user_access.user.email,
"full_name": user_access.user.full_name,
+2
View File
@@ -119,6 +119,8 @@ def test_api_users_retrieve_me_authenticated(settings):
assert response.status_code == 200
assert response.json() == {
"default_room_access_level": None,
"default_room_configuration": {},
"id": str(user.id),
"email": user.email,
"full_name": user.full_name,
@@ -0,0 +1,111 @@
"""
Test the default room preferences exposed on the users API.
"""
import pytest
from rest_framework.test import APIClient
from core import factories
pytestmark = pytest.mark.django_db
def test_api_users_me_includes_default_room_preferences():
"""The "me" endpoint should expose the user's default room preferences."""
user = factories.UserFactory(
default_room_access_level="restricted",
default_room_configuration={"everyone_can_mute": False},
)
client = APIClient()
client.force_login(user)
response = client.get("/api/v1.0/users/me/")
assert response.status_code == 200
content = response.json()
assert content["default_room_access_level"] == "restricted"
assert content["default_room_configuration"] == {"everyone_can_mute": False}
def test_api_users_update_default_room_preferences():
"""Users should be able to update their own default room preferences."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
response = client.patch(
f"/api/v1.0/users/{user.id!s}/",
{
"default_room_access_level": "trusted",
"default_room_configuration": {
"can_publish_sources": ["microphone", "camera"],
"everyone_can_mute": False,
},
},
format="json",
)
assert response.status_code == 200
user.refresh_from_db()
assert user.default_room_access_level == "trusted"
assert user.default_room_configuration == {
"can_publish_sources": ["microphone", "camera"],
"everyone_can_mute": False,
}
def test_api_users_update_default_room_access_level_invalid():
"""An invalid access level should be rejected."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
response = client.patch(
f"/api/v1.0/users/{user.id!s}/",
{"default_room_access_level": "invalid"},
format="json",
)
assert response.status_code == 400
user.refresh_from_db()
assert user.default_room_access_level is None
def test_api_users_update_default_room_configuration_invalid():
"""An invalid room configuration should be rejected."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
response = client.patch(
f"/api/v1.0/users/{user.id!s}/",
{"default_room_configuration": {"unknown_field": True}},
format="json",
)
assert response.status_code == 400
user.refresh_from_db()
assert user.default_room_configuration == {}
def test_api_users_update_other_user_default_room_preferences_forbidden():
"""Users should not be able to update someone else's preferences."""
user = factories.UserFactory()
other_user = factories.UserFactory()
client = APIClient()
client.force_login(user)
response = client.patch(
f"/api/v1.0/users/{other_user.id!s}/",
{"default_room_access_level": "restricted"},
format="json",
)
assert response.status_code == 403
other_user.refresh_from_db()
assert other_user.default_room_access_level is None
@@ -1,4 +1,8 @@
import { BackendLanguage } from '@/utils/languages'
import type {
ApiAccessLevel,
RoomConfiguration,
} from '@/features/rooms/api/ApiRoom'
export type ApiUser = {
id: string
@@ -7,4 +11,6 @@ export type ApiUser = {
last_name: string
language: BackendLanguage
timezone: string
default_room_access_level?: ApiAccessLevel | null
default_room_configuration?: RoomConfiguration | null
}