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>
112 lines
3.7 KiB
C#
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()];
|
|
}
|