feat: add a Tools page printer for note merge and print presets
Chapter officers can merge a markdown note onto filtered students, teams, or events and save the recipe. Extra student-note columns are additional fields (any ## … fields heading) so they work as print tokens whether imported or typed by hand. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
using Core.Printing;
|
||||
|
||||
namespace Tests.Printing;
|
||||
|
||||
[TestFixture]
|
||||
public class NoteTemplateMerger_Tests
|
||||
{
|
||||
[Test]
|
||||
public void Merge_ReplacesKnownTokens()
|
||||
{
|
||||
var tokens = PrintTokenMap.Create();
|
||||
tokens["FirstName"] = "Aria";
|
||||
tokens["LastName"] = "Cole";
|
||||
|
||||
var result = NoteTemplateMerger.Merge("Hello {{FirstName}} {{LastName}}", tokens);
|
||||
|
||||
Assert.That(result, Is.EqualTo("Hello Aria Cole"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_SupportsDottedAndSpacedNames()
|
||||
{
|
||||
var tokens = PrintTokenMap.Create();
|
||||
tokens["Chapter.Name"] = "North TSA";
|
||||
tokens["Interview Time"] = "3:20-3:35";
|
||||
|
||||
var result = NoteTemplateMerger.Merge(
|
||||
"{{Chapter.Name}} at {{Interview Time}}",
|
||||
tokens);
|
||||
|
||||
Assert.That(result, Is.EqualTo("North TSA at 3:20-3:35"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_IsCaseAndWhitespaceTolerant()
|
||||
{
|
||||
var tokens = PrintTokenMap.Create();
|
||||
tokens["FirstName"] = "Aria";
|
||||
|
||||
var result = NoteTemplateMerger.Merge("{{ firstname }}", tokens);
|
||||
|
||||
Assert.That(result, Is.EqualTo("Aria"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_LeavesUnknownTokensInPlace()
|
||||
{
|
||||
var tokens = PrintTokenMap.Create();
|
||||
tokens["FirstName"] = "Aria";
|
||||
|
||||
var result = NoteTemplateMerger.Merge("{{FirstName}} {{Missing}}", tokens);
|
||||
|
||||
Assert.That(result, Is.EqualTo("Aria {{Missing}}"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_KnownEmptyBecomesBlank()
|
||||
{
|
||||
var tokens = PrintTokenMap.Create();
|
||||
tokens["Interview Time"] = string.Empty;
|
||||
|
||||
var result = NoteTemplateMerger.Merge("Time: {{Interview Time}}.", tokens);
|
||||
|
||||
Assert.That(result, Is.EqualTo("Time: ."));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_EmptyTemplate_IsEmpty()
|
||||
{
|
||||
Assert.That(NoteTemplateMerger.Merge(null, PrintTokenMap.Create()), Is.EqualTo(string.Empty));
|
||||
Assert.That(NoteTemplateMerger.Merge("", PrintTokenMap.Create()), Is.EqualTo(string.Empty));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_PageBreak_IsSentinel()
|
||||
{
|
||||
var result = NoteTemplateMerger.Merge(
|
||||
"Above\n{{PageBreak}}\nBelow",
|
||||
PrintTokenMap.Create());
|
||||
|
||||
Assert.That(result, Does.Contain(NoteTemplateMerger.PageBreakSentinel));
|
||||
Assert.That(result, Does.Not.Contain("{{PageBreak}}"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ApplyLayout_ReplacesSentinelAndParagraphWrap()
|
||||
{
|
||||
var raw = NoteTemplateMerger.ApplyLayout(
|
||||
$"x{NoteTemplateMerger.PageBreakSentinel}y");
|
||||
var wrapped = NoteTemplateMerger.ApplyLayout(
|
||||
$"<p>{NoteTemplateMerger.PageBreakSentinel}</p>");
|
||||
|
||||
Assert.That(raw, Is.EqualTo($"x{NoteTemplateMerger.PageBreakHtml}y"));
|
||||
Assert.That(wrapped, Is.EqualTo(NoteTemplateMerger.PageBreakHtml));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_AnswerSpace_IsSentinel()
|
||||
{
|
||||
var result = NoteTemplateMerger.Merge(
|
||||
"Q?\n{{AnswerSpace}}\nNext",
|
||||
PrintTokenMap.Create());
|
||||
|
||||
Assert.That(result, Does.Contain(NoteTemplateMerger.AnswerSpaceSentinel));
|
||||
Assert.That(result, Does.Not.Contain("{{AnswerSpace}}"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ApplyLayout_ReplacesAnswerSpaceSentinelAndParagraphWrap()
|
||||
{
|
||||
var raw = NoteTemplateMerger.ApplyLayout(
|
||||
$"x{NoteTemplateMerger.AnswerSpaceSentinel}y");
|
||||
var wrapped = NoteTemplateMerger.ApplyLayout(
|
||||
$"<p>{NoteTemplateMerger.AnswerSpaceSentinel}</p>");
|
||||
|
||||
Assert.That(raw, Is.EqualTo($"x{NoteTemplateMerger.AnswerSpaceHtml}y"));
|
||||
Assert.That(wrapped, Is.EqualTo(NoteTemplateMerger.AnswerSpaceHtml));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Core.Printing;
|
||||
|
||||
namespace Tests.Printing;
|
||||
|
||||
[TestFixture]
|
||||
public class PrintFieldCatalog_Tests
|
||||
{
|
||||
[Test]
|
||||
public void BuiltInFor_StudentIncludesChapterAndStudentTokens()
|
||||
{
|
||||
var tokens = PrintFieldCatalog.BuiltInFor(PrintEntityType.Student);
|
||||
|
||||
Assert.That(tokens, Does.Contain("FirstName"));
|
||||
Assert.That(tokens, Does.Contain("Chapter.ShortName"));
|
||||
Assert.That(tokens, Does.Not.Contain("Interview Time"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuiltInFor_TeamAndEventHaveExpectedNames()
|
||||
{
|
||||
Assert.That(PrintFieldCatalog.BuiltInFor(PrintEntityType.Team), Does.Contain("EventName"));
|
||||
Assert.That(PrintFieldCatalog.BuiltInFor(PrintEntityType.Event), Does.Contain("RegionalEvent"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Layout_IncludesPageBreakAndAnswerSpace()
|
||||
{
|
||||
Assert.That(PrintFieldCatalog.Layout, Does.Contain(NoteTemplateMerger.PageBreakToken));
|
||||
Assert.That(PrintFieldCatalog.Layout, Does.Contain(NoteTemplateMerger.AnswerSpaceToken));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EntityTokens_MatchesCatalogArrays()
|
||||
{
|
||||
Assert.That(PrintFieldCatalog.EntityTokens(PrintEntityType.Student), Is.EqualTo(PrintFieldCatalog.Student));
|
||||
Assert.That(PrintFieldCatalog.EntityTokens(PrintEntityType.Team), Is.EqualTo(PrintFieldCatalog.Team));
|
||||
Assert.That(PrintFieldCatalog.EntityTokens(PrintEntityType.Event), Is.EqualTo(PrintFieldCatalog.Event));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Core.Entities;
|
||||
using Core.Printing;
|
||||
|
||||
namespace Tests.Printing;
|
||||
|
||||
[TestFixture]
|
||||
public class PrintPresetFilters_Tests
|
||||
{
|
||||
[Test]
|
||||
public void JsonRoundTrip_PreservesStudentAndImportedFilters()
|
||||
{
|
||||
var filters = new PrintPresetFilters
|
||||
{
|
||||
Grade = 9,
|
||||
TsaYear = 1,
|
||||
IsOfficer = true,
|
||||
RegionalOnly = true,
|
||||
EventFormat = EventFormat.Team,
|
||||
NewPagePerRecord = false,
|
||||
FontSizePt = 14,
|
||||
AnswerSpaceLines = 4
|
||||
};
|
||||
|
||||
var restored = PrintPresetFilters.FromJson(filters.ToJson());
|
||||
|
||||
Assert.That(restored.Grade, Is.EqualTo(9));
|
||||
Assert.That(restored.TsaYear, Is.EqualTo(1));
|
||||
Assert.That(restored.IsOfficer, Is.True);
|
||||
Assert.That(restored.RegionalOnly, Is.True);
|
||||
Assert.That(restored.EventFormat, Is.EqualTo(EventFormat.Team));
|
||||
Assert.That(restored.NewPagePerRecord, Is.False);
|
||||
Assert.That(restored.FontSizePt, Is.EqualTo(14));
|
||||
Assert.That(restored.AnswerSpaceLines, Is.EqualTo(4));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FromJson_EmptyOrInvalid_ReturnsDefaults()
|
||||
{
|
||||
Assert.That(PrintPresetFilters.FromJson(null).TsaYear, Is.Null);
|
||||
Assert.That(PrintPresetFilters.FromJson("{}").Grade, Is.Null);
|
||||
Assert.That(PrintPresetFilters.FromJson("{}").NewPagePerRecord, Is.True);
|
||||
Assert.That(PrintPresetFilters.FromJson("{}").FontSizePt, Is.EqualTo(PrintPresetFilters.DefaultFontSizePt));
|
||||
Assert.That(PrintPresetFilters.FromJson("{}").AnswerSpaceLines, Is.EqualTo(PrintPresetFilters.DefaultAnswerSpaceLines));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FromJson_ClampsPrintOptions()
|
||||
{
|
||||
var restored = PrintPresetFilters.FromJson("""{"fontSizePt":99,"answerSpaceLines":0}""");
|
||||
|
||||
Assert.That(restored.FontSizePt, Is.EqualTo(PrintPresetFilters.MaxFontSizePt));
|
||||
Assert.That(restored.AnswerSpaceLines, Is.EqualTo(PrintPresetFilters.MinAnswerSpaceLines));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using Core.Printing;
|
||||
|
||||
namespace Tests.Printing;
|
||||
|
||||
[TestFixture]
|
||||
public class PrintTokenMap_Tests
|
||||
{
|
||||
[Test]
|
||||
public void Build_BuiltInWinsOverImportedSameName()
|
||||
{
|
||||
var map = PrintTokenMap.Build(
|
||||
new Dictionary<string, string?> { ["Grade"] = "imported" },
|
||||
new Dictionary<string, string?> { ["Grade"] = "9" },
|
||||
null);
|
||||
|
||||
Assert.That(map["Grade"], Is.EqualTo("9"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Build_IncludesAllImportedCatalogKeys()
|
||||
{
|
||||
var imported = new Dictionary<string, string?>
|
||||
{
|
||||
["Interview Time"] = "3:20-3:35",
|
||||
["Application"] = null
|
||||
};
|
||||
|
||||
var map = PrintTokenMap.Build(imported, null, null);
|
||||
|
||||
Assert.That(map.ContainsKey("Interview Time"), Is.True);
|
||||
Assert.That(map["Interview Time"], Is.EqualTo("3:20-3:35"));
|
||||
Assert.That(map.ContainsKey("Application"), Is.True);
|
||||
Assert.That(map["Application"], Is.EqualTo(string.Empty));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Escape_MasksMarkdownAndHtml()
|
||||
{
|
||||
var escaped = PrintTokenMap.Escape("A *B* <script>");
|
||||
|
||||
Assert.That(escaped, Does.Contain("\\*"));
|
||||
Assert.That(escaped, Does.Contain("<"));
|
||||
Assert.That(escaped, Does.Contain(">"));
|
||||
Assert.That(escaped, Does.Not.Contain("<script>"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_EscapedAsteriskDoesNotStayRaw()
|
||||
{
|
||||
var map = PrintTokenMap.Build(
|
||||
null,
|
||||
new Dictionary<string, string?> { ["FirstName"] = "A*ria" },
|
||||
null);
|
||||
|
||||
var merged = NoteTemplateMerger.Merge("Hi {{FirstName}}", map);
|
||||
|
||||
Assert.That(merged, Is.EqualTo("Hi A\\*ria"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user