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,111 @@
|
||||
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()];
|
||||
}
|
||||
Reference in New Issue
Block a user