feat: add optional student nickname for informal display
Keep legal first and last names for formal lists; show DisplayFirstName on teams, calendars, and import matching so two Josiahs can be told apart. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,6 +17,10 @@ public class Student : IEquatable<Student>
|
||||
[Display(Name = "Last Name")]
|
||||
public string LastName { get; set; } = null!;
|
||||
|
||||
[StringLength(50)]
|
||||
[Display(Name = "Nickname")]
|
||||
public string? Nickname { get; set; }
|
||||
|
||||
[Range(5,12)]
|
||||
[Display(Name = "Grade")]
|
||||
public int Grade { get; set; }
|
||||
@@ -55,6 +59,17 @@ public class Student : IEquatable<Student>
|
||||
|
||||
public string FirstNameLastName => $"{FirstName} {LastName}";
|
||||
|
||||
/// <summary>
|
||||
/// Nickname when set, otherwise <see cref="FirstName"/>.
|
||||
/// </summary>
|
||||
public string DisplayFirstName =>
|
||||
string.IsNullOrWhiteSpace(Nickname) ? FirstName : Nickname.Trim();
|
||||
|
||||
public void NormalizeNickname()
|
||||
{
|
||||
Nickname = string.IsNullOrWhiteSpace(Nickname) ? null : Nickname.Trim();
|
||||
}
|
||||
|
||||
public static Tuple<string, string> ParseNameParts(string fullName)
|
||||
{
|
||||
var match = Match(fullName, @"(.*),\s*(.*)");
|
||||
@@ -67,10 +82,7 @@ public class Student : IEquatable<Student>
|
||||
: new Tuple<string, string>(fullName, string.Empty);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return FirstName;
|
||||
}
|
||||
public override string ToString() => DisplayFirstName;
|
||||
|
||||
public bool VotingDelegate => OfficerRole is Entities.OfficerRole.President or Entities.OfficerRole.VicePresident;
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ public class AssignmentRequirementParser : CsvParserBase
|
||||
{
|
||||
}
|
||||
|
||||
public AssignmentRequirementParser(StreamReader reader, bool ignoreBlankLines = true) : base(reader, ignoreBlankLines)
|
||||
{
|
||||
}
|
||||
|
||||
public AssignmentRequirement[] Parse(ICollection<EventDefinition> events, ICollection<Student> students)
|
||||
{
|
||||
var assumptions = new List<AssignmentRequirement>();
|
||||
@@ -20,7 +24,11 @@ public class AssignmentRequirementParser : CsvParserBase
|
||||
|
||||
var studentArray =
|
||||
studentColumns
|
||||
.Select(c => students.FirstOrDefault(s => s.FirstName == c)).ToArray();
|
||||
.Select(c => students.FirstOrDefault(s =>
|
||||
string.Equals(s.DisplayFirstName, c, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(s.Nickname, c, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(s.FirstName, c, StringComparison.OrdinalIgnoreCase)))
|
||||
.ToArray();
|
||||
|
||||
while (CsvReader.Read())
|
||||
{
|
||||
|
||||
@@ -23,7 +23,14 @@ public static class FuzzyStudentMatcher
|
||||
|
||||
public static int Score(Student student, string name)
|
||||
{
|
||||
var candidates = new[] { student.Name, student.FirstNameLastName, student.LastNameFirstName };
|
||||
var candidates = new[]
|
||||
{
|
||||
student.Name,
|
||||
student.FirstNameLastName,
|
||||
student.LastNameFirstName,
|
||||
student.DisplayFirstName,
|
||||
student.Nickname
|
||||
}.Where(c => !string.IsNullOrWhiteSpace(c));
|
||||
return candidates.Max(candidate => Math.Max(Fuzz.Ratio(candidate, name), Fuzz.TokenSetRatio(candidate, name)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,10 @@ namespace Core.Parsers
|
||||
{
|
||||
}
|
||||
|
||||
public TeamParser(StreamReader reader, bool ignoreBlankLines = true) : base(reader, ignoreBlankLines)
|
||||
{
|
||||
}
|
||||
|
||||
public Team[] Parse(ICollection<EventDefinition> events, ICollection<Student> students)
|
||||
{
|
||||
var teams = new List<Team>();
|
||||
@@ -118,7 +122,9 @@ namespace Core.Parsers
|
||||
{
|
||||
Fuzz.Ratio(s.Name, studentName),
|
||||
Fuzz.Ratio(s.FirstNameLastName, studentName),
|
||||
Fuzz.Ratio(s.FirstName, studentName)
|
||||
Fuzz.Ratio(s.FirstName, studentName),
|
||||
Fuzz.Ratio(s.DisplayFirstName, studentName),
|
||||
string.IsNullOrWhiteSpace(s.Nickname) ? 0 : Fuzz.Ratio(s.Nickname, studentName)
|
||||
}.Max()
|
||||
where rat > 90
|
||||
orderby rat descending
|
||||
|
||||
@@ -34,7 +34,7 @@ public static class StudentNameFormatter
|
||||
if (student == null)
|
||||
return string.Empty;
|
||||
|
||||
var name = student.FirstName;
|
||||
var name = student.DisplayFirstName;
|
||||
|
||||
// Add overlap marker
|
||||
if (options.HasOverlap)
|
||||
|
||||
@@ -107,7 +107,7 @@ public static class TeamStudentNameFormatter
|
||||
if (student == null)
|
||||
return string.Empty;
|
||||
|
||||
var name = student.FirstName;
|
||||
var name = student.DisplayFirstName;
|
||||
|
||||
// Add captain indicator (before overlap/absent markers)
|
||||
if (options.CaptainIndicator != CaptainIndicatorStyle.None && team != null && team.Captain != null && team.Captain.Equals(student))
|
||||
@@ -137,7 +137,7 @@ public static class TeamStudentNameFormatter
|
||||
|
||||
// Get the suffix from StudentNameFormatter (overlap/absent markers)
|
||||
var baseFormatted = StudentNameFormatter.FormatStudentName(student, studentNameOptions);
|
||||
var suffix = baseFormatted.Substring(student.FirstName.Length);
|
||||
var suffix = baseFormatted.Substring(student.DisplayFirstName.Length);
|
||||
|
||||
return name + suffix;
|
||||
}
|
||||
@@ -214,10 +214,10 @@ public static class TeamStudentNameFormatter
|
||||
{
|
||||
OrderingStyle.CaptainFirst => studentsWithCaptainInfo
|
||||
.OrderBy(x => !x.IsCaptain)
|
||||
.ThenBy(x => x.Student.FirstName)
|
||||
.ThenBy(x => x.Student.DisplayFirstName)
|
||||
.Select(x => x.Student),
|
||||
OrderingStyle.Alphabetical => studentsWithCaptainInfo
|
||||
.OrderBy(x => x.Student.FirstName)
|
||||
.OrderBy(x => x.Student.DisplayFirstName)
|
||||
.Select(x => x.Student),
|
||||
OrderingStyle.GradeDescending => studentsWithCaptainInfo
|
||||
.OrderByDescending(x => x.Student.Grade + x.Student.TsaYear)
|
||||
@@ -252,8 +252,8 @@ public static class TeamStudentNameFormatter
|
||||
{
|
||||
OrderingStyle.CaptainFirst => students
|
||||
.OrderBy(s => team.Captain == null || !team.Captain.Equals(s))
|
||||
.ThenBy(s => s.FirstName),
|
||||
OrderingStyle.Alphabetical => students.OrderBy(s => s.FirstName),
|
||||
.ThenBy(s => s.DisplayFirstName),
|
||||
OrderingStyle.Alphabetical => students.OrderBy(s => s.DisplayFirstName),
|
||||
OrderingStyle.GradeDescending => students.OrderByDescending(s => s.Grade + s.TsaYear),
|
||||
_ => students
|
||||
};
|
||||
|
||||
@@ -242,7 +242,9 @@ public static class YearTransitionPlanner
|
||||
// Exact full-name matches first
|
||||
var fullMatches = students.Where(s =>
|
||||
comparer.Equals(s.FirstNameLastName, line) ||
|
||||
comparer.Equals(s.LastNameFirstName, line)).ToList();
|
||||
comparer.Equals(s.LastNameFirstName, line) ||
|
||||
comparer.Equals($"{s.DisplayFirstName} {s.LastName}", line) ||
|
||||
comparer.Equals($"{s.LastName}, {s.DisplayFirstName}", line)).ToList();
|
||||
if (fullMatches.Count > 0)
|
||||
return fullMatches;
|
||||
|
||||
@@ -251,8 +253,10 @@ public static class YearTransitionPlanner
|
||||
return [];
|
||||
|
||||
return students.Where(s =>
|
||||
comparer.Equals(s.FirstName.Trim(), first) &&
|
||||
comparer.Equals(s.LastName.Trim(), last));
|
||||
comparer.Equals(s.LastName.Trim(), last) &&
|
||||
(comparer.Equals(s.FirstName.Trim(), first)
|
||||
|| comparer.Equals(s.DisplayFirstName, first)
|
||||
|| comparer.Equals(s.Nickname?.Trim(), first)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user