Files
chapter-organizer/WebApp/Services/StudentEventRankingSaveService.cs
T
poprhythmandCursor 4c91db37c2 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>
2026-08-28 23:41:13 -04:00

112 lines
3.7 KiB
C#

using Core.Entities;
using Core.Models;
using Data;
using Microsoft.EntityFrameworkCore;
namespace WebApp.Services;
/// <summary>
/// Replaces event rankings for students present in a successful ranking import.
/// </summary>
public class StudentEventRankingSaveService : IStudentEventRankingSaveService
{
private readonly AppDbContext _context;
private readonly ILogger<StudentEventRankingSaveService> _logger;
public StudentEventRankingSaveService(AppDbContext context, ILogger<StudentEventRankingSaveService> logger)
{
_context = context;
_logger = logger;
}
/// <inheritdoc />
public async Task<IReadOnlyList<string>> GetStudentsWithExistingRankingsAsync(
StudentEventRankingParseResult parseResult,
CancellationToken cancellationToken = default)
{
var studentIds = GetStudentIds(parseResult);
if (studentIds.Count == 0)
return [];
return await _context.Students
.AsNoTracking()
.Where(s => studentIds.Contains(s.Id) && s.EventRankings.Any())
.OrderBy(s => s.LastName)
.ThenBy(s => s.FirstName)
.Select(s => s.FirstName + " " + s.LastName)
.ToListAsync(cancellationToken);
}
/// <inheritdoc />
public async Task<StudentEventRankingSaveResult> SaveAsync(
StudentEventRankingParseResult parseResult,
CancellationToken cancellationToken = default)
{
var studentIds = GetStudentIds(parseResult);
if (studentIds.Count == 0)
return new StudentEventRankingSaveResult();
var students = await _context.Students
.Include(s => s.EventRankings)
.Where(s => studentIds.Contains(s.Id))
.ToListAsync(cancellationToken);
var eventIds = parseResult.Matches
.Select(m => m.Ranking.EventDefinition.Id)
.Where(id => id != 0)
.Distinct()
.ToList();
var events = await _context.Events
.Where(e => eventIds.Contains(e.Id))
.ToDictionaryAsync(e => e.Id, cancellationToken);
var matchesByStudent = parseResult.Matches
.Where(m => m.Ranking.Student.Id != 0)
.GroupBy(m => m.Ranking.Student.Id)
.ToDictionary(g => g.Key, g => g.ToList());
var rankingsSaved = 0;
foreach (var student in students)
{
if (!matchesByStudent.TryGetValue(student.Id, out var matches))
continue;
student.EventRankings.Clear();
foreach (var match in matches.OrderBy(m => m.Ranking.Rank))
{
if (!events.TryGetValue(match.Ranking.EventDefinition.Id, out var eventDefinition))
{
_logger.LogWarning(
"Skipping ranking for student {StudentId}: event {EventId} was not found",
student.Id,
match.Ranking.EventDefinition.Id);
continue;
}
student.EventRankings.Add(new StudentEventRanking
{
Student = student,
EventDefinition = eventDefinition,
Rank = match.Ranking.Rank
});
rankingsSaved++;
}
}
await _context.SaveChangesAsync(cancellationToken);
return new StudentEventRankingSaveResult
{
StudentsUpdated = students.Count,
RankingsSaved = rankingsSaved
};
}
private static List<int> GetStudentIds(StudentEventRankingParseResult parseResult) =>
[.. parseResult.Matches
.Select(m => m.Ranking.Student.Id)
.Where(id => id != 0)
.Distinct()];
}