feat: import leftover student fields into notes and show them on the roster
Store leftover CSV columns on hidden student notes, move catalog import to /events/import, and persist Students index columns from Chapter Settings. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -35,4 +35,19 @@ public interface INoteNamingService
|
||||
/// <param name="noteTitle">The note title to check</param>
|
||||
/// <returns>True if the note is a meeting note, false otherwise</returns>
|
||||
bool IsMeetingNote(string noteTitle);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the title for a student note. Format: "#Student:{id}"
|
||||
/// </summary>
|
||||
string GetStudentNoteTitle(int studentId);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a note title is a student note.
|
||||
/// </summary>
|
||||
bool IsStudentNote(string? noteTitle);
|
||||
|
||||
/// <summary>
|
||||
/// Parses the student id from a student note title.
|
||||
/// </summary>
|
||||
bool TryParseStudentNoteId(string? noteTitle, out int studentId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using Core.Entities;
|
||||
using Core.Models;
|
||||
|
||||
namespace Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Parses student field CSVs and merges them into markdown notes.
|
||||
/// </summary>
|
||||
public interface IStudentNotesImportService
|
||||
{
|
||||
StudentNotesImportResult Parse(
|
||||
Stream stream,
|
||||
ICollection<Student> students,
|
||||
IReadOnlyDictionary<int, string?> existingNotesByStudentId);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ public class NoteNamingService : INoteNamingService
|
||||
{
|
||||
private const string PageNotePrefix = "#";
|
||||
private const string MeetingNotePrefix = "#Meeting Notes";
|
||||
private const string StudentNotePrefix = "#Student:";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetMeetingNoteTitle(DateTime meetingDate)
|
||||
@@ -47,4 +48,26 @@ public class NoteNamingService : INoteNamingService
|
||||
|
||||
return noteTitle.StartsWith(MeetingNotePrefix, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetStudentNoteTitle(int studentId) => $"{StudentNotePrefix}{studentId}";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsStudentNote(string? noteTitle)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(noteTitle))
|
||||
return false;
|
||||
|
||||
return noteTitle.StartsWith(StudentNotePrefix, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryParseStudentNoteId(string? noteTitle, out int studentId)
|
||||
{
|
||||
studentId = 0;
|
||||
if (!IsStudentNote(noteTitle))
|
||||
return false;
|
||||
|
||||
return int.TryParse(noteTitle.AsSpan(StudentNotePrefix.Length), out studentId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
using Core.Models;
|
||||
|
||||
namespace Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Decides which parsed student notes should be created or updated.
|
||||
/// Unchanged merges are omitted so a re-import does not write history.
|
||||
/// </summary>
|
||||
public static class StudentNotesImportPlan
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds persist actions from a parse result. Only matches with field changes are included.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<StudentNotePersistAction> Create(
|
||||
StudentNotesImportResult parseResult,
|
||||
IReadOnlySet<int> studentIdsWithExistingNotes)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(parseResult);
|
||||
ArgumentNullException.ThrowIfNull(studentIdsWithExistingNotes);
|
||||
|
||||
List<StudentNotePersistAction> actions = [];
|
||||
foreach (var match in parseResult.Matches)
|
||||
{
|
||||
if (!match.Merge.Changed)
|
||||
continue;
|
||||
|
||||
actions.Add(new StudentNotePersistAction
|
||||
{
|
||||
StudentId = match.Student.Id,
|
||||
Markdown = match.Merge.Markdown,
|
||||
Kind = studentIdsWithExistingNotes.Contains(match.Student.Id)
|
||||
? StudentNotePersistKind.Update
|
||||
: StudentNotePersistKind.Create
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
}
|
||||
|
||||
public enum StudentNotePersistKind
|
||||
{
|
||||
Create,
|
||||
Update
|
||||
}
|
||||
|
||||
public class StudentNotePersistAction
|
||||
{
|
||||
public required int StudentId { get; init; }
|
||||
|
||||
public required string Markdown { get; init; }
|
||||
|
||||
public required StudentNotePersistKind Kind { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Core.Entities;
|
||||
using Core.Models;
|
||||
using Core.Parsers;
|
||||
|
||||
namespace Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Wraps <see cref="StudentNotesFieldParser"/> for stream-based import.
|
||||
/// </summary>
|
||||
public class StudentNotesImportService : IStudentNotesImportService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public StudentNotesImportResult Parse(
|
||||
Stream stream,
|
||||
ICollection<Student> students,
|
||||
IReadOnlyDictionary<int, string?> existingNotesByStudentId)
|
||||
{
|
||||
var reader = new StreamReader(stream, leaveOpen: true);
|
||||
using var parser = new StudentNotesFieldParser(reader);
|
||||
return parser.Parse(students, existingNotesByStudentId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user