feat: import leftover student fields into notes and show them on the roster
Store leftover CSV columns on hidden student notes, move catalog import to /events/import, and persist Students index columns from Chapter Settings. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
using Core.Parsers;
|
||||
|
||||
namespace Tests.Parsers;
|
||||
|
||||
[TestFixture]
|
||||
public class StudentImportCsvTemplate_Tests
|
||||
{
|
||||
[Test]
|
||||
public void Build_IncludesRosterAndDefaultLeftoverColumns()
|
||||
{
|
||||
var csv = StudentImportCsvTemplate.Build();
|
||||
var header = csv.Split('\n')[0].TrimEnd('\r');
|
||||
|
||||
Assert.That(header, Does.StartWith("Student Name,Grade,TSA year,State ID,Regional ID,National ID"));
|
||||
Assert.That(header, Does.Contain("Interview Time"));
|
||||
Assert.That(header, Does.Contain("Application"));
|
||||
Assert.That(csv, Does.Contain("\"Last, First\""));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Build_UsesConfiguredLeftoverFields_AndSkipsReserved()
|
||||
{
|
||||
var csv = StudentImportCsvTemplate.Build(["Teacher Rec 1", "Grade", " Application "]);
|
||||
var header = csv.Split('\n')[0].TrimEnd('\r');
|
||||
|
||||
Assert.That(header, Does.Contain("Teacher Rec 1"));
|
||||
Assert.That(header, Does.Contain("Application"));
|
||||
Assert.That(header.Split(',').Count(c => c.Equals("Grade", StringComparison.OrdinalIgnoreCase)), Is.EqualTo(1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
using System.Text;
|
||||
using Core.Notes;
|
||||
using Core.Parsers;
|
||||
using Tests.Builders;
|
||||
|
||||
namespace Tests.Parsers;
|
||||
|
||||
[TestFixture]
|
||||
public class StudentNotesFieldParser_Tests
|
||||
{
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
BuilderExtensions.ResetAllBuilders();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_MatchesStudentAndReportsUnmatched()
|
||||
{
|
||||
var aria = StudentBuilder.Create("Aria", "Chittenden").Build();
|
||||
const string csv = """
|
||||
Student Name,Interview Time,Application
|
||||
"Chittenden, Aria",3:20-3:35,x
|
||||
Nobody Here,1:00-1:15,x
|
||||
""";
|
||||
|
||||
var result = Parse(csv, [aria]);
|
||||
|
||||
Assert.That(result.IsSuccess, Is.True);
|
||||
Assert.That(result.Matches, Has.Count.EqualTo(1));
|
||||
Assert.That(result.Matches[0].Student, Is.SameAs(aria));
|
||||
Assert.That(result.Matches[0].Merge.Changed, Is.True);
|
||||
Assert.That(result.Issues, Has.Count.EqualTo(1));
|
||||
Assert.That(result.Issues[0].RawStudentName, Is.EqualTo("Nobody Here"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_SameExistingNote_HasNoChanges()
|
||||
{
|
||||
var aria = StudentBuilder.Create("Aria", "Chittenden").Build();
|
||||
var existing = ImportedFieldsTable.Merge(null,
|
||||
[
|
||||
new ImportedField("Interview Time", "3:20-3:35"),
|
||||
new ImportedField("Application", "Yes")
|
||||
]).Markdown;
|
||||
|
||||
const string csv = """
|
||||
Student Name,Interview Time,Application
|
||||
Aria Chittenden,3:20-3:35,x
|
||||
""";
|
||||
|
||||
var result = Parse(csv, [aria], new Dictionary<int, string?> { [aria.Id] = existing });
|
||||
|
||||
Assert.That(result.Matches, Has.Count.EqualTo(1));
|
||||
Assert.That(result.Matches[0].Merge.Changed, Is.False);
|
||||
Assert.That(result.StudentsWithChanges, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_MissingStudentNameColumn_IsError()
|
||||
{
|
||||
var aria = StudentBuilder.Create("Aria", "Chittenden").Build();
|
||||
const string csv = """
|
||||
Name,Interview Time
|
||||
Aria Chittenden,3:20-3:35
|
||||
""";
|
||||
|
||||
var result = Parse(csv, [aria]);
|
||||
|
||||
Assert.That(result.IsSuccess, Is.False);
|
||||
Assert.That(result.Errors, Has.Some.Contains("Student Name"));
|
||||
Assert.That(result.Matches, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_PreservesFieldsNotInCsv()
|
||||
{
|
||||
var aria = StudentBuilder.Create("Aria", "Chittenden").Build();
|
||||
var existing = ImportedFieldsTable.Merge(null,
|
||||
[
|
||||
new ImportedField("Interview Time", "1:00-1:15"),
|
||||
new ImportedField("Teacher Rec 1", "Fuqua")
|
||||
]).Markdown;
|
||||
|
||||
const string csv = """
|
||||
Student Name,Interview Time
|
||||
Aria Chittenden,3:20-3:35
|
||||
""";
|
||||
|
||||
var result = Parse(csv, [aria], new Dictionary<int, string?> { [aria.Id] = existing });
|
||||
var fields = ImportedFieldsTable.ParseFields(result.Matches[0].Merge.Markdown);
|
||||
|
||||
Assert.That(result.Matches[0].Merge.Changed, Is.True);
|
||||
Assert.That(fields.Single(f => f.Name == "Interview Time").Value, Is.EqualTo("3:20-3:35"));
|
||||
Assert.That(fields.Single(f => f.Name == "Teacher Rec 1").Value, Is.EqualTo("Fuqua"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_X_BecomesYes_AndBlankFieldsRoundTrip()
|
||||
{
|
||||
var aria = StudentBuilder.Create("Aria", "Chittenden").Build();
|
||||
const string csv = """
|
||||
Student Name,Application,Teacher Rec 3
|
||||
Aria Chittenden,x,
|
||||
""";
|
||||
|
||||
var first = Parse(csv, [aria]);
|
||||
var markdown = first.Matches[0].Merge.Markdown;
|
||||
|
||||
Assert.That(ImportedFieldsTable.GetFieldValue(markdown, "Application"), Is.EqualTo("Yes"));
|
||||
Assert.That(ImportedFieldsTable.GetFieldValue(markdown, "Teacher Rec 3"), Is.EqualTo(string.Empty));
|
||||
|
||||
var second = Parse(csv, [aria], new Dictionary<int, string?> { [aria.Id] = markdown });
|
||||
Assert.That(second.Matches[0].Merge.Changed, Is.False);
|
||||
Assert.That(second.StudentsWithChanges, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_DuplicateStudentRows_LastValueWins_OneMatch()
|
||||
{
|
||||
var aria = StudentBuilder.Create("Aria", "Chittenden").Build();
|
||||
const string csv = """
|
||||
Student Name,Interview Time,Teacher Rec 1
|
||||
Aria Chittenden,1:00-1:15,Fuqua
|
||||
"Chittenden, Aria",3:20-3:35,Young
|
||||
""";
|
||||
|
||||
var result = Parse(csv, [aria]);
|
||||
var fields = ImportedFieldsTable.ParseFields(result.Matches[0].Merge.Markdown);
|
||||
|
||||
Assert.That(result.Matches, Has.Count.EqualTo(1));
|
||||
Assert.That(fields.Single(f => f.Name == "Interview Time").Value, Is.EqualTo("3:20-3:35"));
|
||||
Assert.That(fields.Single(f => f.Name == "Teacher Rec 1").Value, Is.EqualTo("Young"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_NameOnlyCsv_WarnsAndMatchesWithoutFieldChanges()
|
||||
{
|
||||
var aria = StudentBuilder.Create("Aria", "Chittenden").Build();
|
||||
const string csv = """
|
||||
Student Name
|
||||
Aria Chittenden
|
||||
""";
|
||||
|
||||
var result = Parse(csv, [aria]);
|
||||
|
||||
Assert.That(result.IsSuccess, Is.True);
|
||||
Assert.That(result.Warnings, Has.Some.Contains("No leftover field columns"));
|
||||
Assert.That(result.Matches, Has.Count.EqualTo(1));
|
||||
Assert.That(result.Matches[0].Merge.Changed, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_ExcludesRosterAndRankingColumns()
|
||||
{
|
||||
var aria = StudentBuilder.Create("Aria", "Chittenden").Build();
|
||||
const string csv = """
|
||||
Student Name,Grade,TSA year,State ID,Regional ID,National ID,Officer,1,2,TOTAL # OF EVENTS,Interview Time,Application
|
||||
Aria Chittenden,6,1st,,,,,Coding,JSS,2,3:20-3:35,x
|
||||
""";
|
||||
|
||||
var result = Parse(csv, [aria]);
|
||||
var fields = ImportedFieldsTable.ParseFields(result.Matches[0].Merge.Markdown);
|
||||
|
||||
Assert.That(result.FieldNames, Is.EquivalentTo(["Interview Time", "Application"]));
|
||||
Assert.That(fields.Select(f => f.Name), Is.EquivalentTo(["Interview Time", "Application"]));
|
||||
Assert.That(fields.Single(f => f.Name == "Application").Value, Is.EqualTo("Yes"));
|
||||
Assert.That(fields.Any(f => f.Name is "Grade" or "1" or "Officer"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_RosterOnlyCsv_HasNoLeftoverFields()
|
||||
{
|
||||
var aria = StudentBuilder.Create("Aria", "Chittenden").Build();
|
||||
const string csv = """
|
||||
Student Name,Grade,TSA year,State ID,Regional ID,National ID
|
||||
Aria Chittenden,6,1st,,,
|
||||
""";
|
||||
|
||||
var result = Parse(csv, [aria]);
|
||||
|
||||
Assert.That(result.FieldNames, Is.Empty);
|
||||
Assert.That(result.Matches[0].Merge.Changed, Is.False);
|
||||
Assert.That(result.Warnings, Has.Some.Contains("No leftover field columns"));
|
||||
}
|
||||
|
||||
private static Core.Models.StudentNotesImportResult Parse(
|
||||
string csv,
|
||||
ICollection<Core.Entities.Student> students,
|
||||
IReadOnlyDictionary<int, string?>? notes = null)
|
||||
{
|
||||
using var reader = new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(csv)));
|
||||
using var parser = new StudentNotesFieldParser(reader);
|
||||
return parser.Parse(students, notes ?? new Dictionary<int, string?>());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user