Sync text zoom preference through the account instead of local settings
Local user://settings.cfg meant zoom level didn't follow a student across devices/browsers. Question bank and CCS text scale now live on the user's study profile in Mongo (QuestionTextScale/CcsTextScale), synced through a new PUT /api/profile/me/text-scale endpoint and the existing preferences GET/cache already used for study preferences. Both question_player.gd and ccs_service.gd read the synced value at startup and react to AccountProfileService.profile_changed so a value pulled down after login (or changed from another session) applies without a restart. Backend validates the 0.85-1.35 range and updates only the field(s) provided via a targeted $set, so it can't clobber the other scale or the unrelated study-profile preferences.
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
using QuestionsApi.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace QuestionsApi.Tests;
|
||||
|
||||
public sealed class UserProfileModelsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(null, null, true)]
|
||||
[InlineData(1.0, 1.0, true)]
|
||||
[InlineData(0.85, 1.35, true)]
|
||||
[InlineData(0.84, null, false)]
|
||||
[InlineData(null, 1.36, false)]
|
||||
public void UpdateTextScaleRequestValidator_EnforcesRange(double? questionScale, double? ccsScale, bool expectedValid)
|
||||
{
|
||||
var request = new UpdateTextScaleRequest { QuestionTextScale = questionScale, CcsTextScale = ccsScale };
|
||||
|
||||
var error = UpdateTextScaleRequestValidator.Validate(request);
|
||||
|
||||
Assert.Equal(expectedValid, error is null);
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,14 @@ public sealed class ProfileController(UserProfileStore profiles, IConfiguration
|
||||
return Ok(await profiles.UpsertAsync(UserId(), request));
|
||||
}
|
||||
|
||||
[HttpPut("me/text-scale")]
|
||||
[Authorize(Roles = "USER,SUPER")]
|
||||
public async Task<IActionResult> UpdateTextScale(UpdateTextScaleRequest request)
|
||||
{
|
||||
if (UpdateTextScaleRequestValidator.Validate(request) is { } error) return BadRequest(error);
|
||||
return Ok(await profiles.UpdateTextScaleAsync(UserId(), request));
|
||||
}
|
||||
|
||||
[HttpGet("me/export")]
|
||||
[Authorize(Roles = "USER,SUPER")]
|
||||
public async Task<IActionResult> Export() => Ok(new
|
||||
|
||||
@@ -13,6 +13,8 @@ public sealed class UserStudyProfile
|
||||
public string PreparationStage { get; set; } = "pre_dedicated";
|
||||
public int WeeklyGoalHours { get; set; } = 10;
|
||||
public string PreferredStudyMode { get; set; } = "mixed";
|
||||
public double QuestionTextScale { get; set; } = 1.0;
|
||||
public double CcsTextScale { get; set; } = 1.0;
|
||||
public int Revision { get; set; }
|
||||
public DateTime UpdatedUtc { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -25,6 +27,12 @@ public sealed class UpdateUserStudyProfileRequest
|
||||
public string PreferredStudyMode { get; set; } = "mixed";
|
||||
}
|
||||
|
||||
public sealed class UpdateTextScaleRequest
|
||||
{
|
||||
public double? QuestionTextScale { get; set; }
|
||||
public double? CcsTextScale { get; set; }
|
||||
}
|
||||
|
||||
public sealed class DeletedUserTombstone
|
||||
{
|
||||
[BsonId]
|
||||
@@ -48,3 +56,18 @@ public static class UserStudyProfileValidator
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class UpdateTextScaleRequestValidator
|
||||
{
|
||||
private const double Min = 0.85;
|
||||
private const double Max = 1.35;
|
||||
|
||||
public static string? Validate(UpdateTextScaleRequest request)
|
||||
{
|
||||
if (request.QuestionTextScale is { } questionScale && (questionScale < Min || questionScale > Max))
|
||||
return "Question text scale must be between 0.85 and 1.35.";
|
||||
if (request.CcsTextScale is { } ccsScale && (ccsScale < Min || ccsScale > Max))
|
||||
return "CCS text scale must be between 0.85 and 1.35.";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,20 @@ public sealed class UserProfileStore
|
||||
new FindOneAndUpdateOptions<UserStudyProfile> { IsUpsert = true, ReturnDocument = ReturnDocument.After });
|
||||
}
|
||||
|
||||
public async Task<UserStudyProfile> UpdateTextScaleAsync(string userId, UpdateTextScaleRequest request)
|
||||
{
|
||||
var update = Builders<UserStudyProfile>.Update.Set(value => value.UpdatedUtc, DateTime.UtcNow);
|
||||
if (request.QuestionTextScale is { } questionScale)
|
||||
update = update.Set(value => value.QuestionTextScale, questionScale);
|
||||
if (request.CcsTextScale is { } ccsScale)
|
||||
update = update.Set(value => value.CcsTextScale, ccsScale);
|
||||
update = update.SetOnInsert(value => value.UserId, userId);
|
||||
return await _profiles.FindOneAndUpdateAsync(
|
||||
value => value.UserId == userId,
|
||||
update,
|
||||
new FindOneAndUpdateOptions<UserStudyProfile> { IsUpsert = true, ReturnDocument = ReturnDocument.After });
|
||||
}
|
||||
|
||||
public Task<bool> IsDeletedAsync(string userId) =>
|
||||
_deletedUsers.Find(value => value.UserId == userId).AnyAsync();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user