Implement nested question remediation and review content
This commit is contained in:
@@ -30,6 +30,9 @@ public sealed class ChoicePoolTests
|
||||
Assert.Equal(9, response.Choices.Count);
|
||||
Assert.Single(response.References);
|
||||
Assert.Equal("https://example.org/guideline", response.References[0].Url);
|
||||
Assert.Equal("Key teaching point", response.KeyTakeaway);
|
||||
Assert.Equal("Exam pearl", response.UsmlePearl);
|
||||
Assert.Single(response.ReviewFollowups);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -54,6 +57,40 @@ public sealed class ChoicePoolTests
|
||||
Assert.Contains("every incorrect choice", QuestionValidator.Validate(request));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OptionalReviewFollowupsRequireValidAuthoredContent()
|
||||
{
|
||||
var request = BuildRequest(5);
|
||||
Assert.Null(QuestionValidator.Validate(request));
|
||||
|
||||
request.ReviewFollowups[0].CorrectChoiceId = "missing";
|
||||
Assert.Contains("reviewFollowups correctChoiceId", QuestionValidator.Validate(request));
|
||||
|
||||
request = BuildRequest(5);
|
||||
request.ReviewFollowups[0].TeachingPoint = "";
|
||||
Assert.Contains("reviewFollowups teachingPoint", QuestionValidator.Validate(request));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LegacyQuestionWithoutReviewContentRemainsValid()
|
||||
{
|
||||
var request = BuildRequest(5);
|
||||
request.KeyTakeaway = "";
|
||||
request.UsmlePearl = "";
|
||||
request.ReviewFollowups = [];
|
||||
|
||||
Assert.Null(QuestionValidator.Validate(request));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActiveSimulationQuestionContractDoesNotExposeReviewContent()
|
||||
{
|
||||
Assert.Null(typeof(ExamSimulationQuestionResponse).GetProperty(nameof(QuestionResponse.Explanation)));
|
||||
Assert.Null(typeof(ExamSimulationQuestionResponse).GetProperty(nameof(QuestionResponse.KeyTakeaway)));
|
||||
Assert.Null(typeof(ExamSimulationQuestionResponse).GetProperty(nameof(QuestionResponse.UsmlePearl)));
|
||||
Assert.Null(typeof(ExamSimulationQuestionResponse).GetProperty(nameof(QuestionResponse.ReviewFollowups)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PublishedQuestionRequiresAtLeastFiveUniqueChoices()
|
||||
{
|
||||
@@ -121,6 +158,9 @@ public sealed class ChoicePoolTests
|
||||
Choices = request.Choices,
|
||||
CorrectChoiceId = request.CorrectChoiceId,
|
||||
Explanation = request.Explanation,
|
||||
KeyTakeaway = request.KeyTakeaway,
|
||||
UsmlePearl = request.UsmlePearl,
|
||||
ReviewFollowups = request.ReviewFollowups,
|
||||
ChoiceExplanations = request.ChoiceExplanations,
|
||||
WrongAnswerFollowups = request.WrongAnswerFollowups,
|
||||
ChoiceSelectionPercentages = request.ChoiceSelectionPercentages,
|
||||
@@ -140,6 +180,22 @@ public sealed class ChoicePoolTests
|
||||
Choices = choices,
|
||||
CorrectChoiceId = "A",
|
||||
Explanation = "Explanation",
|
||||
KeyTakeaway = "Key teaching point",
|
||||
UsmlePearl = "Exam pearl",
|
||||
ReviewFollowups =
|
||||
[
|
||||
new QuestionReviewFollowup
|
||||
{
|
||||
Prompt = "Which statement best reinforces the concept?",
|
||||
Choices =
|
||||
[
|
||||
new QuestionChoice { Id = "A", Text = "Accurate statement" },
|
||||
new QuestionChoice { Id = "B", Text = "Inaccurate statement" }
|
||||
],
|
||||
CorrectChoiceId = "A",
|
||||
TeachingPoint = "Review teaching point"
|
||||
}
|
||||
],
|
||||
ChoiceExplanations = ids.ToDictionary(id => id, id => id == "A" ? "Correct." : "Incorrect."),
|
||||
WrongAnswerFollowups = ids.Where(id => id != "A").ToDictionary(
|
||||
id => id,
|
||||
|
||||
@@ -15,6 +15,9 @@ public class Question
|
||||
public List<QuestionChoice> Choices { get; set; } = [];
|
||||
public string CorrectChoiceId { get; set; } = "";
|
||||
public string Explanation { get; set; } = "";
|
||||
public string KeyTakeaway { get; set; } = "";
|
||||
public string UsmlePearl { get; set; } = "";
|
||||
public List<QuestionReviewFollowup> ReviewFollowups { get; set; } = [];
|
||||
public Dictionary<string, string> ChoiceExplanations { get; set; } = [];
|
||||
public Dictionary<string, WrongAnswerFollowup> WrongAnswerFollowups { get; set; } = [];
|
||||
public Dictionary<string, int> ChoiceSelectionPercentages { get; set; } = [];
|
||||
@@ -53,6 +56,14 @@ public class WrongAnswerFollowup
|
||||
public string TeachingPoint { get; set; } = "";
|
||||
}
|
||||
|
||||
public class QuestionReviewFollowup
|
||||
{
|
||||
public string Prompt { get; set; } = "";
|
||||
public List<QuestionChoice> Choices { get; set; } = [];
|
||||
public string CorrectChoiceId { get; set; } = "";
|
||||
public string TeachingPoint { get; set; } = "";
|
||||
}
|
||||
|
||||
public class QuestionReference
|
||||
{
|
||||
public string Title { get; set; } = "";
|
||||
|
||||
@@ -59,6 +59,9 @@ public class QuestionReportSnapshot
|
||||
public List<QuestionChoice> Choices { get; set; } = [];
|
||||
public string CorrectChoiceId { get; set; } = "";
|
||||
public string Explanation { get; set; } = "";
|
||||
public string KeyTakeaway { get; set; } = "";
|
||||
public string UsmlePearl { get; set; } = "";
|
||||
public List<QuestionReviewFollowup> ReviewFollowups { get; set; } = [];
|
||||
public Dictionary<string, string> ChoiceExplanations { get; set; } = [];
|
||||
public Dictionary<string, WrongAnswerFollowup> WrongAnswerFollowups { get; set; } = [];
|
||||
public List<QuestionLab> Labs { get; set; } = [];
|
||||
|
||||
@@ -21,6 +21,9 @@ public class UpsertQuestionRequest
|
||||
public List<QuestionChoice> Choices { get; set; } = [];
|
||||
public string CorrectChoiceId { get; set; } = "";
|
||||
public string Explanation { get; set; } = "";
|
||||
public string KeyTakeaway { get; set; } = "";
|
||||
public string UsmlePearl { get; set; } = "";
|
||||
public List<QuestionReviewFollowup> ReviewFollowups { get; set; } = [];
|
||||
public Dictionary<string, string> ChoiceExplanations { get; set; } = [];
|
||||
public Dictionary<string, WrongAnswerFollowup> WrongAnswerFollowups { get; set; } = [];
|
||||
public Dictionary<string, int> ChoiceSelectionPercentages { get; set; } = [];
|
||||
|
||||
@@ -9,6 +9,9 @@ public class QuestionResponse
|
||||
public List<QuestionChoice> Choices { get; set; } = [];
|
||||
public string CorrectChoiceId { get; set; } = "";
|
||||
public string Explanation { get; set; } = "";
|
||||
public string KeyTakeaway { get; set; } = "";
|
||||
public string UsmlePearl { get; set; } = "";
|
||||
public List<QuestionReviewFollowup> ReviewFollowups { get; set; } = [];
|
||||
public Dictionary<string, string> ChoiceExplanations { get; set; } = [];
|
||||
public Dictionary<string, WrongAnswerFollowup> WrongAnswerFollowups { get; set; } = [];
|
||||
public Dictionary<string, int> ChoiceSelectionPercentages { get; set; } = [];
|
||||
@@ -41,6 +44,9 @@ public class QuestionResponse
|
||||
Choices = choices,
|
||||
CorrectChoiceId = question.CorrectChoiceId,
|
||||
Explanation = question.Explanation,
|
||||
KeyTakeaway = question.KeyTakeaway,
|
||||
UsmlePearl = question.UsmlePearl,
|
||||
ReviewFollowups = question.ReviewFollowups,
|
||||
ChoiceExplanations = question.ChoiceExplanations
|
||||
.Where(entry => choiceIds.Contains(entry.Key))
|
||||
.ToDictionary(),
|
||||
|
||||
@@ -45,6 +45,9 @@ public class QuestionReportStore
|
||||
.ToList(),
|
||||
CorrectChoiceId = question.CorrectChoiceId,
|
||||
Explanation = question.Explanation,
|
||||
KeyTakeaway = question.KeyTakeaway,
|
||||
UsmlePearl = question.UsmlePearl,
|
||||
ReviewFollowups = question.ReviewFollowups,
|
||||
ChoiceExplanations = new Dictionary<string, string>(question.ChoiceExplanations),
|
||||
WrongAnswerFollowups = question.WrongAnswerFollowups.ToDictionary(
|
||||
item => item.Key,
|
||||
|
||||
@@ -206,6 +206,9 @@ public class QuestionStore
|
||||
Choices = req.Choices,
|
||||
CorrectChoiceId = req.CorrectChoiceId.Trim(),
|
||||
Explanation = req.Explanation.Trim(),
|
||||
KeyTakeaway = req.KeyTakeaway.Trim(),
|
||||
UsmlePearl = req.UsmlePearl.Trim(),
|
||||
ReviewFollowups = req.ReviewFollowups,
|
||||
ChoiceExplanations = req.ChoiceExplanations,
|
||||
WrongAnswerFollowups = req.WrongAnswerFollowups,
|
||||
ChoiceSelectionPercentages = req.ChoiceSelectionPercentages,
|
||||
|
||||
@@ -50,6 +50,21 @@ public static class QuestionValidator
|
||||
return "wrongAnswerFollowups correctChoiceId must match a follow-up choice id";
|
||||
}
|
||||
|
||||
if (req.ReviewFollowups is null) return "reviewFollowups must be an array";
|
||||
if (req.ReviewFollowups.Count > 10) return "questions support at most ten review follow-ups";
|
||||
foreach (var followup in req.ReviewFollowups)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(followup.Prompt)) return "reviewFollowups prompt required";
|
||||
if (string.IsNullOrWhiteSpace(followup.TeachingPoint)) return "reviewFollowups teachingPoint required";
|
||||
if (followup.Choices.Count < 2) return "reviewFollowups require at least two choices";
|
||||
if (followup.Choices.Any(choice => string.IsNullOrWhiteSpace(choice.Id) || string.IsNullOrWhiteSpace(choice.Text)))
|
||||
return "reviewFollowups choice ids and text are required";
|
||||
if (followup.Choices.Select(choice => choice.Id).Distinct(StringComparer.Ordinal).Count() != followup.Choices.Count)
|
||||
return "reviewFollowups choice ids must be unique";
|
||||
if (!followup.Choices.Any(choice => choice.Id == followup.CorrectChoiceId))
|
||||
return "reviewFollowups correctChoiceId must match a follow-up choice id";
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(req.System)) return "system required";
|
||||
if (string.IsNullOrWhiteSpace(req.Discipline)) return "discipline required";
|
||||
if (req.Difficulty is < 1 or > 5) return "difficulty must be between 1 and 5";
|
||||
|
||||
Reference in New Issue
Block a user