Scale unique answer tracking

This commit is contained in:
hz
2026-07-13 02:11:54 -05:00
parent 700da4e415
commit f2c59388eb
23 changed files with 1064 additions and 131 deletions
@@ -15,17 +15,23 @@ public class QuestionsController : ControllerBase
private readonly QuestionAttemptStore _attempts;
private readonly RatingStore _ratings;
private readonly QuestionMediaStore _media;
private readonly AnswerStatisticsStore _statistics;
private readonly ServiceMetrics _metrics;
public QuestionsController(
QuestionStore questions,
QuestionAttemptStore attempts,
RatingStore ratings,
QuestionMediaStore media)
QuestionMediaStore media,
AnswerStatisticsStore statistics,
ServiceMetrics metrics)
{
_questions = questions;
_attempts = attempts;
_ratings = ratings;
_media = media;
_statistics = statistics;
_metrics = metrics;
}
[HttpGet("systems")]
@@ -41,7 +47,7 @@ public class QuestionsController : ControllerBase
public async Task<IActionResult> Search([FromBody] QuestionSearchRequest req)
{
var questions = await _questions.SearchAsync(req, User.IsInRole("SUPER"));
var percentages = await _attempts.GetPercentagesByQuestionAsync(questions.Select(q => q.ExternalId));
var percentages = await _statistics.GetPercentagesByQuestionAsync(questions.Select(q => q.ExternalId));
var questionElos = await _ratings.GetQuestionElosAsync(questions.Select(q => q.ExternalId));
return Ok(questions.Select(q =>
{
@@ -58,7 +64,7 @@ public class QuestionsController : ControllerBase
if (question is null || (!question.IsPublished && !User.IsInRole("SUPER")))
return NotFound();
var percentages = await _attempts.GetPercentagesForQuestionAsync(question.ExternalId);
var percentages = await _statistics.GetPercentagesForQuestionAsync(question.ExternalId);
var questionElo = await _ratings.GetQuestionEloAsync(question.ExternalId);
return Ok(QuestionResponse.FromModel(question, percentages, questionElo));
}
@@ -85,23 +91,58 @@ public class QuestionsController : ControllerBase
if (string.IsNullOrWhiteSpace(userId))
return Unauthorized();
var wasFirstAttempt = await _attempts.RecordFirstAttemptAsync(
var record = await _attempts.RecordFirstAttemptAsync(
question.ExternalId,
userId,
req.ChoiceId.Trim(),
req.ChoiceId.Trim() == question.CorrectChoiceId);
var rating = await _ratings.ApplyFirstResultAsync(
userId,
question,
req.ChoiceId.Trim() == question.CorrectChoiceId,
wasFirstAttempt);
if (record.Inserted)
{
_metrics.AttemptCreated();
try
{
await _statistics.IncrementAsync(record.Attempt);
}
catch
{
_metrics.CounterFailure();
}
}
else
{
_metrics.AttemptDuplicate();
}
var ratingOwner = Guid.NewGuid().ToString("N");
var shouldRate = await _attempts.TryAcquireRatingLeaseAsync(
record.Attempt.Id ?? "",
ratingOwner,
TimeSpan.FromSeconds(30));
RatingChangeResponse rating;
try
{
rating = await _ratings.ApplyFirstResultAsync(
userId,
question,
record.Attempt.IsCorrect,
shouldRate);
if (shouldRate)
await _attempts.MarkRatingAppliedAsync(record.Attempt.Id ?? "", ratingOwner);
}
catch
{
_metrics.RatingFailure();
if (shouldRate)
await _attempts.ReleaseRatingLeaseAsync(record.Attempt.Id ?? "", ratingOwner);
throw;
}
return Ok(new FirstQuestionAttemptResponse
{
QuestionExternalId = question.ExternalId,
ChoiceSelectionPercentages = QuestionResponse.BuildChoiceSelectionPercentages(
question,
await _attempts.GetPercentagesForQuestionAsync(question.ExternalId)),
await _statistics.GetPercentagesForQuestionAsync(question.ExternalId)),
QuestionElo = rating.QuestionElo,
Rating = rating
});
@@ -164,13 +205,36 @@ public class QuestionsController : ControllerBase
if (string.IsNullOrWhiteSpace(userId))
return Unauthorized();
var wasFirstAttempt = await _attempts.RecordFirstAttemptAsync(
var record = await _attempts.RecordFirstAttemptAsync(
question.ExternalId,
userId,
"",
false,
true);
var rating = await _ratings.ApplyFirstResultAsync(userId, question, false, wasFirstAttempt);
if (record.Inserted)
_metrics.AttemptCreated();
else
_metrics.AttemptDuplicate();
var ratingOwner = Guid.NewGuid().ToString("N");
var shouldRate = await _attempts.TryAcquireRatingLeaseAsync(
record.Attempt.Id ?? "",
ratingOwner,
TimeSpan.FromSeconds(30));
RatingChangeResponse rating;
try
{
rating = await _ratings.ApplyFirstResultAsync(userId, question, false, shouldRate);
if (shouldRate)
await _attempts.MarkRatingAppliedAsync(record.Attempt.Id ?? "", ratingOwner);
}
catch
{
_metrics.RatingFailure();
if (shouldRate)
await _attempts.ReleaseRatingLeaseAsync(record.Attempt.Id ?? "", ratingOwner);
throw;
}
return Ok(new SkipQuestionResponse
{
@@ -178,7 +242,7 @@ public class QuestionsController : ControllerBase
CorrectChoiceId = question.CorrectChoiceId,
ChoiceSelectionPercentages = QuestionResponse.BuildChoiceSelectionPercentages(
question,
await _attempts.GetPercentagesForQuestionAsync(question.ExternalId)),
await _statistics.GetPercentagesForQuestionAsync(question.ExternalId)),
QuestionElo = rating.QuestionElo,
Rating = rating
});