Redesign exam and CCS experiences
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using System.Text;
|
||||
using QuestionsApi.Models;
|
||||
|
||||
namespace QuestionsApi.Services;
|
||||
@@ -37,6 +38,8 @@ public class ExamSimulationStore
|
||||
Builders<ExamSimulation>.IndexKeys.Ascending(item => item.UserId).Ascending(item => item.State).Descending(item => item.CompletedUtc)));
|
||||
_simulations.Indexes.CreateOne(new CreateIndexModel<ExamSimulation>(
|
||||
Builders<ExamSimulation>.IndexKeys.Ascending(item => item.UserId).Descending(item => item.CreatedUtc)));
|
||||
_simulations.Indexes.CreateOne(new CreateIndexModel<ExamSimulation>(
|
||||
Builders<ExamSimulation>.IndexKeys.Ascending(item => item.UserId).Ascending(item => item.State).Descending(item => item.CreatedUtc).Descending(item => item.Id)));
|
||||
_simulations.Indexes.CreateOne(new CreateIndexModel<ExamSimulation>(
|
||||
Builders<ExamSimulation>.IndexKeys.Ascending(item => item.UpdatedUtc)));
|
||||
_outcomes.Indexes.CreateOne(new CreateIndexModel<Step3Outcome>(
|
||||
@@ -200,22 +203,91 @@ public class ExamSimulationStore
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<ExamSimulationHistoryPage> GetHistoryAsync(string userId, int limit, DateTime? before)
|
||||
public async Task<ExamSimulationHistoryPage> GetHistoryAsync(
|
||||
string userId,
|
||||
int limit,
|
||||
string status = "all",
|
||||
string sort = "newest",
|
||||
string? cursor = null,
|
||||
DateTime? legacyBefore = null)
|
||||
{
|
||||
limit = Math.Clamp(limit, 1, 50);
|
||||
var filter = Builders<ExamSimulation>.Filter.Eq(item => item.UserId, userId);
|
||||
if (before is not null)
|
||||
filter &= Builders<ExamSimulation>.Filter.Lt(item => item.CreatedUtc, before.Value);
|
||||
var simulations = await _simulations.Find(filter).SortByDescending(item => item.CreatedUtc).Limit(limit + 1).ToListAsync();
|
||||
status = status.Trim().ToLowerInvariant();
|
||||
sort = sort.Trim().ToLowerInvariant();
|
||||
if (status is not ("all" or "completed" or "abandoned"))
|
||||
throw new ArgumentException("The simulation-history status is invalid.");
|
||||
if (sort is not ("newest" or "oldest"))
|
||||
throw new ArgumentException("The simulation-history sort order is invalid.");
|
||||
|
||||
var filters = Builders<ExamSimulation>.Filter;
|
||||
var filter = filters.Eq(item => item.UserId, userId);
|
||||
filter &= status switch
|
||||
{
|
||||
"completed" => filters.Eq(item => item.State, ExamSimulationStates.Completed),
|
||||
"abandoned" => filters.Eq(item => item.State, ExamSimulationStates.Abandoned),
|
||||
_ => filters.In(item => item.State, [ExamSimulationStates.Completed, ExamSimulationStates.Abandoned])
|
||||
};
|
||||
|
||||
HistoryCursor? decodedCursor = null;
|
||||
if (!string.IsNullOrWhiteSpace(cursor))
|
||||
{
|
||||
if (!TryDecodeHistoryCursor(cursor, out decodedCursor))
|
||||
throw new ArgumentException("The simulation-history cursor is invalid.");
|
||||
var dateComparison = sort == "newest"
|
||||
? filters.Lt(item => item.CreatedUtc, decodedCursor!.CreatedUtc)
|
||||
: filters.Gt(item => item.CreatedUtc, decodedCursor!.CreatedUtc);
|
||||
var idComparison = sort == "newest"
|
||||
? filters.Lt(item => item.Id, decodedCursor!.Id)
|
||||
: filters.Gt(item => item.Id, decodedCursor!.Id);
|
||||
filter &= dateComparison | (filters.Eq(item => item.CreatedUtc, decodedCursor!.CreatedUtc) & idComparison);
|
||||
}
|
||||
else if (legacyBefore is not null)
|
||||
{
|
||||
filter &= filters.Lt(item => item.CreatedUtc, legacyBefore.Value);
|
||||
}
|
||||
|
||||
var sortDefinition = sort == "newest"
|
||||
? Builders<ExamSimulation>.Sort.Descending(item => item.CreatedUtc).Descending(item => item.Id)
|
||||
: Builders<ExamSimulation>.Sort.Ascending(item => item.CreatedUtc).Ascending(item => item.Id);
|
||||
var simulations = await _simulations.Find(filter).Sort(sortDefinition).Limit(limit + 1).ToListAsync();
|
||||
var hasMore = simulations.Count > limit;
|
||||
if (hasMore) simulations.RemoveAt(simulations.Count - 1);
|
||||
var last = simulations.LastOrDefault();
|
||||
return new ExamSimulationHistoryPage
|
||||
{
|
||||
Items = simulations.Select(ToHistorySummary).ToList(),
|
||||
NextBefore = hasMore && simulations.Count > 0 ? simulations[^1].CreatedUtc : null
|
||||
NextBefore = sort == "newest" && hasMore && last is not null ? last.CreatedUtc : null,
|
||||
NextCursor = hasMore && last is not null ? EncodeHistoryCursor(last.CreatedUtc, last.Id ?? "") : null
|
||||
};
|
||||
}
|
||||
|
||||
public static string EncodeHistoryCursor(DateTime createdUtc, string id) =>
|
||||
Convert.ToBase64String(Encoding.UTF8.GetBytes($"{createdUtc.ToUniversalTime().Ticks}|{id}"));
|
||||
|
||||
public static bool TryDecodeHistoryCursor(string value, out HistoryCursor? cursor)
|
||||
{
|
||||
cursor = null;
|
||||
try
|
||||
{
|
||||
var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(value));
|
||||
var parts = decoded.Split('|', 2);
|
||||
if (parts.Length != 2 || !long.TryParse(parts[0], out var ticks) || !ObjectId.TryParse(parts[1], out _))
|
||||
return false;
|
||||
cursor = new HistoryCursor(new DateTime(ticks, DateTimeKind.Utc), parts[1]);
|
||||
return true;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record HistoryCursor(DateTime CreatedUtc, string Id);
|
||||
|
||||
public static ExamSimulationHistorySummary ToHistorySummary(ExamSimulation item) => new()
|
||||
{
|
||||
Id = item.Id ?? "",
|
||||
@@ -228,6 +300,7 @@ public class ExamSimulationStore
|
||||
PredictedScore = item.Score?.PredictedScore,
|
||||
RangeLow = item.Score?.RangeLow,
|
||||
RangeHigh = item.Score?.RangeHigh,
|
||||
PassProbability = item.Score?.PassProbability,
|
||||
Correct = item.Score?.Correct,
|
||||
Total = item.Score?.Total ?? item.Items.Count
|
||||
};
|
||||
@@ -306,6 +379,8 @@ public class ExamSimulationStore
|
||||
CurrentBlock = simulation.CurrentBlock,
|
||||
BreakSecondsRemaining = simulation.BreakSecondsRemaining,
|
||||
BlockDeadlineUtc = simulation.BlockDeadlineUtc,
|
||||
CreatedUtc = simulation.CreatedUtc,
|
||||
UpdatedUtc = simulation.UpdatedUtc,
|
||||
Responses = simulation.Responses,
|
||||
Score = simulation.Score,
|
||||
BlueprintTargets = simulation.BlueprintTargets,
|
||||
|
||||
Reference in New Issue
Block a user