Add server-side ELO ratings

This commit is contained in:
hz
2026-07-11 02:05:43 -05:00
parent bdd32c9dac
commit c716b16c53
21 changed files with 688 additions and 47 deletions
+1
View File
@@ -19,6 +19,7 @@ public class Question
public Dictionary<string, int> ChoiceSelectionPercentages { get; set; } = [];
public string Discipline { get; set; } = "";
public string System { get; set; } = "";
public string PerformanceCategory { get; set; } = "";
public int Difficulty { get; set; } = 1;
public List<QuestionLab> Labs { get; set; } = [];
public List<string> Media { get; set; } = [];
@@ -13,5 +13,6 @@ public class QuestionAttempt
public string UserId { get; set; } = "";
public string ChoiceId { get; set; } = "";
public bool IsCorrect { get; set; }
public bool IsSkip { get; set; }
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
}
@@ -22,6 +22,7 @@ public class UpsertQuestionRequest
public Dictionary<string, int> ChoiceSelectionPercentages { get; set; } = [];
public string Discipline { get; set; } = "";
public string System { get; set; } = "";
public string PerformanceCategory { get; set; } = "";
public int Difficulty { get; set; } = 1;
public List<QuestionLab> Labs { get; set; } = [];
public List<string> Media { get; set; } = [];
+63 -1
View File
@@ -13,12 +13,17 @@ public class QuestionResponse
public Dictionary<string, int> ChoiceSelectionPercentages { get; set; } = [];
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 static QuestionResponse FromModel(Question question, Dictionary<string, int>? liveChoiceSelectionPercentages = null) => new()
public static QuestionResponse FromModel(
Question question,
Dictionary<string, int>? liveChoiceSelectionPercentages = null,
int questionElo = 1000) => new()
{
Id = question.Id ?? "",
ExternalId = question.ExternalId,
@@ -31,7 +36,9 @@ public class QuestionResponse
ChoiceSelectionPercentages = BuildChoiceSelectionPercentages(question, liveChoiceSelectionPercentages),
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
@@ -52,4 +59,59 @@ 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;
}
}
+48
View File
@@ -0,0 +1,48 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace QuestionsApi.Models;
public class UserRating
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
public string UserId { get; set; } = "";
public int OverallElo { get; set; } = 1000;
public Dictionary<string, int> CategoryElos { get; set; } = [];
public int RatedQuestionCount { get; set; }
public DateTime UpdatedUtc { get; set; } = DateTime.UtcNow;
}
public class QuestionRating
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
public string QuestionExternalId { get; set; } = "";
public int Elo { get; set; } = 1000;
public int AttemptCount { get; set; }
public DateTime UpdatedUtc { get; set; } = DateTime.UtcNow;
}
public class RatingChangeResponse
{
public bool Rated { get; set; }
public string Category { get; set; } = "";
public int UserOverallElo { get; set; }
public int UserOverallDelta { get; set; }
public int UserCategoryElo { get; set; }
public int UserCategoryDelta { get; set; }
public int QuestionElo { get; set; }
public int QuestionEloDelta { get; set; }
}
public class RatingProfileResponse
{
public int OverallElo { get; set; }
public int RatedQuestionCount { get; set; }
public Dictionary<string, int> CategoryElos { get; set; } = [];
}