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,260 @@
|
||||
using System.Text;
|
||||
using Core.Entities;
|
||||
using Core.Models;
|
||||
using Core.Parsers;
|
||||
using Tests.Builders;
|
||||
|
||||
namespace Tests.Parsers;
|
||||
|
||||
[TestFixture]
|
||||
public class StudentEventRankingParser_Tests
|
||||
{
|
||||
private Student _aria = null!;
|
||||
private EventDefinition _videoGame = null!;
|
||||
private EventDefinition _coding = null!;
|
||||
private EventDefinition _jss = null!;
|
||||
private EventDefinition _digitalPhoto = null!;
|
||||
private EventDefinition _biotech = null!;
|
||||
private EventDefinition _inventions = null!;
|
||||
private EventDefinition _techBowl = null!;
|
||||
private EventDefinition _techDesign = null!;
|
||||
private List<Student> _students = null!;
|
||||
private List<EventDefinition> _events = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
BuilderExtensions.ResetAllBuilders();
|
||||
|
||||
_aria = StudentBuilder.Create("Aria", "Chittenden").Build();
|
||||
_videoGame = EventDefinitionBuilder.Team("Video Game Design", 2, 6).WithShortName("Video Game").Build();
|
||||
_coding = EventDefinitionBuilder.Team("Coding", 2, 2).WithShortName("Coding").Build();
|
||||
_jss = EventDefinitionBuilder.Team("Junior Solar Sprint", 2, 4).WithShortName("JSS").Build();
|
||||
_digitalPhoto = EventDefinitionBuilder.Individual("Digital Photography").WithShortName("Digital Photo").Build();
|
||||
_biotech = EventDefinitionBuilder.Team("Biotechnology", 2, 6).WithShortName("Biotech").Build();
|
||||
_inventions = EventDefinitionBuilder.Team("Inventions & Innovations", 3, 6).WithShortName("I&I").Build();
|
||||
_techBowl = EventDefinitionBuilder.Team("Tech Bowl", 3, 3).WithShortName("Tech Bowl").Build();
|
||||
_techDesign = EventDefinitionBuilder.Team("Technical Design", 2, 2).WithShortName("Tech Design").Build();
|
||||
|
||||
_students = [_aria];
|
||||
_events = [_videoGame, _coding, _jss, _digitalPhoto, _biotech, _inventions, _techBowl, _techDesign];
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_LastCommaFirst_MatchesStudent()
|
||||
{
|
||||
var result = ParseCsv("""
|
||||
Student Name,1
|
||||
"Chittenden, Aria",Coding
|
||||
""", _students, _events);
|
||||
|
||||
Assert.That(result.IsSuccess, Is.True);
|
||||
Assert.That(result.Matches, Has.Count.EqualTo(1));
|
||||
Assert.That(result.Matches[0].Ranking.Student, Is.SameAs(_aria));
|
||||
Assert.That(result.Matches[0].Ranking.EventDefinition, Is.SameAs(_coding));
|
||||
Assert.That(result.Matches[0].Ranking.Rank, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_FirstLast_MatchesStudent()
|
||||
{
|
||||
var result = ParseCsv("""
|
||||
Student Name,1
|
||||
Aria Chittenden,Coding
|
||||
""", _students, _events);
|
||||
|
||||
Assert.That(result.IsSuccess, Is.True);
|
||||
Assert.That(result.Matches, Has.Count.EqualTo(1));
|
||||
Assert.That(result.Matches[0].Ranking.Student, Is.SameAs(_aria));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_ShortName_MatchesEvent()
|
||||
{
|
||||
var result = ParseCsv("""
|
||||
Student Name,1,2,3
|
||||
Aria Chittenden,Video Game,JSS,I&I
|
||||
""", _students, _events);
|
||||
|
||||
Assert.That(result.Issues, Is.Empty);
|
||||
Assert.That(result.Matches, Has.Count.EqualTo(3));
|
||||
Assert.That(result.Matches.Select(m => m.Ranking.EventDefinition), Is.EqualTo(new[] { _videoGame, _jss, _inventions }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_NearMissFullName_MatchesEvent()
|
||||
{
|
||||
var result = ParseCsv("""
|
||||
Student Name,1
|
||||
Aria Chittenden,Digital Photo
|
||||
""", _students, _events);
|
||||
|
||||
Assert.That(result.Matches, Has.Count.EqualTo(1));
|
||||
Assert.That(result.Matches[0].Ranking.EventDefinition, Is.SameAs(_digitalPhoto));
|
||||
Assert.That(result.Matches[0].EventScore, Is.GreaterThanOrEqualTo(StudentEventRankingParser.EventMatchThreshold));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_BiotechShortName_MatchesBiotechnology()
|
||||
{
|
||||
var result = ParseCsv("""
|
||||
Student Name,1
|
||||
Aria Chittenden,Biotech
|
||||
""", _students, _events);
|
||||
|
||||
Assert.That(result.Matches, Has.Count.EqualTo(1));
|
||||
Assert.That(result.Matches[0].Ranking.EventDefinition, Is.SameAs(_biotech));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_UnmatchedStudent_IsReported()
|
||||
{
|
||||
var result = ParseCsv("""
|
||||
Student Name,1
|
||||
Nobody Here,Coding
|
||||
""", _students, _events);
|
||||
|
||||
Assert.That(result.Matches, Is.Empty);
|
||||
Assert.That(result.Issues, Has.Count.EqualTo(1));
|
||||
Assert.That(result.Issues[0].IssueType, Is.EqualTo(StudentEventRankingIssueType.UnmatchedStudent));
|
||||
Assert.That(result.Issues[0].RawStudentName, Is.EqualTo("Nobody Here"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_SolarRacer_MatchesJuniorSolarSprint()
|
||||
{
|
||||
var result = ParseCsv("""
|
||||
Student Name,1
|
||||
Aria Chittenden,Solar Racer
|
||||
""", _students, _events);
|
||||
|
||||
Assert.That(result.Issues, Is.Empty);
|
||||
Assert.That(result.Matches, Has.Count.EqualTo(1));
|
||||
Assert.That(result.Matches[0].Ranking.EventDefinition, Is.SameAs(_jss));
|
||||
}
|
||||
|
||||
[TestCase("Solar Racer", "Junior Solar Sprint")]
|
||||
[TestCase("Solar Race", "Junior Solar Sprint")]
|
||||
[TestCase("Challenging Tech", "Challenging Technology Issues")]
|
||||
[TestCase("Digital Photo", "Digital Photography")]
|
||||
[TestCase("Forensics", "Forensic Technology")]
|
||||
[TestCase("Micro Controller", "Microcontroller Design")]
|
||||
[TestCase("Med Tech", "Medical Technology")]
|
||||
[TestCase("Innovations & Inventions", "Inventions & Innovations")]
|
||||
[TestCase("Inventions and Innovations", "Inventions & Innovations")]
|
||||
[TestCase("Systems Control Tech", "System Control Technology")]
|
||||
[TestCase("Systems Control Technology", "System Control Technology")]
|
||||
[TestCase("Structural Eng", "Structural Engineering")]
|
||||
[TestCase("Drone Challenge", "Drone Challenge (UAV)")]
|
||||
[TestCase("Robotics", "TSA Robotics")]
|
||||
[TestCase("Audio Podcast", "Audio Podcasting")]
|
||||
public void Parse_KnownAlias_MatchesOfficialEvent(string alias, string officialName)
|
||||
{
|
||||
var events = TestEntityHandler.GetEvents().ToList();
|
||||
events.AddRange(
|
||||
[
|
||||
EventDefinitionBuilder.Team("Drone Challenge (UAV)", 2, 6).WithShortName("Drone").Build(),
|
||||
EventDefinitionBuilder.Team("TSA Robotics", 2, 6).WithShortName("Robotics").Build(),
|
||||
EventDefinitionBuilder.Team("Audio Podcasting", 2, 6).WithShortName("Podcasting").Build()
|
||||
]);
|
||||
|
||||
var result = ParseCsv($"""
|
||||
Student Name,1
|
||||
Aria Chittenden,{alias}
|
||||
""", _students, events);
|
||||
|
||||
Assert.That(result.Issues, Is.Empty, $"Expected '{alias}' to match '{officialName}'");
|
||||
Assert.That(result.Matches, Has.Count.EqualTo(1));
|
||||
Assert.That(result.Matches[0].Ranking.EventDefinition.Name, Is.EqualTo(officialName));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_UnmatchedEvent_IsReported()
|
||||
{
|
||||
var result = ParseCsv("""
|
||||
Student Name,1
|
||||
Aria Chittenden,Underwater Basket Weaving
|
||||
""", _students, _events);
|
||||
|
||||
Assert.That(result.Matches, Is.Empty);
|
||||
Assert.That(result.Issues, Has.Count.EqualTo(1));
|
||||
Assert.That(result.Issues[0].IssueType, Is.EqualTo(StudentEventRankingIssueType.UnmatchedEvent));
|
||||
Assert.That(result.Issues[0].RawEventName, Is.EqualTo("Underwater Basket Weaving"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_AmbiguousEvent_IsReported()
|
||||
{
|
||||
var result = ParseCsv("""
|
||||
Student Name,1
|
||||
Aria Chittenden,Tech
|
||||
""", _students, _events);
|
||||
|
||||
Assert.That(result.Matches, Is.Empty);
|
||||
Assert.That(result.Issues, Has.Count.EqualTo(1));
|
||||
Assert.That(result.Issues[0].IssueType, Is.EqualTo(StudentEventRankingIssueType.AmbiguousEvent));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_RankColumnsBeyondSix_AreRead()
|
||||
{
|
||||
var result = ParseCsv("""
|
||||
Student Name,1,2,3,4,5,6,7
|
||||
Aria Chittenden,Coding,,,,,,JSS
|
||||
""", _students, _events);
|
||||
|
||||
Assert.That(result.Matches, Has.Count.EqualTo(2));
|
||||
Assert.That(result.Matches.Select(m => m.Ranking.Rank), Is.EqualTo(new[] { 1, 7 }));
|
||||
Assert.That(result.Matches[1].Ranking.EventDefinition, Is.SameAs(_jss));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_DuplicateEvent_IsReported()
|
||||
{
|
||||
var result = ParseCsv("""
|
||||
Student Name,1,2
|
||||
Aria Chittenden,Coding,Coding
|
||||
""", _students, _events);
|
||||
|
||||
Assert.That(result.Matches, Has.Count.EqualTo(1));
|
||||
Assert.That(result.Issues, Has.Count.EqualTo(1));
|
||||
Assert.That(result.Issues[0].IssueType, Is.EqualTo(StudentEventRankingIssueType.DuplicateEvent));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_MissingStudentNameHeader_IsError()
|
||||
{
|
||||
var result = ParseCsv("""
|
||||
Name,1
|
||||
Aria Chittenden,Coding
|
||||
""", _students, _events);
|
||||
|
||||
Assert.That(result.IsSuccess, Is.False);
|
||||
Assert.That(result.Errors, Is.Not.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_2024RankingsFile_MatchesKnownStudentsAndEvents()
|
||||
{
|
||||
var events = TestEntityHandler.GetEvents();
|
||||
var students = TestEntityHandler.GetStudents(events);
|
||||
var rankings = TestEntityHandler.GetStudentEventRankings(students, events);
|
||||
|
||||
Assert.That(students, Has.Length.EqualTo(29));
|
||||
Assert.That(rankings, Is.Not.Empty);
|
||||
Assert.That(rankings.Select(r => r.Student.FirstNameLastName).Distinct().Count(), Is.EqualTo(29));
|
||||
Assert.That(rankings.All(r => r.EventDefinition is not null), Is.True);
|
||||
Assert.That(rankings.All(r => r.Rank is >= 1 and <= 10), Is.True);
|
||||
Assert.That(rankings.Count(r => r.Student.FirstName == "First26"), Is.EqualTo(7));
|
||||
}
|
||||
|
||||
private static StudentEventRankingParseResult ParseCsv(
|
||||
string csv,
|
||||
ICollection<Student> students,
|
||||
ICollection<EventDefinition> events)
|
||||
{
|
||||
using var reader = new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(csv)));
|
||||
using var parser = new StudentEventRankingParser(reader);
|
||||
return parser.Parse(students, events);
|
||||
}
|
||||
}
|
||||
@@ -69,9 +69,9 @@ public static class TestEntityHandler
|
||||
|
||||
public static StudentEventRanking[] GetStudentEventRankings(Student[] students, EventDefinition[] events)
|
||||
{
|
||||
var fileInfo = FileUtility.GetContentFile(ContentDirectory, "2025 Student Event Rankings.csv");
|
||||
var fileInfo = FileUtility.GetContentFile(ContentDirectory, "2024 Student Event Rankings.csv");
|
||||
|
||||
var rankingParser = new StudentEventRankingParser(fileInfo);
|
||||
return rankingParser.Parse(students, events);
|
||||
using var rankingParser = new StudentEventRankingParser(fileInfo);
|
||||
return [.. rankingParser.Parse(students, events).Rankings];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user