177 lines
8.3 KiB
C#
177 lines
8.3 KiB
C#
namespace QuestionsApi.Models;
|
|
|
|
public class QuestionResponse
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string ExternalId { get; set; } = "";
|
|
public string Stem { get; set; } = "";
|
|
public string LeadIn { get; set; } = "";
|
|
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; } = [];
|
|
public string ChoiceSelectionRateBasis { get; set; } = "presented";
|
|
public string Discipline { get; set; } = "";
|
|
public string System { get; set; } = "";
|
|
public string PerformanceCategory { get; set; } = "";
|
|
public int Difficulty { get; set; }
|
|
public int QuestionElo { get; set; } = 1000;
|
|
public List<QuestionLab> Labs { get; set; } = [];
|
|
public List<string> Media { get; set; } = [];
|
|
public List<string> Tags { get; set; } = [];
|
|
public List<QuestionReference> References { get; set; } = [];
|
|
public bool IsBookmarked { get; set; }
|
|
|
|
public static QuestionResponse FromModel(
|
|
Question question,
|
|
Dictionary<string, int>? liveChoiceSelectionPercentages = null,
|
|
int questionElo = 1000,
|
|
bool includeChoicePool = true)
|
|
{
|
|
var choices = includeChoicePool ? question.Choices : SelectLegacyChoices(question);
|
|
var choiceIds = choices.Select(choice => choice.Id).ToHashSet(StringComparer.Ordinal);
|
|
var percentages = BuildChoiceSelectionPercentages(question, liveChoiceSelectionPercentages);
|
|
return new QuestionResponse
|
|
{
|
|
Id = question.Id ?? "",
|
|
ExternalId = question.ExternalId,
|
|
Stem = question.Stem,
|
|
LeadIn = question.LeadIn,
|
|
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(),
|
|
WrongAnswerFollowups = question.WrongAnswerFollowups
|
|
.Where(entry => choiceIds.Contains(entry.Key))
|
|
.ToDictionary(),
|
|
ChoiceSelectionPercentages = percentages
|
|
.Where(entry => choiceIds.Contains(entry.Key))
|
|
.ToDictionary(),
|
|
Discipline = question.Discipline,
|
|
System = question.System,
|
|
PerformanceCategory = RatingCategory.Normalize(question.PerformanceCategory, question.System, question.Discipline),
|
|
Difficulty = question.Difficulty,
|
|
QuestionElo = questionElo,
|
|
Labs = question.Labs,
|
|
Media = question.Media,
|
|
Tags = question.Tags,
|
|
References = question.References
|
|
};
|
|
}
|
|
|
|
public static QuestionResponse FromModelWithChoices(
|
|
Question question,
|
|
IReadOnlyCollection<string> presentedChoiceIds,
|
|
int questionElo = 1000)
|
|
{
|
|
var response = FromModel(question, questionElo: questionElo, includeChoicePool: true);
|
|
var ids = presentedChoiceIds.ToHashSet(StringComparer.Ordinal);
|
|
response.Choices = presentedChoiceIds
|
|
.Select(id => question.Choices.FirstOrDefault(choice => choice.Id == id))
|
|
.Where(choice => choice is not null)
|
|
.Cast<QuestionChoice>()
|
|
.ToList();
|
|
response.ChoiceExplanations = response.ChoiceExplanations.Where(entry => ids.Contains(entry.Key)).ToDictionary();
|
|
response.WrongAnswerFollowups = response.WrongAnswerFollowups.Where(entry => ids.Contains(entry.Key)).ToDictionary();
|
|
response.ChoiceSelectionPercentages = response.ChoiceSelectionPercentages.Where(entry => ids.Contains(entry.Key)).ToDictionary();
|
|
return response;
|
|
}
|
|
|
|
private static List<QuestionChoice> SelectLegacyChoices(Question question)
|
|
{
|
|
if (question.Choices.Count <= 5)
|
|
return question.Choices;
|
|
|
|
var correct = question.Choices.First(choice => choice.Id == question.CorrectChoiceId);
|
|
var distractors = question.Choices
|
|
.Where(choice => choice.Id != question.CorrectChoiceId)
|
|
.OrderBy(_ => Random.Shared.Next())
|
|
.Take(4)
|
|
.Append(correct)
|
|
.OrderBy(_ => Random.Shared.Next())
|
|
.ToList();
|
|
return distractors;
|
|
}
|
|
|
|
public static Dictionary<string, int> BuildChoiceSelectionPercentages(Question question, Dictionary<string, int>? liveChoiceSelectionPercentages)
|
|
{
|
|
if (liveChoiceSelectionPercentages is not { Count: > 0 })
|
|
return question.ChoiceSelectionPercentages;
|
|
|
|
return question.Choices.ToDictionary(
|
|
choice => choice.Id,
|
|
choice => liveChoiceSelectionPercentages.GetValueOrDefault(choice.Id, 0));
|
|
}
|
|
}
|
|
|
|
public class FirstQuestionAttemptResponse
|
|
{
|
|
public string QuestionExternalId { get; set; } = "";
|
|
public Dictionary<string, int> ChoiceSelectionPercentages { get; set; } = [];
|
|
public int QuestionElo { get; set; } = 1000;
|
|
public RatingChangeResponse? Rating { get; set; }
|
|
}
|
|
|
|
public class SkipQuestionResponse
|
|
{
|
|
public string QuestionExternalId { get; set; } = "";
|
|
public string CorrectChoiceId { get; set; } = "";
|
|
public Dictionary<string, int> ChoiceSelectionPercentages { get; set; } = [];
|
|
public int QuestionElo { get; set; } = 1000;
|
|
public RatingChangeResponse? Rating { get; set; }
|
|
}
|
|
|
|
public static class RatingCategory
|
|
{
|
|
public const string Uncategorized = "Uncategorized";
|
|
|
|
public static readonly string[] Categories =
|
|
[
|
|
"Renal/Urinary & Male/Female Systems & Pregnancy",
|
|
"Behavioral Health & Social Sciences: Communication, Ethics, Patient Safety",
|
|
"Immune/Blood & Lymphoreticular/Endocrine/Multisystem",
|
|
"Biostatistics & Epidemiology/Population Health",
|
|
"Musculoskeletal/Skin & Subcutaneous Tissue",
|
|
"Cardiovascular System",
|
|
"Nervous System & Special Senses",
|
|
"Respiratory System",
|
|
"Gastrointestinal System",
|
|
Uncategorized
|
|
];
|
|
|
|
public static string Normalize(string? explicitCategory, string? system, string? discipline)
|
|
{
|
|
foreach (var value in new[] { explicitCategory, system, discipline })
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
continue;
|
|
var normalized = Categories.FirstOrDefault(c => string.Equals(c, value.Trim(), StringComparison.OrdinalIgnoreCase));
|
|
if (normalized is not null)
|
|
return normalized;
|
|
|
|
var lower = value.ToLowerInvariant();
|
|
if (lower.Contains("cardio")) return "Cardiovascular System";
|
|
if (lower.Contains("resp")) return "Respiratory System";
|
|
if (lower.Contains("gastro") || lower.Contains("hepato")) return "Gastrointestinal System";
|
|
if (lower.Contains("neuro") || lower.Contains("special sense")) return "Nervous System & Special Senses";
|
|
if (lower.Contains("renal") || lower.Contains("urinary") || lower.Contains("pregnan") || lower.Contains("reproductive")) return "Renal/Urinary & Male/Female Systems & Pregnancy";
|
|
if (lower.Contains("behavior") || lower.Contains("ethic") || lower.Contains("communication") || lower.Contains("safety")) return "Behavioral Health & Social Sciences: Communication, Ethics, Patient Safety";
|
|
if (lower.Contains("immune") || lower.Contains("blood") || lower.Contains("endocrine") || lower.Contains("multisystem")) return "Immune/Blood & Lymphoreticular/Endocrine/Multisystem";
|
|
if (lower.Contains("biostat") || lower.Contains("epidemi") || lower.Contains("population")) return "Biostatistics & Epidemiology/Population Health";
|
|
if (lower.Contains("musculo") || lower.Contains("skin") || lower.Contains("derm")) return "Musculoskeletal/Skin & Subcutaneous Tissue";
|
|
}
|
|
|
|
return Uncategorized;
|
|
}
|
|
}
|