From f8c22690d49bc9abb7fe4db9e7f1854f6b7fe98e Mon Sep 17 00:00:00 2001 From: James Kolpack Date: Sun, 11 Jan 2026 21:43:00 -0500 Subject: [PATCH] Add student name formatting utilities and refactor team student name handling This commit introduces two new utility classes: StudentNameFormatter and TeamStudentNameFormatter, which provide methods for formatting student names with options for indicating absence and overlaps. The Team class has been updated to remove the StudentsFirstNames property, and various components across the WebApp have been refactored to utilize the new formatting utilities. This enhances the maintainability and readability of the code while improving the presentation of student names in the UI. --- Core/Entities/Team.cs | 13 - Core/Utility/StudentNameFormatter.cs | 53 ++++ Core/Utility/TeamStudentNameFormatter.cs | 261 ++++++++++++++++++ .../Components/Features/Calendar/Index.razor | 40 +-- .../Features/MeetingSchedule/Index.razor | 55 +++- .../MeetingSchedule/ScheduledTeamsList.razor | 79 +++--- .../UnscheduledStudentsList.razor | 68 +++-- .../Features/Students/Registration.razor | 8 +- .../Teams/Components/TeamToggleSelector.razor | 15 +- WebApp/Components/Features/Teams/Goals.razor | 8 +- .../Components/Features/Teams/Handout.razor | 8 +- WebApp/Components/Features/Teams/Index.razor | 1 + .../Components/Features/Teams/Printout.razor | 16 +- 13 files changed, 504 insertions(+), 121 deletions(-) create mode 100644 Core/Utility/StudentNameFormatter.cs create mode 100644 Core/Utility/TeamStudentNameFormatter.cs diff --git a/Core/Entities/Team.cs b/Core/Entities/Team.cs index 43efc9a..3b57ede 100644 --- a/Core/Entities/Team.cs +++ b/Core/Entities/Team.cs @@ -63,17 +63,4 @@ public class Team { return $"{Event.Name} {(Identifier != null ? $"({Identifier})" : "")}"; } - - public string StudentsFirstNames - { - get - { - return - string.Join(", ", - Students.Select(e => - e.FirstName - + (Captain != null && (Captain.Equals(e)) ? "(Cpt)" : "")) - ); - } - } } \ No newline at end of file diff --git a/Core/Utility/StudentNameFormatter.cs b/Core/Utility/StudentNameFormatter.cs new file mode 100644 index 0000000..5044a5a --- /dev/null +++ b/Core/Utility/StudentNameFormatter.cs @@ -0,0 +1,53 @@ +using Core.Entities; + +namespace Core.Utility; + +/// +/// Utility class for formatting individual student names with overlap and absent markers. +/// +public static class StudentNameFormatter +{ + /// + /// Options for formatting student names. + /// + public record FormatOptions + { + /// + /// Whether the student is absent. If true, adds "(absent)" suffix. Default is false. + /// + public bool IsAbsent { get; init; } = false; + + /// + /// Whether the student has schedule overlaps. If true, adds "*" suffix. Default is false. + /// + public bool HasOverlap { get; init; } = false; + } + + /// + /// Formats a single student name with overlap and absent markers. + /// + /// The student to format. + /// Formatting options. + /// Formatted student name. + public static string FormatStudentName(Student student, FormatOptions options) + { + if (student == null) + return string.Empty; + + var name = student.FirstName; + + // Add overlap marker + if (options.HasOverlap) + { + name += "*"; + } + + // Add absent marker + if (options.IsAbsent) + { + name += " (absent)"; + } + + return name; + } +} diff --git a/Core/Utility/TeamStudentNameFormatter.cs b/Core/Utility/TeamStudentNameFormatter.cs new file mode 100644 index 0000000..d7a838a --- /dev/null +++ b/Core/Utility/TeamStudentNameFormatter.cs @@ -0,0 +1,261 @@ +using Core.Entities; + +namespace Core.Utility; + +/// +/// Utility class for formatting student names for teams with various formatting options. +/// +public static class TeamStudentNameFormatter +{ + /// + /// Options for formatting student names. + /// + public record FormatOptions + { + /// + /// Style for indicating the team captain. Default is None. + /// + public CaptainIndicatorStyle CaptainIndicator { get; init; } = CaptainIndicatorStyle.None; + + /// + /// How to order the students. Default is None (preserve original order). + /// + public OrderingStyle Ordering { get; init; } = OrderingStyle.None; + + /// + /// Whether to add "*" suffix for students with schedule overlaps. Default is false. + /// + public bool MarkOverlaps { get; init; } = false; + + /// + /// Whether to add "(absent)" suffix for absent students. Default is false. + /// + public bool MarkAbsent { get; init; } = false; + + /// + /// Function to determine if a student has overlaps. Required if MarkOverlaps is true. + /// + public Func? HasOverlaps { get; init; } + + /// + /// Collection of absent students. Required if MarkAbsent is true. + /// + public ICollection? AbsentStudents { get; init; } + + /// + /// Whether to only show captain indicator for team events (EventFormat.Team). Default is true. + /// + public bool OnlyTeamEventsForCaptain { get; init; } = true; + } + + /// + /// Style for indicating the team captain. + /// + public enum CaptainIndicatorStyle + { + /// + /// No captain indicator. + /// + None, + + /// + /// Use asterisk (*) for captain. + /// + Star, + + /// + /// Use "(Cpt)" for captain. + /// + Captain + } + + /// + /// Style for ordering students. + /// + public enum OrderingStyle + { + /// + /// Preserve original order. + /// + None, + + /// + /// Captain first, then alphabetical by first name. + /// + CaptainFirst, + + /// + /// Alphabetical by first name. + /// + Alphabetical, + + /// + /// By grade + TSA year descending (highest first). + /// + GradeDescending + } + + /// + /// Formats a single student name with the specified options. + /// + /// The student to format. + /// The team the student belongs to. + /// Formatting options. + /// Formatted student name. + public static string FormatStudentName(Student student, Team team, FormatOptions options) + { + if (student == null) + return string.Empty; + + var name = student.FirstName; + + // Add captain indicator (before overlap/absent markers) + if (options.CaptainIndicator != CaptainIndicatorStyle.None && team != null && team.Captain != null && team.Captain.Equals(student)) + { + var shouldShow = !options.OnlyTeamEventsForCaptain || (team.Event != null && team.Event.EventFormat == EventFormat.Team); + if (shouldShow) + { + name += options.CaptainIndicator switch + { + CaptainIndicatorStyle.Star => "*", + CaptainIndicatorStyle.Captain => "(Cpt)", + _ => string.Empty + }; + } + } + + // Convert collections/functions to booleans for StudentNameFormatter + var hasOverlap = options.MarkOverlaps && options.HasOverlaps != null && options.HasOverlaps(student); + var isAbsent = options.MarkAbsent && options.AbsentStudents != null && options.AbsentStudents.Contains(student); + + // Use StudentNameFormatter for overlap/absent markers (appended after captain indicator) + var studentNameOptions = new StudentNameFormatter.FormatOptions + { + HasOverlap = hasOverlap, + IsAbsent = isAbsent + }; + + // Get the suffix from StudentNameFormatter (overlap/absent markers) + var baseFormatted = StudentNameFormatter.FormatStudentName(student, studentNameOptions); + var suffix = baseFormatted.Substring(student.FirstName.Length); + + return name + suffix; + } + + /// + /// Formats all students for a team as a comma-separated string. + /// + /// The team to format students for. + /// Formatting options. + /// Comma-separated string of formatted student names. + public static string FormatStudentList(Team team, FormatOptions options) + { + if (team?.Students == null || !team.Students.Any()) + return string.Empty; + + var students = ApplyOrdering(team.Students, team, options.Ordering); + + return string.Join(", ", students.Select(s => FormatStudentName(s, team, options))); + } + + /// + /// Formats all students for a team as a list of strings. + /// + /// The team to format students for. + /// Formatting options. + /// List of formatted student names. + public static List FormatStudentListAsList(Team team, FormatOptions options) + { + if (team?.Students == null || !team.Students.Any()) + return []; + + var students = ApplyOrdering(team.Students, team, options.Ordering); + + return students.Select(s => FormatStudentName(s, team, options)).ToList(); + } + + /// + /// Formats all unique students across multiple teams for an event definition. + /// Returns a list of unique student names (by student ID). + /// + /// The event definition. + /// The teams for this event. + /// Formatting options. + /// List of unique formatted student names. + public static List FormatStudentListForEvent(EventDefinition eventDefinition, IEnumerable teams, FormatOptions options) + { + if (eventDefinition == null || teams == null) + return []; + + var teamsList = teams.ToList(); + if (!teamsList.Any()) + return []; + + // Get all unique students from all teams for this event + var allStudents = teamsList + .SelectMany(t => t.Students) + .DistinctBy(s => s.Id) + .ToList(); + + if (!allStudents.Any()) + return []; + + // Determine if each student is a captain in any team + var studentsWithCaptainInfo = allStudents.Select(s => + { + var isCaptain = teamsList.Any(t => t.Captain != null && t.Captain.Equals(s)); + // Find a team this student belongs to for formatting context + var studentTeam = teamsList.FirstOrDefault(t => t.Students.Contains(s)); + return new { Student = s, IsCaptain = isCaptain, Team = studentTeam }; + }).ToList(); + + // Apply ordering + var orderedStudents = options.Ordering switch + { + OrderingStyle.CaptainFirst => studentsWithCaptainInfo + .OrderBy(x => !x.IsCaptain) + .ThenBy(x => x.Student.FirstName) + .Select(x => x.Student), + OrderingStyle.Alphabetical => studentsWithCaptainInfo + .OrderBy(x => x.Student.FirstName) + .Select(x => x.Student), + OrderingStyle.GradeDescending => studentsWithCaptainInfo + .OrderByDescending(x => x.Student.Grade + x.Student.TsaYear) + .Select(x => x.Student), + _ => studentsWithCaptainInfo.Select(x => x.Student) + }; + + // Format names - for event-level formatting, we need to handle captain indicator specially + // since a student might be captain in one team but not another + return orderedStudents.Select(s => + { + var studentInfo = studentsWithCaptainInfo.First(x => x.Student.Equals(s)); + // Use the student's team if available, otherwise create a minimal team for formatting context + var team = studentInfo.Team ?? new Team { Students = [s], Event = eventDefinition }; + + // Create options that handle captain indicator for event-level + var eventOptions = options with + { + CaptainIndicator = studentInfo.IsCaptain && + (!options.OnlyTeamEventsForCaptain || eventDefinition.EventFormat == EventFormat.Team) + ? options.CaptainIndicator + : CaptainIndicatorStyle.None + }; + + return FormatStudentName(s, team, eventOptions); + }).ToList(); + } + + private static IEnumerable ApplyOrdering(IEnumerable students, Team team, OrderingStyle ordering) + { + return ordering switch + { + OrderingStyle.CaptainFirst => students + .OrderBy(s => team.Captain == null || !team.Captain.Equals(s)) + .ThenBy(s => s.FirstName), + OrderingStyle.Alphabetical => students.OrderBy(s => s.FirstName), + OrderingStyle.GradeDescending => students.OrderByDescending(s => s.Grade + s.TsaYear), + _ => students + }; + } +} diff --git a/WebApp/Components/Features/Calendar/Index.razor b/WebApp/Components/Features/Calendar/Index.razor index 81d2297..1feb986 100644 --- a/WebApp/Components/Features/Calendar/Index.razor +++ b/WebApp/Components/Features/Calendar/Index.razor @@ -5,6 +5,7 @@ @using Heron.MudCalendar @using Microsoft.Extensions.Logging @using WebApp.Authentication +@using Core.Utility @inject IEventOccurrenceService EventOccurrenceService @inject ILogger Logger @inject IDialogService DialogService @@ -110,8 +111,15 @@ } // Get student first names for this event definition - var studentFirstNames = occ.EventDefinition != null - ? StudentFirstNames(occ.EventDefinition, teamsByEventId) + var studentFirstNames = occ.EventDefinition != null && teamsByEventId.TryGetValue(occ.EventDefinition.Id, out var teams) + ? TeamStudentNameFormatter.FormatStudentListForEvent( + occ.EventDefinition, + teams, + new TeamStudentNameFormatter.FormatOptions + { + CaptainIndicator = TeamStudentNameFormatter.CaptainIndicatorStyle.Star, + Ordering = TeamStudentNameFormatter.OrderingStyle.Alphabetical + }) : []; var calendarItem = new CalendarEventItem(occ, studentFirstNames); @@ -143,34 +151,6 @@ } } - private static List StudentFirstNames(EventDefinition ed, Dictionary> teamsByEventId) - { - List studentFirstNames = []; - if (ed?.Id == null || !teamsByEventId.TryGetValue(ed.Id, out var teams)) return studentFirstNames; - - // Get all unique student first names from all teams for this event - // Include captain indicator (*) for team events - var allStudents = teams - .SelectMany(t => t.Students) - .DistinctBy(s => s.Id) // Ensure uniqueness by student ID - .Select(s => - { - var isCaptain = teams.Any(t => t.Captain?.Id == s.Id); - var name = s.FirstName; - // Add star for captain in team events (EventFormat == Team) - if (isCaptain && ed.EventFormat == Core.Entities.EventFormat.Team) - { - name += "*"; - } - return name; - }) - .OrderBy(name => name) - .ToList(); - - studentFirstNames = allStudents; - - return studentFirstNames; - } private DateTime GetNextDateWithEvents() { diff --git a/WebApp/Components/Features/MeetingSchedule/Index.razor b/WebApp/Components/Features/MeetingSchedule/Index.razor index 5ad3abe..cbc8755 100644 --- a/WebApp/Components/Features/MeetingSchedule/Index.razor +++ b/WebApp/Components/Features/MeetingSchedule/Index.razor @@ -4,6 +4,7 @@ @using Core.Calculation @using Microsoft.EntityFrameworkCore @using WebApp.Components.Shared.Components +@using Core.Utility @inject IConfiguration Configuration @inject AppDbContext Context @inject ClipboardService ClipboardService @@ -380,21 +381,44 @@ private string FormatStudentList(Team team, TeamScheduleTimeSlot timeslot) { - return string.Join(", ", - team.Students - .OrderBy(e => e == team.Captain) - .ThenBy(e => e.FirstName) - .Select(e => FormatStudentName(e, timeslot))); + return TeamStudentNameFormatter.FormatStudentList( + team, + new TeamStudentNameFormatter.FormatOptions + { + Ordering = TeamStudentNameFormatter.OrderingStyle.CaptainFirst, + MarkOverlaps = true, + HasOverlaps = timeslot.StudentHasOverlaps, + MarkAbsent = true, + AbsentStudents = _absentStudents.ToList() + }); } private string FormatStudentName(Student student, TeamScheduleTimeSlot timeslot) { - var name = student.FirstName; - if (timeslot.StudentHasOverlaps(student)) - name += "*"; - if (_absentStudents.Contains(student)) - name += " (absent)"; - return name; + // Find the team this student belongs to for formatting context + var team = _teams.FirstOrDefault(t => t.Students.Contains(student)); + if (team == null) + { + // No team context, use StudentNameFormatter directly + return StudentNameFormatter.FormatStudentName( + student, + new StudentNameFormatter.FormatOptions + { + HasOverlap = timeslot.StudentHasOverlaps(student), + IsAbsent = _absentStudents.Contains(student) + }); + } + + return TeamStudentNameFormatter.FormatStudentName( + student, + team, + new TeamStudentNameFormatter.FormatOptions + { + MarkOverlaps = true, + HasOverlaps = timeslot.StudentHasOverlaps, + MarkAbsent = true, + AbsentStudents = _absentStudents.ToList() + }); } private void AppendUnscheduledStudents(StringBuilder sb, TeamScheduleTimeSlot timeslot) @@ -407,9 +431,12 @@ foreach (var student in timeslot.UnscheduledStudents) { - var studentName = student.FirstName; - if (_absentStudents.Contains(student)) - studentName += " (absent)"; + var studentName = StudentNameFormatter.FormatStudentName( + student, + new StudentNameFormatter.FormatOptions + { + IsAbsent = _absentStudents.Contains(student) + }); var unassignedTeams = _solution.StudentUnassignedTeams(student); var teamsList = string.Join(", ", unassignedTeams.Select(e => e.ToString())); diff --git a/WebApp/Components/Features/MeetingSchedule/ScheduledTeamsList.razor b/WebApp/Components/Features/MeetingSchedule/ScheduledTeamsList.razor index 7c033e5..70045ce 100644 --- a/WebApp/Components/Features/MeetingSchedule/ScheduledTeamsList.razor +++ b/WebApp/Components/Features/MeetingSchedule/ScheduledTeamsList.razor @@ -1,5 +1,6 @@ @using Core.Calculation @using WebApp.Models +@using Core.Utility @TimeSlotName @@ -8,31 +9,54 @@ var scheduledTeamIds = ScheduledTeams.Select(t => t.Id).ToHashSet(); var removed = !scheduledTeamIds.Contains(team.Id); - - - - @team - - @foreach (var student in team.Students) - { - var overlap = StudentHasOverlaps(student); - var isAbsent = AbsentStudents.Contains(student); - var color = overlap ? Color.Warning : Color.Default; - var suffix = GetStudentSuffix(overlap, isAbsent); - - if (student != team.Students.First()) - { - , + + + + + @{ + var teamMembers = TeamStudentNameFormatter.FormatStudentList( + team, + new TeamStudentNameFormatter.FormatOptions + { + Ordering = TeamStudentNameFormatter.OrderingStyle.None + }); } - -  @student.FirstName@suffix - - } - + +
+ + @team + +
+
+
+ + @foreach (var student in team.Students) + { + var overlap = StudentHasOverlaps(student); + var chipColor = overlap ? Color.Warning : Color.Default; + var formattedName = TeamStudentNameFormatter.FormatStudentName( + student, + team, + new TeamStudentNameFormatter.FormatOptions + { + MarkOverlaps = true, + HasOverlaps = StudentHasOverlaps, + MarkAbsent = true, + AbsentStudents = AbsentStudents.ToList() + }); + + + @formattedName + + } + + } @@ -54,11 +78,4 @@ [Parameter] public EventCallback OnToggleTeam { get; set; } - - private string GetStudentSuffix(bool overlap, bool isAbsent) - { - var suffix = overlap ? "*" : ""; - suffix += isAbsent ? " (absent)" : ""; - return suffix; - } } diff --git a/WebApp/Components/Features/MeetingSchedule/UnscheduledStudentsList.razor b/WebApp/Components/Features/MeetingSchedule/UnscheduledStudentsList.razor index 12b90d8..04b6f19 100644 --- a/WebApp/Components/Features/MeetingSchedule/UnscheduledStudentsList.razor +++ b/WebApp/Components/Features/MeetingSchedule/UnscheduledStudentsList.razor @@ -1,4 +1,5 @@ @using Core.Calculation +@using Core.Utility @if (UnscheduledStudents.Any()) { @@ -6,32 +7,51 @@ @foreach (var student in UnscheduledStudents) { - var isAbsent = AbsentStudents.Contains(student); - - @student.FirstName@(isAbsent ? " (absent)" : "")  - - @foreach (var unassignedTeam in UnassignedTeams(student)) - { - var isPossibleAddition = PossibleAdditions.Contains(unassignedTeam, new TeamIdComparer()); - var scheduledTeamIds = ScheduledTeams.Select(t => t.Id).ToHashSet(); - var isScheduled = scheduledTeamIds.Contains(unassignedTeam.Id); - var color = isPossibleAddition ? Color.Success : Color.Default; - - if (unassignedTeam != UnassignedTeams(student).First()) - { - , - } - - - - @unassignedTeam - + @{ + var formattedName = StudentNameFormatter.FormatStudentName( + student, + new StudentNameFormatter.FormatOptions + { + IsAbsent = AbsentStudents.Contains(student) + }); } + + + + @formattedName + + + + @foreach (var unassignedTeam in UnassignedTeams(student)) + { + var isPossibleAddition = PossibleAdditions.Contains(unassignedTeam, new TeamIdComparer()); + var scheduledTeamIds = ScheduledTeams.Select(t => t.Id).ToHashSet(); + var isScheduled = scheduledTeamIds.Contains(unassignedTeam.Id); + var chipColor = isPossibleAddition ? Color.Success : Color.Default; + var teamMembers = TeamStudentNameFormatter.FormatStudentList( + unassignedTeam, + new TeamStudentNameFormatter.FormatOptions + { + Ordering = TeamStudentNameFormatter.OrderingStyle.None + }); + + +
+ + @if (isScheduled) + { + + } + @unassignedTeam + +
+
+ } +
+
}
diff --git a/WebApp/Components/Features/Students/Registration.razor b/WebApp/Components/Features/Students/Registration.razor index 414823e..af22628 100644 --- a/WebApp/Components/Features/Students/Registration.razor +++ b/WebApp/Components/Features/Students/Registration.razor @@ -4,6 +4,7 @@ @using WebApp.Models @using WebApp.Components.Shared.Components @using Core.Validation +@using Core.Utility @inject AppDbContext Context @inject WebApp.LocalStorageService LocalStorage @inject ValidationService ValidationService @@ -83,7 +84,12 @@ @foreach (var team in teamsToDisplay) { var isCaptain = team.Captain != null && team.Captain.Equals(context.Item.Student); - var teamMembers = string.Join(", ", team.Students.Select(s => s.FirstName)); + var teamMembers = TeamStudentNameFormatter.FormatStudentList( + team, + new TeamStudentNameFormatter.FormatOptions + { + Ordering = TeamStudentNameFormatter.OrderingStyle.None + }); e.Event.Name)) { - - - @team.ToString() + +
+ @team.ToString() @if (ShowEventAttributes) { } - +
} diff --git a/WebApp/Components/Features/Teams/Goals.razor b/WebApp/Components/Features/Teams/Goals.razor index 22e29d6..5521906 100644 --- a/WebApp/Components/Features/Teams/Goals.razor +++ b/WebApp/Components/Features/Teams/Goals.razor @@ -3,6 +3,7 @@ @using Microsoft.EntityFrameworkCore @using WebApp.Models @using WebApp.Components.Shared.Components +@using Core.Utility @inject IConfiguration Configuration @inject AppDbContext Context @@ -35,7 +36,12 @@ else @if (team.Event.EventFormat == EventFormat.Team) { - (Team: @string.Join(", ", team.Students.OrderByDescending(e => e.Grade + e.TsaYear).Select(e => e.FirstName))) + (Team: @TeamStudentNameFormatter.FormatStudentList( + team, + new TeamStudentNameFormatter.FormatOptions + { + Ordering = TeamStudentNameFormatter.OrderingStyle.GradeDescending + })) } diff --git a/WebApp/Components/Features/Teams/Handout.razor b/WebApp/Components/Features/Teams/Handout.razor index 1e82e16..cd7aa6b 100644 --- a/WebApp/Components/Features/Teams/Handout.razor +++ b/WebApp/Components/Features/Teams/Handout.razor @@ -3,6 +3,7 @@ @using Microsoft.EntityFrameworkCore @using WebApp.Models @using WebApp.Components.Shared.Components +@using Core.Utility @inject IConfiguration Configuration @inject AppDbContext Context @@ -57,7 +58,12 @@ else { - Team Members: @string.Join(", ", team.Students.OrderByDescending(e => e.Grade + e.TsaYear).Select(e => e.FirstName)) + Team Members: @TeamStudentNameFormatter.FormatStudentList( + team, + new TeamStudentNameFormatter.FormatOptions + { + Ordering = TeamStudentNameFormatter.OrderingStyle.GradeDescending + }) } diff --git a/WebApp/Components/Features/Teams/Index.razor b/WebApp/Components/Features/Teams/Index.razor index 094678f..7ab0322 100644 --- a/WebApp/Components/Features/Teams/Index.razor +++ b/WebApp/Components/Features/Teams/Index.razor @@ -128,6 +128,7 @@ = Context.Teams .AsNoTracking() .Include(e => e.Event) + .Include(e => e.Captain) .Include(e => e.Students) .ThenInclude(e => e.EventRankings); diff --git a/WebApp/Components/Features/Teams/Printout.razor b/WebApp/Components/Features/Teams/Printout.razor index f93cdc8..6d3eea2 100644 --- a/WebApp/Components/Features/Teams/Printout.razor +++ b/WebApp/Components/Features/Teams/Printout.razor @@ -3,6 +3,7 @@ @using Microsoft.EntityFrameworkCore @using WebApp.Models @using WebApp.Components.Shared.Components +@using Core.Utility @inject IConfiguration Configuration @inject AppDbContext Context @@ -56,7 +57,13 @@ else .Find(e => e.EventDefinition == context.Event)?.Rank ?? int.MaxValue; - @student.Name @if(context?.Captain == student) { (Cpt)} + @TeamStudentNameFormatter.FormatStudentName( + student, + context, + new TeamStudentNameFormatter.FormatOptions + { + CaptainIndicator = TeamStudentNameFormatter.CaptainIndicatorStyle.Captain + }) } else @@ -191,7 +198,12 @@ else } - @string.Join(", ", team.Students.Where(e => e != context).Select(e => e.FirstName)) + @TeamStudentNameFormatter.FormatStudentList( + new Team { Students = team.Students.Where(e => e != context).ToList(), Event = team.Event }, + new TeamStudentNameFormatter.FormatOptions + { + Ordering = TeamStudentNameFormatter.OrderingStyle.None + })