feat: add CSV import for student event rankings
Let advisors load preference ranks from a converted CSV with fuzzy matching, known aliases, and a parse-preview-save page instead of relying on the ranking editor. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
using Core.Entities;
|
||||
|
||||
namespace Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Result of parsing a student event ranking CSV.
|
||||
/// </summary>
|
||||
public class StudentEventRankingParseResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Accepted ranking matches, including the raw CSV text and fuzzy scores.
|
||||
/// </summary>
|
||||
public List<StudentEventRankingMatch> Matches { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Unmatched students, unmatched or ambiguous events, and other row-level issues.
|
||||
/// </summary>
|
||||
public List<StudentEventRankingIssue> Issues { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Critical errors that prevented parsing (for example a missing header).
|
||||
/// </summary>
|
||||
public List<string> Errors { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Non-critical warnings about the file as a whole.
|
||||
/// </summary>
|
||||
public List<string> Warnings { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Accepted rankings without match metadata.
|
||||
/// </summary>
|
||||
public IReadOnlyList<StudentEventRanking> Rankings => [.. Matches.Select(m => m.Ranking)];
|
||||
|
||||
/// <summary>
|
||||
/// Number of accepted ranking rows.
|
||||
/// </summary>
|
||||
public int TotalParsed => Matches.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Distinct students who have at least one accepted rank.
|
||||
/// Uses Id when assigned, otherwise first and last name, so unsaved parsed students stay distinct.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Student> StudentsWithAcceptedRanks =>
|
||||
[.. Matches
|
||||
.Select(m => m.Ranking.Student)
|
||||
.GroupBy(s => s.Id != 0 ? $"id:{s.Id}" : $"name:{s.FirstName}|{s.LastName}")
|
||||
.Select(g => g.First())];
|
||||
|
||||
/// <summary>
|
||||
/// True when no critical parse errors were recorded.
|
||||
/// </summary>
|
||||
public bool IsSuccess => Errors.Count == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A successfully matched ranking cell from the CSV.
|
||||
/// </summary>
|
||||
public class StudentEventRankingMatch
|
||||
{
|
||||
public required StudentEventRanking Ranking { get; set; }
|
||||
|
||||
public string RawStudentName { get; set; } = string.Empty;
|
||||
|
||||
public string RawEventName { get; set; } = string.Empty;
|
||||
|
||||
public int StudentScore { get; set; }
|
||||
|
||||
public int EventScore { get; set; }
|
||||
|
||||
public int RowNumber { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A row-level problem encountered while parsing rankings.
|
||||
/// </summary>
|
||||
public class StudentEventRankingIssue
|
||||
{
|
||||
public int RowNumber { get; set; }
|
||||
|
||||
public int Rank { get; set; }
|
||||
|
||||
public string RawStudentName { get; set; } = string.Empty;
|
||||
|
||||
public string RawEventName { get; set; } = string.Empty;
|
||||
|
||||
public StudentEventRankingIssueType IssueType { get; set; }
|
||||
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
public string? SuggestedEventName { get; set; }
|
||||
|
||||
public int? Score { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Types of issues reported while parsing student event rankings.
|
||||
/// </summary>
|
||||
public enum StudentEventRankingIssueType
|
||||
{
|
||||
UnmatchedStudent,
|
||||
UnmatchedEvent,
|
||||
AmbiguousEvent,
|
||||
DuplicateEvent,
|
||||
DuplicateRank,
|
||||
InvalidFormat
|
||||
}
|
||||
Reference in New Issue
Block a user