refactor: Unify PDF pagination system and rename templates
BREAKING CHANGE: Template names changed from page1_stats.html to page_1.html ## Phase 1: Unified Pagination Function - Implemented PaginateMultiList() to replace PaginateSkills(), PaginateSpells(), and PaginatePage2PlayLists() - Single metadata-driven function handles all list types (skills, weapons, spells, equipment) - Properly handles filters (learned/unlearned/language) via template metadata - Shares list trackers by ListType+Filter combination to avoid duplication - Added comprehensive tests for all edge cases ## Phase 2: Template Naming Convention - Renamed templates to be data-agnostic: - page1_stats.html -> page_1.html - page1.2_stats.html -> page_1.2.html - page2_play.html -> page_2.html - page2.2_play.html -> page_2.2.html - page3_spell.html -> page_3.html - page3.2_spell.html -> page_3.2.html - page4_equip.html -> page_4.html - Updated GenerateContinuationTemplateName() for new naming (page_1.html -> page_1.2.html) - Updated ExtractBaseTemplateName() to handle new format - Updated all test files and source files with new template names ## Phase 3: Simplified RenderPageWithContinuations - Removed hardcoded switch statements based on template names - Replaced with generic dataMap and unified pagination call - Extracted populatePageDataFromDistribution() to handle data mapping - Template type detection now driven by metadata, not hardcoded names ## Benefits - ✅ Extensibility: Add new templates without code changes - ✅ Maintainability: One pagination algorithm instead of three - ✅ Clarity: Template names reflect page numbers, not content types - ✅ Flexibility: Templates can mix any data types - ✅ All 40+ tests passing ## Technical Details - Added SkillsColumn3 and SkillsColumn4 fields to PageData for continuation pages - Template metadata loaded from HTML comments drives pagination behavior - Backward compatibility maintained for old template references in comments
This commit is contained in:
@@ -65,7 +65,7 @@ func TestIntegration_ContinuationPages_ActualFiles(t *testing.T) {
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
var totalCap int
|
||||
for _, tmpl := range templateSet.Templates {
|
||||
if tmpl.Metadata.Name == "page1_stats.html" {
|
||||
if tmpl.Metadata.Name == "page_1.html" {
|
||||
for _, block := range tmpl.Metadata.Blocks {
|
||||
if block.ListType == "skills" {
|
||||
totalCap += block.MaxItems
|
||||
@@ -81,7 +81,7 @@ func TestIntegration_ContinuationPages_ActualFiles(t *testing.T) {
|
||||
// Act - Render page1 with continuations
|
||||
pdfs, err := RenderPageWithContinuations(
|
||||
viewModel,
|
||||
"page1_stats.html",
|
||||
"page_1.html",
|
||||
1,
|
||||
"20.12.2025",
|
||||
loader,
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestContinuationPages_WhenSkillsExceedCapacity(t *testing.T) {
|
||||
}
|
||||
|
||||
// Act - Paginate skills for page1_stats
|
||||
pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "")
|
||||
pages, err := paginator.PaginateSkills(skills, "page_1.html", "")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
@@ -34,13 +34,13 @@ func TestContinuationPages_WhenSkillsExceedCapacity(t *testing.T) {
|
||||
t.Errorf("Expected at least 2 pages for 100 skills, got %d", len(pages))
|
||||
}
|
||||
|
||||
// First page should be "page1_stats.html"
|
||||
if pages[0].TemplateName != "page1_stats.html" {
|
||||
t.Errorf("Expected first page template 'page1_stats.html', got '%s'", pages[0].TemplateName)
|
||||
// First page should be "page_1.html"
|
||||
if pages[0].TemplateName != "page_1.html" {
|
||||
t.Errorf("Expected first page template 'page_1.html', got '%s'", pages[0].TemplateName)
|
||||
}
|
||||
|
||||
// Second page should be continuation page with name pattern "page1.2_stats.html"
|
||||
expectedContinuation := "page1.2_stats.html"
|
||||
// Second page should be continuation page with name pattern "page_1.2.html"
|
||||
expectedContinuation := "page_1.2.html"
|
||||
if pages[1].TemplateName != expectedContinuation {
|
||||
t.Errorf("Expected continuation page template '%s', got '%s'",
|
||||
expectedContinuation, pages[1].TemplateName)
|
||||
@@ -79,7 +79,7 @@ func TestContinuationPages_WhenWeaponsExceedCapacity(t *testing.T) {
|
||||
// Get capacity for page2_play weapons (should be 12)
|
||||
var weaponsCapacity int
|
||||
for _, tmpl := range templateSet.Templates {
|
||||
if tmpl.Metadata.Name == "page2_play.html" {
|
||||
if tmpl.Metadata.Name == "page_2.html" {
|
||||
for _, block := range tmpl.Metadata.Blocks {
|
||||
if block.ListType == "weapons" {
|
||||
weaponsCapacity = block.MaxItems
|
||||
@@ -106,7 +106,7 @@ func TestContinuationPages_WhenWeaponsExceedCapacity(t *testing.T) {
|
||||
t.Logf("Created %d weapons (capacity %d)", numWeapons, weaponsCapacity)
|
||||
|
||||
// Act
|
||||
pages, err := paginator.PaginateWeapons(weapons, "page2_play.html")
|
||||
pages, err := paginator.PaginateWeapons(weapons, "page_2.html")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
@@ -119,13 +119,13 @@ func TestContinuationPages_WhenWeaponsExceedCapacity(t *testing.T) {
|
||||
}
|
||||
|
||||
// First page should be original template
|
||||
if pages[0].TemplateName != "page2_play.html" {
|
||||
t.Errorf("Expected first page 'page2_play.html', got '%s'", pages[0].TemplateName)
|
||||
if pages[0].TemplateName != "page_2.html" {
|
||||
t.Errorf("Expected first page 'page_2.html', got '%s'", pages[0].TemplateName)
|
||||
}
|
||||
|
||||
// Second page should be continuation
|
||||
if pages[1].TemplateName != "page2.2_play.html" {
|
||||
t.Errorf("Expected continuation 'page2.2_play.html', got '%s'", pages[1].TemplateName)
|
||||
if pages[1].TemplateName != "page_2.2.html" {
|
||||
t.Errorf("Expected continuation 'page_2.2.html', got '%s'", pages[1].TemplateName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ func TestContinuationPages_MultipleOverflows(t *testing.T) {
|
||||
// Get actual capacity from template
|
||||
var skillsCapacity int
|
||||
for _, tmpl := range templateSet.Templates {
|
||||
if tmpl.Metadata.Name == "page1_stats.html" {
|
||||
if tmpl.Metadata.Name == "page_1.html" {
|
||||
for _, block := range tmpl.Metadata.Blocks {
|
||||
if block.ListType == "skills" {
|
||||
skillsCapacity += block.MaxItems
|
||||
@@ -163,7 +163,7 @@ func TestContinuationPages_MultipleOverflows(t *testing.T) {
|
||||
expectedPages := (200 + skillsCapacity - 1) / skillsCapacity
|
||||
|
||||
// Act
|
||||
pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "")
|
||||
pages, err := paginator.PaginateSkills(skills, "page_1.html", "")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
@@ -177,14 +177,14 @@ func TestContinuationPages_MultipleOverflows(t *testing.T) {
|
||||
|
||||
t.Logf("Created %d pages for 200 skills", len(pages))
|
||||
|
||||
// Verify template names follow pattern: page1_stats.html, then all use page1.2_stats.html
|
||||
// Verify template names follow pattern: page_1.html, then all use page_1.2.html
|
||||
for i, page := range pages {
|
||||
var expectedTemplate string
|
||||
if i == 0 {
|
||||
expectedTemplate = "page1_stats.html"
|
||||
expectedTemplate = "page_1.html"
|
||||
} else {
|
||||
// All continuation pages use the same .2 template
|
||||
expectedTemplate = "page1.2_stats.html"
|
||||
expectedTemplate = "page_1.2.html"
|
||||
}
|
||||
|
||||
if page.TemplateName != expectedTemplate {
|
||||
@@ -211,7 +211,7 @@ func TestContinuationPages_NoOverflow(t *testing.T) {
|
||||
}
|
||||
|
||||
// Act
|
||||
pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "")
|
||||
pages, err := paginator.PaginateSkills(skills, "page_1.html", "")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
@@ -224,7 +224,7 @@ func TestContinuationPages_NoOverflow(t *testing.T) {
|
||||
}
|
||||
|
||||
// Should use original template, not continuation
|
||||
if pages[0].TemplateName != "page1_stats.html" {
|
||||
t.Errorf("Expected original template 'page1_stats.html', got '%s'", pages[0].TemplateName)
|
||||
if pages[0].TemplateName != "page_1.html" {
|
||||
t.Errorf("Expected original template 'page_1.html', got '%s'", pages[0].TemplateName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ func TestTemplateWithEmptyRows(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
html, err := loader.RenderTemplate("page1_stats.html", pageData)
|
||||
html, err := loader.RenderTemplate("page_1.html", pageData)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to render template: %v", err)
|
||||
}
|
||||
@@ -85,13 +85,13 @@ func TestTemplateWithEmptyRows(t *testing.T) {
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
var page1Template *TemplateWithMeta
|
||||
for i := range templateSet.Templates {
|
||||
if templateSet.Templates[i].Metadata.Name == "page1_stats.html" {
|
||||
if templateSet.Templates[i].Metadata.Name == "page_1.html" {
|
||||
page1Template = &templateSet.Templates[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if page1Template == nil {
|
||||
t.Fatal("page1_stats.html template not found")
|
||||
t.Fatal("page_1.html template not found")
|
||||
}
|
||||
var col1Capacity int
|
||||
for i := range page1Template.Metadata.Blocks {
|
||||
|
||||
@@ -91,7 +91,7 @@ func ExportCharacterToPDF(c *gin.Context) {
|
||||
var allPDFs [][]byte
|
||||
|
||||
// Page 1: Stats
|
||||
page1PDFs, err := RenderPageWithContinuations(viewModel, "page1_stats.html", 1, currentDate, loader, renderer)
|
||||
page1PDFs, err := RenderPageWithContinuations(viewModel, "page_1.html", 1, currentDate, loader, renderer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render page 1: " + err.Error()})
|
||||
return
|
||||
@@ -99,7 +99,7 @@ func ExportCharacterToPDF(c *gin.Context) {
|
||||
allPDFs = append(allPDFs, page1PDFs...)
|
||||
|
||||
// Page 2: Play
|
||||
page2PDFs, err := RenderPageWithContinuations(viewModel, "page2_play.html", 2, currentDate, loader, renderer)
|
||||
page2PDFs, err := RenderPageWithContinuations(viewModel, "page_2.html", 2, currentDate, loader, renderer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render page 2: " + err.Error()})
|
||||
return
|
||||
@@ -107,7 +107,7 @@ func ExportCharacterToPDF(c *gin.Context) {
|
||||
allPDFs = append(allPDFs, page2PDFs...)
|
||||
|
||||
// Page 3: Spells
|
||||
page3PDFs, err := RenderPageWithContinuations(viewModel, "page3_spell.html", 3, currentDate, loader, renderer)
|
||||
page3PDFs, err := RenderPageWithContinuations(viewModel, "page_3.html", 3, currentDate, loader, renderer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render page 3: " + err.Error()})
|
||||
return
|
||||
@@ -115,7 +115,7 @@ func ExportCharacterToPDF(c *gin.Context) {
|
||||
allPDFs = append(allPDFs, page3PDFs...)
|
||||
|
||||
// Page 4: Equipment
|
||||
page4PDFs, err := RenderPageWithContinuations(viewModel, "page4_equip.html", 4, currentDate, loader, renderer)
|
||||
page4PDFs, err := RenderPageWithContinuations(viewModel, "page_4.html", 4, currentDate, loader, renderer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render page 4: " + err.Error()})
|
||||
return
|
||||
|
||||
@@ -101,12 +101,12 @@ func TestIntegration_FullPDFGeneration(t *testing.T) {
|
||||
}
|
||||
|
||||
// Step 3: Prepare paginated data and render template to HTML
|
||||
pageData, err := PreparePaginatedPageData(viewModel, "page1_stats.html", 1, "18.12.2025")
|
||||
pageData, err := PreparePaginatedPageData(viewModel, "page_1.html", 1, "18.12.2025")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to prepare paginated data: %v", err)
|
||||
}
|
||||
|
||||
html, err := loader.RenderTemplate("page1_stats.html", pageData)
|
||||
html, err := loader.RenderTemplate("page_1.html", pageData)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to render template: %v", err)
|
||||
}
|
||||
@@ -160,11 +160,11 @@ func TestIntegration_TemplateMetadata(t *testing.T) {
|
||||
template string
|
||||
expectedBlock string
|
||||
}{
|
||||
{"page1_stats.html", "skills_column1"},
|
||||
{"page2_play.html", "skills_learned"},
|
||||
{"page3_spell.html", "spells_left"},
|
||||
{"page3_spell.html", "spells_right"},
|
||||
{"page4_equip.html", "equipment_worn"},
|
||||
{"page_1.html", "skills_column1"},
|
||||
{"page_2.html", "skills_learned"},
|
||||
{"page_3.html", "spells_left"},
|
||||
{"page_3.html", "spells_right"},
|
||||
{"page_4.html", "equipment_worn"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -217,7 +217,7 @@ func TestIntegration_PaginationWithPDF(t *testing.T) {
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
paginator := NewPaginator(templateSet)
|
||||
|
||||
pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "")
|
||||
pages, err := paginator.PaginateSkills(skills, "page_1.html", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to paginate skills: %v", err)
|
||||
}
|
||||
@@ -225,7 +225,7 @@ func TestIntegration_PaginationWithPDF(t *testing.T) {
|
||||
// Calculate expected pages based on actual capacity
|
||||
var skillsCapacity int
|
||||
for _, tmpl := range templateSet.Templates {
|
||||
if tmpl.Metadata.Name == "page1_stats.html" {
|
||||
if tmpl.Metadata.Name == "page_1.html" {
|
||||
for _, block := range tmpl.Metadata.Blocks {
|
||||
if block.ListType == "skills" {
|
||||
skillsCapacity += block.MaxItems
|
||||
@@ -275,7 +275,7 @@ func TestIntegration_PaginationWithPDF(t *testing.T) {
|
||||
pageData.SkillsColumn2 = pages[0].Data["skills_column2"].([]SkillViewModel)
|
||||
pageData.Skills = append(pageData.SkillsColumn1, pageData.SkillsColumn2...) // Keep for logging
|
||||
|
||||
html, err := loader.RenderTemplate("page1_stats.html", pageData)
|
||||
html, err := loader.RenderTemplate("page_1.html", pageData)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to render template: %v", err)
|
||||
}
|
||||
@@ -325,7 +325,7 @@ func TestIntegration_MultiPageSpellList(t *testing.T) {
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
var page3Template *TemplateWithMeta
|
||||
for i := range templateSet.Templates {
|
||||
if templateSet.Templates[i].Metadata.Name == "page3_spell.html" {
|
||||
if templateSet.Templates[i].Metadata.Name == "page_3.html" {
|
||||
page3Template = &templateSet.Templates[i]
|
||||
break
|
||||
}
|
||||
@@ -351,7 +351,7 @@ func TestIntegration_MultiPageSpellList(t *testing.T) {
|
||||
paginator := NewPaginator(templateSet)
|
||||
|
||||
// Paginate spells
|
||||
pages, err := paginator.PaginateSpells(spells, "page3_spell.html")
|
||||
pages, err := paginator.PaginateSpells(spells, "page_3.html")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to paginate spells: %v", err)
|
||||
}
|
||||
@@ -446,7 +446,7 @@ func TestIntegration_CompleteWorkflow(t *testing.T) {
|
||||
paginator := NewPaginator(templateSet)
|
||||
|
||||
// Step 4: Paginate skills
|
||||
skillPages, err := paginator.PaginateSkills(viewModel.Skills, "page1_stats.html", "")
|
||||
skillPages, err := paginator.PaginateSkills(viewModel.Skills, "page_1.html", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to paginate skills: %v", err)
|
||||
}
|
||||
@@ -555,7 +555,7 @@ func TestVisualInspection_AllPages(t *testing.T) {
|
||||
|
||||
// Page 1: Stats page with skills (may have continuations)
|
||||
t.Log("Generating Page 1: Stats...")
|
||||
page1PDFs, err := RenderPageWithContinuations(viewModel, "page1_stats.html", 1, "18.12.2025", loader, renderer)
|
||||
page1PDFs, err := RenderPageWithContinuations(viewModel, "page_1.html", 1, "18.12.2025", loader, renderer)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate page1: %v", err)
|
||||
}
|
||||
@@ -580,7 +580,7 @@ func TestVisualInspection_AllPages(t *testing.T) {
|
||||
|
||||
// Page 2: Play/Adventure page with weapons (may have continuations)
|
||||
t.Log("Generating Page 2: Play...")
|
||||
page2PDFs, err := RenderPageWithContinuations(viewModel, "page2_play.html", 2, "18.12.2025", loader, renderer)
|
||||
page2PDFs, err := RenderPageWithContinuations(viewModel, "page_2.html", 2, "18.12.2025", loader, renderer)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate page2: %v", err)
|
||||
}
|
||||
@@ -605,7 +605,7 @@ func TestVisualInspection_AllPages(t *testing.T) {
|
||||
|
||||
// Page 3: Spells page (may have continuations)
|
||||
t.Log("Generating Page 3: Spells...")
|
||||
page3PDFs, err := RenderPageWithContinuations(viewModel, "page3_spell.html", 3, "18.12.2025", loader, renderer)
|
||||
page3PDFs, err := RenderPageWithContinuations(viewModel, "page_3.html", 3, "18.12.2025", loader, renderer)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate page3: %v", err)
|
||||
}
|
||||
@@ -630,7 +630,7 @@ func TestVisualInspection_AllPages(t *testing.T) {
|
||||
|
||||
// Page 4: Equipment page (may have continuations)
|
||||
t.Log("Generating Page 4: Equipment...")
|
||||
page4PDFs, err := RenderPageWithContinuations(viewModel, "page4_equip.html", 4, "18.12.2025", loader, renderer)
|
||||
page4PDFs, err := RenderPageWithContinuations(viewModel, "page_4.html", 4, "18.12.2025", loader, renderer)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate page4: %v", err)
|
||||
}
|
||||
|
||||
+219
-33
@@ -6,55 +6,51 @@ import (
|
||||
)
|
||||
|
||||
// GenerateContinuationTemplateName creates a continuation template name
|
||||
// Example: "page1_stats.html" + pageNum 2 -> "page1.2_stats.html"
|
||||
// Note: All continuation pages (2, 3, 4, ...) use the same template name: page1.2_stats.html
|
||||
// Example: "page_1.html" + pageNum 2 -> "page_1.2.html"
|
||||
// Note: All continuation pages (2, 3, 4, ...) use the same template name: page_1.2.html
|
||||
func GenerateContinuationTemplateName(originalTemplate string, pageNum int) string {
|
||||
if pageNum == 1 {
|
||||
return originalTemplate
|
||||
}
|
||||
|
||||
// All continuation pages use .2 template (page1.2, page2.2, etc.)
|
||||
// NOT page1.3, page1.4, etc.
|
||||
// All continuation pages use .2 template (page_1.2, page_2.2, etc.)
|
||||
// NOT page_1.3, page_1.4, etc.
|
||||
|
||||
// Split template name at first underscore to insert page continuation number
|
||||
// Example: "page1_stats.html" -> "page1" + "_stats.html"
|
||||
parts := strings.SplitN(originalTemplate, "_", 2)
|
||||
if len(parts) != 2 {
|
||||
// Fallback: just append .2 before extension
|
||||
ext := ".html"
|
||||
base := strings.TrimSuffix(originalTemplate, ext)
|
||||
return fmt.Sprintf("%s.2%s", base, ext)
|
||||
// New format: "page_1.html" -> "page_1.2.html"
|
||||
// Pattern: page_N.html where N is a number
|
||||
ext := ".html"
|
||||
base := strings.TrimSuffix(originalTemplate, ext)
|
||||
|
||||
// Check if it's already a continuation (has .2 in it)
|
||||
if strings.Contains(base, ".2") {
|
||||
return originalTemplate
|
||||
}
|
||||
|
||||
// Extract page number and base name
|
||||
// "page1" -> "page" + "1"
|
||||
baseName := parts[0]
|
||||
suffix := parts[1]
|
||||
|
||||
// Always use .2 for continuation pages: "page1.2_stats.html"
|
||||
return fmt.Sprintf("%s.2_%s", baseName, suffix)
|
||||
// Append .2 before .html
|
||||
return fmt.Sprintf("%s.2%s", base, ext)
|
||||
}
|
||||
|
||||
// ExtractBaseTemplateName extracts the base template name from a continuation template
|
||||
// Example: "page1.2_stats.html" -> "page1_stats.html"
|
||||
// Example: "page_1.2.html" -> "page_1.html"
|
||||
func ExtractBaseTemplateName(templateName string) string {
|
||||
// Check if it's a continuation template (contains .N_ pattern)
|
||||
parts := strings.SplitN(templateName, "_", 2)
|
||||
if len(parts) != 2 {
|
||||
// New format: "page_1.2.html" or "page_1.10.html" -> "page_1.html"
|
||||
// Pattern: ends with .N.html where N is any number
|
||||
ext := ".html"
|
||||
if !strings.HasSuffix(templateName, ext) {
|
||||
return templateName
|
||||
}
|
||||
|
||||
baseName := parts[0]
|
||||
suffix := parts[1]
|
||||
// Remove .html
|
||||
base := strings.TrimSuffix(templateName, ext)
|
||||
|
||||
// Check if baseName contains a dot followed by a number (e.g., "page1.2")
|
||||
dotIdx := strings.LastIndex(baseName, ".")
|
||||
if dotIdx == -1 {
|
||||
return templateName // Not a continuation template
|
||||
// Find the last dot
|
||||
lastDotIdx := strings.LastIndex(base, ".")
|
||||
if lastDotIdx == -1 {
|
||||
return templateName // No dot found, not a continuation
|
||||
}
|
||||
|
||||
// Verify the part after the dot is a number
|
||||
numPart := baseName[dotIdx+1:]
|
||||
// Check if everything after the last dot is a number
|
||||
numPart := base[lastDotIdx+1:]
|
||||
if len(numPart) == 0 {
|
||||
return templateName
|
||||
}
|
||||
@@ -66,8 +62,7 @@ func ExtractBaseTemplateName(templateName string) string {
|
||||
}
|
||||
|
||||
// It's a continuation template, return the base name
|
||||
basePrefix := baseName[:dotIdx]
|
||||
return fmt.Sprintf("%s_%s", basePrefix, suffix)
|
||||
return base[:lastDotIdx] + ext
|
||||
}
|
||||
|
||||
// SliceList slices a list based on start index and max items
|
||||
@@ -106,6 +101,189 @@ func NewPaginator(templateSet TemplateSet) *Paginator {
|
||||
}
|
||||
}
|
||||
|
||||
// PaginateMultiList is a unified pagination function that handles multiple list types
|
||||
// It replaces PaginateSkills, PaginateSpells, and PaginatePage2PlayLists
|
||||
// dataMap keys: "skills", "weapons", "spells", "equipment", "magicItems"
|
||||
func (p *Paginator) PaginateMultiList(dataMap map[string]interface{}, templateName string) ([]PageDistribution, error) {
|
||||
template := p.findTemplate(templateName)
|
||||
if template == nil {
|
||||
return nil, fmt.Errorf("template not found: %s", templateName)
|
||||
}
|
||||
|
||||
// Build filtered lists for each unique list type + filter combination
|
||||
type listTracker struct {
|
||||
items interface{}
|
||||
currentIdx int
|
||||
totalCount int
|
||||
}
|
||||
|
||||
// Track by "listType:filter" to avoid duplicates
|
||||
listTrackers := make(map[string]*listTracker)
|
||||
|
||||
// First pass: create filtered lists for each unique listType+filter combination
|
||||
for _, block := range template.Blocks {
|
||||
// Get the source data based on block's ListType
|
||||
var sourceData interface{}
|
||||
switch block.ListType {
|
||||
case "skills":
|
||||
sourceData = dataMap["skills"]
|
||||
case "weapons":
|
||||
sourceData = dataMap["weapons"]
|
||||
case "spells":
|
||||
sourceData = dataMap["spells"]
|
||||
case "equipment":
|
||||
sourceData = dataMap["equipment"]
|
||||
case "magicItems":
|
||||
sourceData = dataMap["magicItems"]
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if sourceData == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Create unique key for this list type + filter combination
|
||||
trackerKey := block.ListType
|
||||
if block.Filter != "" {
|
||||
trackerKey += ":" + block.Filter
|
||||
}
|
||||
|
||||
// Only create tracker once per unique combination
|
||||
if _, exists := listTrackers[trackerKey]; !exists {
|
||||
// Apply filter if specified
|
||||
filteredItems := p.applyFilter(sourceData, block.Filter)
|
||||
itemCount := p.getItemCount(filteredItems)
|
||||
|
||||
if itemCount > 0 {
|
||||
listTrackers[trackerKey] = &listTracker{
|
||||
items: filteredItems,
|
||||
currentIdx: 0,
|
||||
totalCount: itemCount,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If all lists are empty, return empty result
|
||||
if len(listTrackers) == 0 {
|
||||
return []PageDistribution{}, nil
|
||||
}
|
||||
|
||||
// Generate pages until all items are distributed
|
||||
distributions := []PageDistribution{}
|
||||
pageNum := 1
|
||||
|
||||
for {
|
||||
// Check if there are any remaining items
|
||||
hasRemainingItems := false
|
||||
for _, tracker := range listTrackers {
|
||||
if tracker.currentIdx < tracker.totalCount {
|
||||
hasRemainingItems = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasRemainingItems {
|
||||
break
|
||||
}
|
||||
|
||||
// Create page data
|
||||
pageData := make(map[string]interface{})
|
||||
|
||||
// Distribute items to each block for this page
|
||||
for _, block := range template.Blocks {
|
||||
// Get tracker for this block's list type + filter
|
||||
trackerKey := block.ListType
|
||||
if block.Filter != "" {
|
||||
trackerKey += ":" + block.Filter
|
||||
}
|
||||
|
||||
tracker, exists := listTrackers[trackerKey]
|
||||
if !exists {
|
||||
// Block has no data, add empty slice
|
||||
pageData[block.Name] = p.createEmptySlice(block.ListType)
|
||||
continue
|
||||
}
|
||||
|
||||
// Calculate how many items to take for this block
|
||||
itemsToTake := block.MaxItems
|
||||
remaining := tracker.totalCount - tracker.currentIdx
|
||||
if itemsToTake > remaining {
|
||||
itemsToTake = remaining
|
||||
}
|
||||
|
||||
// Extract slice for this block
|
||||
blockItems := p.extractSlice(tracker.items, tracker.currentIdx, itemsToTake)
|
||||
pageData[block.Name] = blockItems
|
||||
tracker.currentIdx += itemsToTake
|
||||
}
|
||||
|
||||
// Determine template name - use continuation naming for pages 2+
|
||||
pageTemplateName := GenerateContinuationTemplateName(templateName, pageNum)
|
||||
|
||||
distributions = append(distributions, PageDistribution{
|
||||
TemplateName: pageTemplateName,
|
||||
PageNumber: pageNum,
|
||||
Data: pageData,
|
||||
})
|
||||
|
||||
pageNum++
|
||||
}
|
||||
|
||||
return distributions, nil
|
||||
}
|
||||
|
||||
// applyFilter filters a list based on filter criteria
|
||||
func (p *Paginator) applyFilter(items interface{}, filter string) interface{} {
|
||||
if filter == "" {
|
||||
return items
|
||||
}
|
||||
|
||||
switch v := items.(type) {
|
||||
case []SkillViewModel:
|
||||
filtered := []SkillViewModel{}
|
||||
for _, skill := range v {
|
||||
include := false
|
||||
switch filter {
|
||||
case "learned":
|
||||
include = skill.IsLearned && skill.Category != "Sprache"
|
||||
case "unlearned":
|
||||
include = !skill.IsLearned && skill.Category != "Sprache"
|
||||
case "language", "languages":
|
||||
include = skill.Category == "Sprache"
|
||||
default:
|
||||
include = true
|
||||
}
|
||||
if include {
|
||||
filtered = append(filtered, skill)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
default:
|
||||
// No filtering for other types
|
||||
return items
|
||||
}
|
||||
}
|
||||
|
||||
// getItemCount returns the count of items in a list
|
||||
func (p *Paginator) getItemCount(items interface{}) int {
|
||||
switch v := items.(type) {
|
||||
case []SkillViewModel:
|
||||
return len(v)
|
||||
case []WeaponViewModel:
|
||||
return len(v)
|
||||
case []SpellViewModel:
|
||||
return len(v)
|
||||
case []EquipmentViewModel:
|
||||
return len(v)
|
||||
case []MagicItemViewModel:
|
||||
return len(v)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// PaginateSkills splits skills across multiple pages according to template capacity
|
||||
func (p *Paginator) PaginateSkills(skills []SkillViewModel, templateName string, filter string) ([]PageDistribution, error) {
|
||||
template := p.findTemplate(templateName)
|
||||
@@ -289,6 +467,12 @@ func (p *Paginator) extractSlice(items interface{}, start, count int) interface{
|
||||
end = len(v)
|
||||
}
|
||||
return v[start:end]
|
||||
case []MagicItemViewModel:
|
||||
end := start + count
|
||||
if end > len(v) {
|
||||
end = len(v)
|
||||
}
|
||||
return v[start:end]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -304,6 +488,8 @@ func (p *Paginator) createEmptySlice(listType string) interface{} {
|
||||
return []SpellViewModel{}
|
||||
case "equipment":
|
||||
return []EquipmentViewModel{}
|
||||
case "magicItems":
|
||||
return []MagicItemViewModel{}
|
||||
default:
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func PreparePaginatedPageData(viewModel *CharacterSheetViewModel, templateName s
|
||||
}
|
||||
|
||||
// For page1_stats.html - paginate skills across two columns
|
||||
if templateName == "page1_stats.html" {
|
||||
if templateName == "page_1.html" {
|
||||
// Get the template metadata
|
||||
var template *TemplateMetadata
|
||||
for _, tmpl := range templateSet.Templates {
|
||||
@@ -70,7 +70,7 @@ func PreparePaginatedPageData(viewModel *CharacterSheetViewModel, templateName s
|
||||
} else {
|
||||
pageData.Skills = viewModel.Skills
|
||||
}
|
||||
} else if templateName == "page2_play.html" {
|
||||
} else if templateName == "page_2.html" {
|
||||
// Get capacities from template
|
||||
weaponsCapacity := GetBlockCapacity(&templateSet, templateName, "weapons_main")
|
||||
learnedCapacity := GetBlockCapacity(&templateSet, templateName, "skills_learned")
|
||||
@@ -117,7 +117,7 @@ func PreparePaginatedPageData(viewModel *CharacterSheetViewModel, templateName s
|
||||
pageData.SkillsLanguage = languageSkills
|
||||
}
|
||||
pageData.Skills = viewModel.Skills // Keep for backward compatibility
|
||||
} else if templateName == "page3_spell.html" {
|
||||
} else if templateName == "page_3.html" {
|
||||
// Get capacities from template
|
||||
spellsLeftCapacity := GetBlockCapacity(&templateSet, templateName, "spells_left")
|
||||
spellsRightCapacity := GetBlockCapacity(&templateSet, templateName, "spells_right")
|
||||
@@ -148,7 +148,7 @@ func PreparePaginatedPageData(viewModel *CharacterSheetViewModel, templateName s
|
||||
} else {
|
||||
pageData.MagicItems = magicItems
|
||||
}
|
||||
} else if templateName == "page4_equip.html" {
|
||||
} else if templateName == "page_4.html" {
|
||||
// Page 4 needs ALL equipment to properly render containers
|
||||
// The template has complex logic showing containers on left, worn items and container sections on right
|
||||
// Don't truncate based on capacity - let the template handle all items
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestPreparePaginatedPageData_Page1Stats(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
pageData, err := PreparePaginatedPageData(viewModel, "page1_stats.html", 1, "2024-01-01")
|
||||
pageData, err := PreparePaginatedPageData(viewModel, "page_1.html", 1, "2024-01-01")
|
||||
if err != nil {
|
||||
t.Fatalf("PreparePaginatedPageData failed: %v", err)
|
||||
}
|
||||
@@ -36,7 +36,7 @@ func TestPreparePaginatedPageData_Page1Stats(t *testing.T) {
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
var page1Template *TemplateWithMeta
|
||||
for i := range templateSet.Templates {
|
||||
if templateSet.Templates[i].Metadata.Name == "page1_stats.html" {
|
||||
if templateSet.Templates[i].Metadata.Name == "page_1.html" {
|
||||
page1Template = &templateSet.Templates[i]
|
||||
break
|
||||
}
|
||||
@@ -75,7 +75,7 @@ func TestSplitSkillsForColumns(t *testing.T) {
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
var page1Template *TemplateWithMeta
|
||||
for i := range templateSet.Templates {
|
||||
if templateSet.Templates[i].Metadata.Name == "page1_stats.html" {
|
||||
if templateSet.Templates[i].Metadata.Name == "page_1.html" {
|
||||
page1Template = &templateSet.Templates[i]
|
||||
break
|
||||
}
|
||||
@@ -174,7 +174,7 @@ func TestPreparePaginatedPageData_Page2Play(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
pageData, err := PreparePaginatedPageData(viewModel, "page2_play.html", 2, "2024-01-01")
|
||||
pageData, err := PreparePaginatedPageData(viewModel, "page_2.html", 2, "2024-01-01")
|
||||
if err != nil {
|
||||
t.Fatalf("PreparePaginatedPageData failed: %v", err)
|
||||
}
|
||||
@@ -190,9 +190,9 @@ func TestPreparePaginatedPageData_Page2Play(t *testing.T) {
|
||||
func TestPreparePaginatedPageData_Page3Spell(t *testing.T) {
|
||||
// Get capacities from template
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
leftCap := GetBlockCapacity(&templateSet, "page3_spell.html", "spells_left")
|
||||
rightCap := GetBlockCapacity(&templateSet, "page3_spell.html", "spells_right")
|
||||
magicItemsCap := GetBlockCapacity(&templateSet, "page3_spell.html", "magic_items")
|
||||
leftCap := GetBlockCapacity(&templateSet, "page_3.html", "spells_left")
|
||||
rightCap := GetBlockCapacity(&templateSet, "page_3.html", "spells_right")
|
||||
magicItemsCap := GetBlockCapacity(&templateSet, "page_3.html", "magic_items")
|
||||
|
||||
// Create test data exceeding capacities
|
||||
viewModel := &CharacterSheetViewModel{
|
||||
@@ -210,7 +210,7 @@ func TestPreparePaginatedPageData_Page3Spell(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
pageData, err := PreparePaginatedPageData(viewModel, "page3_spell.html", 3, "2024-01-01")
|
||||
pageData, err := PreparePaginatedPageData(viewModel, "page_3.html", 3, "2024-01-01")
|
||||
if err != nil {
|
||||
t.Fatalf("PreparePaginatedPageData failed: %v", err)
|
||||
}
|
||||
@@ -247,7 +247,7 @@ func TestPreparePaginatedPageData_Page4Equipment(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
pageData, err := PreparePaginatedPageData(viewModel, "page4_equip.html", 4, "2024-01-01")
|
||||
pageData, err := PreparePaginatedPageData(viewModel, "page_4.html", 4, "2024-01-01")
|
||||
if err != nil {
|
||||
t.Fatalf("PreparePaginatedPageData failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,314 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestPaginateMultiList_SingleListType tests pagination with a single list type (skills only)
|
||||
func TestPaginateMultiList_SingleListType(t *testing.T) {
|
||||
// Arrange
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
paginator := NewPaginator(templateSet)
|
||||
|
||||
// Create skills that will fit on one page (less than total capacity)
|
||||
skills := make([]SkillViewModel, 20)
|
||||
for i := 0; i < 20; i++ {
|
||||
skills[i] = SkillViewModel{Name: fmt.Sprintf("Skill%d", i+1)}
|
||||
}
|
||||
|
||||
dataMap := map[string]interface{}{
|
||||
"skills": skills,
|
||||
}
|
||||
|
||||
// Act
|
||||
distributions, err := paginator.PaginateMultiList(dataMap, "page_1.html")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(distributions) != 1 {
|
||||
t.Fatalf("Expected 1 page, got %d", len(distributions))
|
||||
}
|
||||
|
||||
// Verify data is distributed correctly
|
||||
page := distributions[0]
|
||||
if page.TemplateName != "page_1.html" {
|
||||
t.Errorf("Expected template 'page_1.html', got '%s'", page.TemplateName)
|
||||
}
|
||||
|
||||
// Check that skills are distributed to appropriate blocks
|
||||
// Note: Skills are distributed to multiple columns, so we count each block
|
||||
totalSkills := 0
|
||||
skillBlocks := []string{"skills_column1", "skills_column2"}
|
||||
for _, blockName := range skillBlocks {
|
||||
if data, exists := page.Data[blockName]; exists {
|
||||
if skillsList, ok := data.([]SkillViewModel); ok {
|
||||
totalSkills += len(skillsList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if totalSkills != 20 {
|
||||
t.Errorf("Expected 20 total skills distributed across columns, got %d", totalSkills)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPaginateMultiList_MultipleListTypes tests pagination with multiple list types
|
||||
func TestPaginateMultiList_MultipleListTypes(t *testing.T) {
|
||||
// Arrange
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
paginator := NewPaginator(templateSet)
|
||||
|
||||
// Create skills and weapons for page_2.html
|
||||
skills := make([]SkillViewModel, 30)
|
||||
for i := 0; i < 30; i++ {
|
||||
skills[i] = SkillViewModel{
|
||||
Name: fmt.Sprintf("Skill%d", i+1),
|
||||
IsLearned: i < 15, // First 15 are learned
|
||||
Category: "Kampf",
|
||||
}
|
||||
}
|
||||
|
||||
weapons := make([]WeaponViewModel, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
weapons[i] = WeaponViewModel{
|
||||
Name: fmt.Sprintf("Weapon%d", i+1),
|
||||
Value: 10 + i,
|
||||
}
|
||||
}
|
||||
|
||||
dataMap := map[string]interface{}{
|
||||
"skills": skills,
|
||||
"weapons": weapons,
|
||||
}
|
||||
|
||||
// Act
|
||||
distributions, err := paginator.PaginateMultiList(dataMap, "page_2.html")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(distributions) == 0 {
|
||||
t.Fatal("Expected at least 1 page")
|
||||
}
|
||||
|
||||
// Verify all data is distributed across pages
|
||||
totalSkills := 0
|
||||
totalWeapons := 0
|
||||
|
||||
for _, dist := range distributions {
|
||||
for _, data := range dist.Data {
|
||||
switch v := data.(type) {
|
||||
case []SkillViewModel:
|
||||
totalSkills += len(v)
|
||||
case []WeaponViewModel:
|
||||
totalWeapons += len(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if totalSkills != 30 {
|
||||
t.Errorf("Expected 30 total skills, got %d", totalSkills)
|
||||
}
|
||||
if totalWeapons != 10 {
|
||||
t.Errorf("Expected 10 total weapons, got %d", totalWeapons)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPaginateMultiList_WithOverflow tests continuation pages are created
|
||||
func TestPaginateMultiList_WithOverflow(t *testing.T) {
|
||||
// Arrange
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
paginator := NewPaginator(templateSet)
|
||||
|
||||
// Create many skills to force continuation pages
|
||||
skills := make([]SkillViewModel, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
skills[i] = SkillViewModel{Name: fmt.Sprintf("Skill%d", i+1)}
|
||||
}
|
||||
|
||||
dataMap := map[string]interface{}{
|
||||
"skills": skills,
|
||||
}
|
||||
|
||||
// Act
|
||||
distributions, err := paginator.PaginateMultiList(dataMap, "page_1.html")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(distributions) <= 1 {
|
||||
t.Fatalf("Expected multiple pages for 100 skills, got %d", len(distributions))
|
||||
}
|
||||
|
||||
// Verify continuation template naming
|
||||
if distributions[0].TemplateName != "page_1.html" {
|
||||
t.Errorf("First page should use base template, got '%s'", distributions[0].TemplateName)
|
||||
}
|
||||
|
||||
for i := 1; i < len(distributions); i++ {
|
||||
expectedName := "page_1.2.html"
|
||||
if distributions[i].TemplateName != expectedName {
|
||||
t.Errorf("Page %d should use continuation template '%s', got '%s'",
|
||||
i+1, expectedName, distributions[i].TemplateName)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify all skills are distributed
|
||||
totalSkills := 0
|
||||
for _, dist := range distributions {
|
||||
for blockName, data := range dist.Data {
|
||||
// Only count skill blocks to avoid counting the same skills multiple times
|
||||
if skillsList, ok := data.([]SkillViewModel); ok &&
|
||||
(blockName == "skills_column1" || blockName == "skills_column2" ||
|
||||
blockName == "skills_column3" || blockName == "skills_column4") {
|
||||
totalSkills += len(skillsList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if totalSkills != 100 {
|
||||
t.Errorf("Expected 100 total skills distributed, got %d", totalSkills)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPaginateMultiList_EmptyLists tests handling of empty data
|
||||
func TestPaginateMultiList_EmptyLists(t *testing.T) {
|
||||
// Arrange
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
paginator := NewPaginator(templateSet)
|
||||
|
||||
dataMap := map[string]interface{}{
|
||||
"skills": []SkillViewModel{},
|
||||
}
|
||||
|
||||
// Act
|
||||
distributions, err := paginator.PaginateMultiList(dataMap, "page_1.html")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(distributions) != 0 {
|
||||
t.Errorf("Expected 0 pages for empty data, got %d", len(distributions))
|
||||
}
|
||||
}
|
||||
|
||||
// TestPaginateMultiList_WithFilters tests filtering by learned/unlearned/language
|
||||
func TestPaginateMultiList_WithFilters(t *testing.T) {
|
||||
// Arrange
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
paginator := NewPaginator(templateSet)
|
||||
|
||||
// Create mixed skills
|
||||
skills := []SkillViewModel{
|
||||
{Name: "Learned1", IsLearned: true, Category: "Kampf"},
|
||||
{Name: "Learned2", IsLearned: true, Category: "Körper"},
|
||||
{Name: "Unlearned1", IsLearned: false, Category: "Kampf"},
|
||||
{Name: "Unlearned2", IsLearned: false, Category: "Social"},
|
||||
{Name: "Language1", Category: "Sprache"},
|
||||
{Name: "Language2", Category: "Sprache"},
|
||||
}
|
||||
|
||||
dataMap := map[string]interface{}{
|
||||
"skills": skills,
|
||||
}
|
||||
|
||||
// Act
|
||||
distributions, err := paginator.PaginateMultiList(dataMap, "page_2.html")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(distributions) == 0 {
|
||||
t.Fatal("Expected at least 1 page")
|
||||
}
|
||||
|
||||
// Verify filtering worked - check that learned, unlearned, and language skills are in separate blocks
|
||||
page := distributions[0]
|
||||
|
||||
if learnedData, ok := page.Data["skills_learned"]; ok {
|
||||
learned := learnedData.([]SkillViewModel)
|
||||
for _, skill := range learned {
|
||||
if !skill.IsLearned {
|
||||
t.Errorf("Found unlearned skill '%s' in learned block", skill.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if languageData, ok := page.Data["skills_languages"]; ok {
|
||||
languages := languageData.([]SkillViewModel)
|
||||
for _, skill := range languages {
|
||||
if skill.Category != "Sprache" {
|
||||
t.Errorf("Found non-language skill '%s' in language block", skill.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPaginateMultiList_UnknownTemplate tests error handling for unknown templates
|
||||
func TestPaginateMultiList_UnknownTemplate(t *testing.T) {
|
||||
// Arrange
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
paginator := NewPaginator(templateSet)
|
||||
|
||||
dataMap := map[string]interface{}{
|
||||
"skills": []SkillViewModel{{Name: "Test"}},
|
||||
}
|
||||
|
||||
// Act
|
||||
distributions, err := paginator.PaginateMultiList(dataMap, "nonexistent_template.html")
|
||||
|
||||
// Assert
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for unknown template, got nil")
|
||||
}
|
||||
|
||||
if distributions != nil {
|
||||
t.Error("Expected nil distributions for error case")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPaginateMultiList_PageNumbering tests that page numbers are sequential
|
||||
func TestPaginateMultiList_PageNumbering(t *testing.T) {
|
||||
// Arrange
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
paginator := NewPaginator(templateSet)
|
||||
|
||||
// Create enough skills for 3 pages
|
||||
skills := make([]SkillViewModel, 120)
|
||||
for i := 0; i < 120; i++ {
|
||||
skills[i] = SkillViewModel{Name: fmt.Sprintf("Skill%d", i+1)}
|
||||
}
|
||||
|
||||
dataMap := map[string]interface{}{
|
||||
"skills": skills,
|
||||
}
|
||||
|
||||
// Act
|
||||
distributions, err := paginator.PaginateMultiList(dataMap, "page_1.html")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
// Verify page numbers are sequential
|
||||
for i, dist := range distributions {
|
||||
expectedPageNum := i + 1
|
||||
if dist.PageNumber != expectedPageNum {
|
||||
t.Errorf("Page %d has incorrect PageNumber: expected %d, got %d",
|
||||
i+1, expectedPageNum, dist.PageNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSliceList_Basic(t *testing.T) {
|
||||
// Arrange
|
||||
items := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
@@ -64,7 +372,7 @@ func TestPaginateSkills_SinglePage(t *testing.T) {
|
||||
// Get column capacities from template
|
||||
var col1Capacity int
|
||||
for _, tmpl := range templateSet.Templates {
|
||||
if tmpl.Metadata.Name == "page1_stats.html" {
|
||||
if tmpl.Metadata.Name == "page_1.html" {
|
||||
for _, block := range tmpl.Metadata.Blocks {
|
||||
if block.Name == "skills_column1" {
|
||||
col1Capacity = block.MaxItems
|
||||
@@ -83,7 +391,7 @@ func TestPaginateSkills_SinglePage(t *testing.T) {
|
||||
}
|
||||
|
||||
// Act
|
||||
pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "")
|
||||
pages, err := paginator.PaginateSkills(skills, "page_1.html", "")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
@@ -127,7 +435,7 @@ func TestPaginateSkills_MultiColumn(t *testing.T) {
|
||||
// Get expected capacity from template
|
||||
var page1Template *TemplateWithMeta
|
||||
for i := range templateSet.Templates {
|
||||
if templateSet.Templates[i].Metadata.Name == "page1_stats.html" {
|
||||
if templateSet.Templates[i].Metadata.Name == "page_1.html" {
|
||||
page1Template = &templateSet.Templates[i]
|
||||
break
|
||||
}
|
||||
@@ -149,7 +457,7 @@ func TestPaginateSkills_MultiColumn(t *testing.T) {
|
||||
}
|
||||
|
||||
// Act
|
||||
pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "")
|
||||
pages, err := paginator.PaginateSkills(skills, "page_1.html", "")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
@@ -186,7 +494,7 @@ func TestPaginateSkills_MultiPage(t *testing.T) {
|
||||
// Get column capacities from template
|
||||
var page1Template *TemplateWithMeta
|
||||
for i := range templateSet.Templates {
|
||||
if templateSet.Templates[i].Metadata.Name == "page1_stats.html" {
|
||||
if templateSet.Templates[i].Metadata.Name == "page_1.html" {
|
||||
page1Template = &templateSet.Templates[i]
|
||||
break
|
||||
}
|
||||
@@ -203,7 +511,7 @@ func TestPaginateSkills_MultiPage(t *testing.T) {
|
||||
}
|
||||
|
||||
// Act
|
||||
pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "")
|
||||
pages, err := paginator.PaginateSkills(skills, "page_1.html", "")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
@@ -247,7 +555,7 @@ func TestPaginateSpells_TwoColumns(t *testing.T) {
|
||||
// Get spell column capacities from template
|
||||
var page3Template *TemplateWithMeta
|
||||
for i := range templateSet.Templates {
|
||||
if templateSet.Templates[i].Metadata.Name == "page3_spell.html" {
|
||||
if templateSet.Templates[i].Metadata.Name == "page_3.html" {
|
||||
page3Template = &templateSet.Templates[i]
|
||||
break
|
||||
}
|
||||
@@ -271,7 +579,7 @@ func TestPaginateSpells_TwoColumns(t *testing.T) {
|
||||
}
|
||||
|
||||
// Act
|
||||
pages, err := paginator.PaginateSpells(spells, "page3_spell.html")
|
||||
pages, err := paginator.PaginateSpells(spells, "page_3.html")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
@@ -311,7 +619,7 @@ func TestPaginateSpells_MultiPage(t *testing.T) {
|
||||
// Get spell column capacities from template
|
||||
var page3Template *TemplateWithMeta
|
||||
for i := range templateSet.Templates {
|
||||
if templateSet.Templates[i].Metadata.Name == "page3_spell.html" {
|
||||
if templateSet.Templates[i].Metadata.Name == "page_3.html" {
|
||||
page3Template = &templateSet.Templates[i]
|
||||
break
|
||||
}
|
||||
@@ -332,7 +640,7 @@ func TestPaginateSpells_MultiPage(t *testing.T) {
|
||||
}
|
||||
|
||||
// Act
|
||||
pages, err := paginator.PaginateSpells(spells, "page3_spell.html")
|
||||
pages, err := paginator.PaginateSpells(spells, "page_3.html")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
@@ -366,7 +674,7 @@ func TestPaginateWeapons_SinglePage(t *testing.T) {
|
||||
// Get weapon capacity from template
|
||||
var weaponCapacity int
|
||||
for _, tmpl := range templateSet.Templates {
|
||||
if tmpl.Metadata.Name == "page2_play.html" {
|
||||
if tmpl.Metadata.Name == "page_2.html" {
|
||||
for _, block := range tmpl.Metadata.Blocks {
|
||||
if block.ListType == "weapons" {
|
||||
weaponCapacity = block.MaxItems
|
||||
@@ -388,7 +696,7 @@ func TestPaginateWeapons_SinglePage(t *testing.T) {
|
||||
}
|
||||
|
||||
// Act
|
||||
pages, err := paginator.PaginateWeapons(weapons, "page2_play.html")
|
||||
pages, err := paginator.PaginateWeapons(weapons, "page_2.html")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
@@ -415,13 +723,13 @@ func TestPaginateWeapons_MultiPage(t *testing.T) {
|
||||
// Get weapon capacity from template
|
||||
var page2Template *TemplateWithMeta
|
||||
for i := range templateSet.Templates {
|
||||
if templateSet.Templates[i].Metadata.Name == "page2_play.html" {
|
||||
if templateSet.Templates[i].Metadata.Name == "page_2.html" {
|
||||
page2Template = &templateSet.Templates[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if page2Template == nil {
|
||||
t.Fatal("page2_play.html template not found")
|
||||
t.Fatal("page_2.html template not found")
|
||||
}
|
||||
var weaponsBlock *BlockMetadata
|
||||
for i := range page2Template.Metadata.Blocks {
|
||||
@@ -442,7 +750,7 @@ func TestPaginateWeapons_MultiPage(t *testing.T) {
|
||||
}
|
||||
|
||||
// Act
|
||||
pages, err := paginator.PaginateWeapons(weapons, "page2_play.html")
|
||||
pages, err := paginator.PaginateWeapons(weapons, "page_2.html")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
@@ -479,11 +787,11 @@ func TestCalculatePagesNeeded(t *testing.T) {
|
||||
var page1Template, page2Template, page3Template *TemplateWithMeta
|
||||
for i := range templateSet.Templates {
|
||||
switch templateSet.Templates[i].Metadata.Name {
|
||||
case "page1_stats.html":
|
||||
case "page_1.html":
|
||||
page1Template = &templateSet.Templates[i]
|
||||
case "page2_play.html":
|
||||
case "page_2.html":
|
||||
page2Template = &templateSet.Templates[i]
|
||||
case "page3_spell.html":
|
||||
case "page_3.html":
|
||||
page3Template = &templateSet.Templates[i]
|
||||
}
|
||||
}
|
||||
@@ -526,16 +834,16 @@ func TestCalculatePagesNeeded(t *testing.T) {
|
||||
itemCount int
|
||||
expectedPages int
|
||||
}{
|
||||
{"10 skills on page1", "page1_stats.html", "skills", 10, 1},
|
||||
{fmt.Sprintf("%d skills on page1", skillCapacity), "page1_stats.html", "skills", skillCapacity, 1},
|
||||
{fmt.Sprintf("%d skills on page1", skillCapacity+1), "page1_stats.html", "skills", skillCapacity + 1, 2},
|
||||
{"100 skills on page1", "page1_stats.html", "skills", 100, (100 + skillCapacity - 1) / skillCapacity}, // Dynamic calculation
|
||||
{"10 weapons on page2", "page2_play.html", "weapons", 10, (10 + weaponCapacity - 1) / weaponCapacity}, // Dynamic calculation
|
||||
{fmt.Sprintf("%d weapons on page2", weaponCapacity), "page2_play.html", "weapons", weaponCapacity, 1},
|
||||
{fmt.Sprintf("%d weapons on page2", weaponCapacity+1), "page2_play.html", "weapons", weaponCapacity + 1, 2},
|
||||
{"10 spells on page3", "page3_spell.html", "spells", 10, (10 + spellCapacity - 1) / spellCapacity}, // Dynamic calculation
|
||||
{fmt.Sprintf("%d spells on page3", spellCapacity), "page3_spell.html", "spells", spellCapacity, 1},
|
||||
{fmt.Sprintf("%d spells on page3", spellCapacity+1), "page3_spell.html", "spells", spellCapacity + 1, 2},
|
||||
{"10 skills on page1", "page_1.html", "skills", 10, 1},
|
||||
{fmt.Sprintf("%d skills on page1", skillCapacity), "page_1.html", "skills", skillCapacity, 1},
|
||||
{fmt.Sprintf("%d skills on page1", skillCapacity+1), "page_1.html", "skills", skillCapacity + 1, 2},
|
||||
{"100 skills on page1", "page_1.html", "skills", 100, (100 + skillCapacity - 1) / skillCapacity}, // Dynamic calculation
|
||||
{"10 weapons on page2", "page_2.html", "weapons", 10, (10 + weaponCapacity - 1) / weaponCapacity}, // Dynamic calculation
|
||||
{fmt.Sprintf("%d weapons on page2", weaponCapacity), "page_2.html", "weapons", weaponCapacity, 1},
|
||||
{fmt.Sprintf("%d weapons on page2", weaponCapacity+1), "page_2.html", "weapons", weaponCapacity + 1, 2},
|
||||
{"10 spells on page3", "page_3.html", "spells", 10, (10 + spellCapacity - 1) / spellCapacity}, // Dynamic calculation
|
||||
{fmt.Sprintf("%d spells on page3", spellCapacity), "page_3.html", "spells", spellCapacity, 1},
|
||||
{fmt.Sprintf("%d spells on page3", spellCapacity+1), "page_3.html", "spells", spellCapacity + 1, 2},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -562,7 +870,7 @@ func TestPaginateSkills_EmptyList(t *testing.T) {
|
||||
skills := []SkillViewModel{}
|
||||
|
||||
// Act
|
||||
pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "")
|
||||
pages, err := paginator.PaginateSkills(skills, "page_1.html", "")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
|
||||
@@ -11,27 +11,27 @@ func TestGenerateContinuationTemplateName(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "First page returns original",
|
||||
template: "page1_stats.html",
|
||||
template: "page_1.html",
|
||||
pageNum: 1,
|
||||
expected: "page1_stats.html",
|
||||
expected: "page_1.html",
|
||||
},
|
||||
{
|
||||
name: "Second page gets continuation name",
|
||||
template: "page1_stats.html",
|
||||
template: "page_1.html",
|
||||
pageNum: 2,
|
||||
expected: "page1.2_stats.html",
|
||||
expected: "page_1.2.html",
|
||||
},
|
||||
{
|
||||
name: "Third page uses same continuation template (.2)",
|
||||
template: "page2_play.html",
|
||||
template: "page_2.html",
|
||||
pageNum: 3,
|
||||
expected: "page2.2_play.html",
|
||||
expected: "page_2.2.html",
|
||||
},
|
||||
{
|
||||
name: "All continuation pages use .2 template",
|
||||
template: "page1_stats.html",
|
||||
template: "page_1.html",
|
||||
pageNum: 10,
|
||||
expected: "page1.2_stats.html",
|
||||
expected: "page_1.2.html",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -53,23 +53,23 @@ func TestExtractBaseTemplateName(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "Base template returns itself",
|
||||
template: "page1_stats.html",
|
||||
expected: "page1_stats.html",
|
||||
template: "page_1.html",
|
||||
expected: "page_1.html",
|
||||
},
|
||||
{
|
||||
name: "Continuation template returns base",
|
||||
template: "page1.2_stats.html",
|
||||
expected: "page1_stats.html",
|
||||
template: "page_1.2.html",
|
||||
expected: "page_1.html",
|
||||
},
|
||||
{
|
||||
name: "Multiple digit continuation",
|
||||
template: "page1.10_stats.html",
|
||||
expected: "page1_stats.html",
|
||||
template: "page_1.10.html",
|
||||
expected: "page_1.html",
|
||||
},
|
||||
{
|
||||
name: "Different page type",
|
||||
template: "page2.3_play.html",
|
||||
expected: "page2_play.html",
|
||||
template: "page_2.3.html",
|
||||
expected: "page_2.html",
|
||||
},
|
||||
{
|
||||
name: "Template without underscore",
|
||||
@@ -90,7 +90,7 @@ func TestExtractBaseTemplateName(t *testing.T) {
|
||||
|
||||
func TestContinuationRoundTrip(t *testing.T) {
|
||||
// Test that generating a continuation name and extracting the base works correctly
|
||||
original := "page1_stats.html"
|
||||
original := "page_1.html"
|
||||
|
||||
for pageNum := 1; pageNum <= 5; pageNum++ {
|
||||
continuation := GenerateContinuationTemplateName(original, pageNum)
|
||||
|
||||
@@ -20,53 +20,23 @@ func RenderPageWithContinuations(
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
paginator := NewPaginator(templateSet)
|
||||
|
||||
// Determine which list type this template handles
|
||||
var distributions []PageDistribution
|
||||
var err error
|
||||
// Build data map from view model
|
||||
dataMap := map[string]interface{}{
|
||||
"skills": viewModel.Skills,
|
||||
"weapons": viewModel.Weapons,
|
||||
"spells": viewModel.Spells,
|
||||
"equipment": viewModel.Equipment,
|
||||
"magicItems": viewModel.MagicItems,
|
||||
}
|
||||
|
||||
switch templateName {
|
||||
case "page1_stats.html":
|
||||
// Paginate skills
|
||||
distributions, err = paginator.PaginateSkills(viewModel.Skills, templateName, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to paginate skills: %w", err)
|
||||
}
|
||||
// Use unified pagination for all templates
|
||||
distributions, err := paginator.PaginateMultiList(dataMap, templateName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to paginate: %w", err)
|
||||
}
|
||||
|
||||
case "page2_play.html":
|
||||
// Page 2 has both skills and weapons that overflow together
|
||||
// Use multi-list pagination so remaining items from both lists go to continuation pages
|
||||
distributions, err = paginator.PaginatePage2PlayLists(viewModel.Skills, viewModel.Weapons, templateName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to paginate page2 lists: %w", err)
|
||||
}
|
||||
|
||||
case "page3_spell.html":
|
||||
// Paginate spells
|
||||
distributions, err = paginator.PaginateSpells(viewModel.Spells, templateName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to paginate spells: %w", err)
|
||||
}
|
||||
|
||||
case "page4_equip.html":
|
||||
// Page 4 has a complex container-based layout where items are grouped by containers.
|
||||
// The template expects the full equipment list to properly render containers and their contents.
|
||||
// Pagination doesn't make sense here - render as single page with all equipment.
|
||||
pageData, err := PreparePaginatedPageData(viewModel, templateName, startPageNumber, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
html, err := loader.RenderTemplateWithInlinedResources(templateName, pageData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pdf, err := renderer.RenderHTMLToPDF(html)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return [][]byte{pdf}, nil
|
||||
|
||||
default:
|
||||
// For unknown templates, render single page without pagination
|
||||
// If no distributions (empty data), render single empty page
|
||||
if len(distributions) == 0 {
|
||||
pageData, err := PreparePaginatedPageData(viewModel, templateName, startPageNumber, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -82,24 +52,7 @@ func RenderPageWithContinuations(
|
||||
return [][]byte{pdf}, nil
|
||||
}
|
||||
|
||||
// If only one page, use the simplified approach
|
||||
if len(distributions) == 1 {
|
||||
pageData, err := PreparePaginatedPageData(viewModel, templateName, startPageNumber, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
html, err := loader.RenderTemplateWithInlinedResources(templateName, pageData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pdf, err := renderer.RenderHTMLToPDF(html)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return [][]byte{pdf}, nil
|
||||
}
|
||||
|
||||
// Multiple pages needed - render each one
|
||||
// Render each distributed page
|
||||
for i, dist := range distributions {
|
||||
pageData := &PageData{
|
||||
Character: viewModel.Character,
|
||||
@@ -112,58 +65,10 @@ func RenderPageWithContinuations(
|
||||
},
|
||||
}
|
||||
|
||||
// Populate the page data based on the distribution
|
||||
switch templateName {
|
||||
case "page1_stats.html":
|
||||
// Extract skills from distribution
|
||||
if col1, ok := dist.Data["skills_column1"].([]SkillViewModel); ok {
|
||||
pageData.SkillsColumn1 = col1
|
||||
}
|
||||
if col2, ok := dist.Data["skills_column2"].([]SkillViewModel); ok {
|
||||
pageData.SkillsColumn2 = col2
|
||||
}
|
||||
// Combine for backward compatibility
|
||||
pageData.Skills = append(pageData.SkillsColumn1, pageData.SkillsColumn2...)
|
||||
// Populate page data from distribution
|
||||
populatePageDataFromDistribution(pageData, dist)
|
||||
|
||||
case "page2_play.html":
|
||||
// Extract all lists from distribution (skills and weapons)
|
||||
if weapons, ok := dist.Data["weapons_main"].([]WeaponViewModel); ok {
|
||||
pageData.Weapons = weapons
|
||||
}
|
||||
if learned, ok := dist.Data["skills_learned"].([]SkillViewModel); ok {
|
||||
pageData.SkillsLearned = learned
|
||||
}
|
||||
if unlearned, ok := dist.Data["skills_unlearned"].([]SkillViewModel); ok {
|
||||
// Unlearned skills are typically shown via general skills list
|
||||
// Add to Skills for template compatibility
|
||||
pageData.Skills = append(pageData.Skills, unlearned...)
|
||||
}
|
||||
if languages, ok := dist.Data["skills_languages"].([]SkillViewModel); ok {
|
||||
pageData.SkillsLanguage = languages
|
||||
}
|
||||
|
||||
case "page3_spell.html":
|
||||
// Extract spells from distribution
|
||||
if left, ok := dist.Data["spells_left"].([]SpellViewModel); ok {
|
||||
pageData.SpellsLeft = left
|
||||
}
|
||||
if right, ok := dist.Data["spells_right"].([]SpellViewModel); ok {
|
||||
pageData.SpellsRight = right
|
||||
}
|
||||
// Combine for backward compatibility
|
||||
pageData.Spells = append(pageData.SpellsLeft, pageData.SpellsRight...)
|
||||
|
||||
case "page4_equip.html":
|
||||
// Extract equipment from distribution
|
||||
if equipment, ok := dist.Data["equipment_worn"].([]EquipmentViewModel); ok {
|
||||
pageData.Equipment = append(pageData.Equipment, equipment...)
|
||||
}
|
||||
if equipment, ok := dist.Data["equipment_carried"].([]EquipmentViewModel); ok {
|
||||
pageData.Equipment = append(pageData.Equipment, equipment...)
|
||||
}
|
||||
}
|
||||
|
||||
// Render the page (use continuation template name if needed)
|
||||
// Render the page
|
||||
html, err := loader.RenderTemplateWithInlinedResources(dist.TemplateName, pageData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to render %s: %w", dist.TemplateName, err)
|
||||
@@ -180,6 +85,84 @@ func RenderPageWithContinuations(
|
||||
return pdfs, nil
|
||||
}
|
||||
|
||||
// populatePageDataFromDistribution populates PageData from a distribution
|
||||
// This replaces the hardcoded switch statements for each template type
|
||||
func populatePageDataFromDistribution(pageData *PageData, dist PageDistribution) {
|
||||
// Populate data based on block names in distribution
|
||||
for blockName, data := range dist.Data {
|
||||
switch blockName {
|
||||
// Skills blocks
|
||||
case "skills_column1":
|
||||
if skills, ok := data.([]SkillViewModel); ok {
|
||||
pageData.SkillsColumn1 = skills
|
||||
pageData.Skills = append(pageData.Skills, skills...)
|
||||
}
|
||||
case "skills_column2":
|
||||
if skills, ok := data.([]SkillViewModel); ok {
|
||||
pageData.SkillsColumn2 = skills
|
||||
pageData.Skills = append(pageData.Skills, skills...)
|
||||
}
|
||||
case "skills_column3":
|
||||
if skills, ok := data.([]SkillViewModel); ok {
|
||||
pageData.SkillsColumn3 = skills
|
||||
pageData.Skills = append(pageData.Skills, skills...)
|
||||
}
|
||||
case "skills_column4":
|
||||
if skills, ok := data.([]SkillViewModel); ok {
|
||||
pageData.SkillsColumn4 = skills
|
||||
pageData.Skills = append(pageData.Skills, skills...)
|
||||
}
|
||||
case "skills_learned":
|
||||
if skills, ok := data.([]SkillViewModel); ok {
|
||||
pageData.SkillsLearned = skills
|
||||
}
|
||||
case "skills_unlearned":
|
||||
if skills, ok := data.([]SkillViewModel); ok {
|
||||
// Add to general Skills list for template compatibility
|
||||
pageData.Skills = append(pageData.Skills, skills...)
|
||||
}
|
||||
case "skills_languages":
|
||||
if skills, ok := data.([]SkillViewModel); ok {
|
||||
pageData.SkillsLanguage = skills
|
||||
}
|
||||
|
||||
// Weapons blocks
|
||||
case "weapons_main":
|
||||
if weapons, ok := data.([]WeaponViewModel); ok {
|
||||
pageData.Weapons = weapons
|
||||
}
|
||||
|
||||
// Spells blocks
|
||||
case "spells_left":
|
||||
if spells, ok := data.([]SpellViewModel); ok {
|
||||
pageData.SpellsLeft = spells
|
||||
pageData.Spells = append(pageData.Spells, spells...)
|
||||
}
|
||||
case "spells_right":
|
||||
if spells, ok := data.([]SpellViewModel); ok {
|
||||
pageData.SpellsRight = spells
|
||||
pageData.Spells = append(pageData.Spells, spells...)
|
||||
}
|
||||
|
||||
// Equipment blocks
|
||||
case "equipment_worn":
|
||||
if equipment, ok := data.([]EquipmentViewModel); ok {
|
||||
pageData.Equipment = append(pageData.Equipment, equipment...)
|
||||
}
|
||||
case "equipment_carried":
|
||||
if equipment, ok := data.([]EquipmentViewModel); ok {
|
||||
pageData.Equipment = append(pageData.Equipment, equipment...)
|
||||
}
|
||||
|
||||
// Magic items
|
||||
case "magic_items":
|
||||
if items, ok := data.([]MagicItemViewModel); ok {
|
||||
pageData.MagicItems = items
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MergePDFs merges multiple PDF byte slices into a single PDF
|
||||
func MergePDFs(pdfList [][]byte, outputPath string) error {
|
||||
if len(pdfList) == 0 {
|
||||
|
||||
@@ -61,7 +61,7 @@ func TestRenderWithContinuations_SkillsOverflow(t *testing.T) {
|
||||
// Act - Render page1 with continuations
|
||||
pdfs, err := RenderPageWithContinuations(
|
||||
viewModel,
|
||||
"page1_stats.html",
|
||||
"page_1.html",
|
||||
1,
|
||||
"20.12.2025",
|
||||
loader,
|
||||
@@ -77,7 +77,7 @@ func TestRenderWithContinuations_SkillsOverflow(t *testing.T) {
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
var skillsCapacity int
|
||||
for _, tmpl := range templateSet.Templates {
|
||||
if tmpl.Metadata.Name == "page1_stats.html" {
|
||||
if tmpl.Metadata.Name == "page_1.html" {
|
||||
for _, block := range tmpl.Metadata.Blocks {
|
||||
if block.ListType == "skills" {
|
||||
skillsCapacity += block.MaxItems
|
||||
@@ -163,7 +163,7 @@ func TestRenderWithContinuations_NoOverflow(t *testing.T) {
|
||||
// Act
|
||||
pdfs, err := RenderPageWithContinuations(
|
||||
viewModel,
|
||||
"page1_stats.html",
|
||||
"page_1.html",
|
||||
1,
|
||||
"20.12.2025",
|
||||
loader,
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
// TemplateMetadata contains information about a template's capacity and requirements
|
||||
type TemplateMetadata struct {
|
||||
Name string // Template name (e.g., "page1_stats.html")
|
||||
Name string // Template name (e.g., "page_1.html")
|
||||
PageType string // "stats", "play", "spell", "equip"
|
||||
Description string
|
||||
Blocks []BlockMetadata // List blocks and their capacities
|
||||
@@ -49,10 +49,10 @@ func LoadTemplateSetFromFiles(templateDir string) (TemplateSet, error) {
|
||||
pageType string
|
||||
description string
|
||||
}{
|
||||
{"page1_stats.html", "stats", "Statistikseite mit Grundwerten"},
|
||||
{"page2_play.html", "play", "Spielbogen mit gelernten Fertigkeiten und Waffen"},
|
||||
{"page3_spell.html", "spell", "Zauberseite mit Zauberliste"},
|
||||
{"page4_equip.html", "equip", "Ausrüstungsseite"},
|
||||
{"page_1.html", "stats", "Statistikseite mit Grundwerten"},
|
||||
{"page_2.html", "play", "Spielbogen mit gelernten Fertigkeiten und Waffen"},
|
||||
{"page_3.html", "spell", "Zauberseite mit Zauberliste"},
|
||||
{"page_4.html", "equip", "Ausrüstungsseite"},
|
||||
}
|
||||
|
||||
// Load each template file and parse its metadata
|
||||
@@ -106,7 +106,7 @@ func getHardcodedTemplateSet() TemplateSet {
|
||||
Templates: []TemplateWithMeta{
|
||||
{
|
||||
Metadata: TemplateMetadata{
|
||||
Name: "page1_stats.html",
|
||||
Name: "page_1.html",
|
||||
PageType: "stats",
|
||||
Description: "Statistikseite mit Grundwerten",
|
||||
Blocks: []BlockMetadata{
|
||||
@@ -124,11 +124,11 @@ func getHardcodedTemplateSet() TemplateSet {
|
||||
},
|
||||
},
|
||||
},
|
||||
Path: "templates/Default_A4_Quer/page1_stats.html",
|
||||
Path: "templates/Default_A4_Quer/page_1.html",
|
||||
},
|
||||
{
|
||||
Metadata: TemplateMetadata{
|
||||
Name: "page2_play.html",
|
||||
Name: "page_2.html",
|
||||
PageType: "play",
|
||||
Description: "Spielbogen mit gelernten Fertigkeiten und Waffen",
|
||||
Blocks: []BlockMetadata{
|
||||
@@ -157,11 +157,11 @@ func getHardcodedTemplateSet() TemplateSet {
|
||||
},
|
||||
},
|
||||
},
|
||||
Path: "templates/Default_A4_Quer/page2_play.html",
|
||||
Path: "templates/Default_A4_Quer/page_2.html",
|
||||
},
|
||||
{
|
||||
Metadata: TemplateMetadata{
|
||||
Name: "page3_spell.html",
|
||||
Name: "page_3.html",
|
||||
PageType: "spell",
|
||||
Description: "Zauberseite mit Zauberliste",
|
||||
Blocks: []BlockMetadata{
|
||||
@@ -184,11 +184,11 @@ func getHardcodedTemplateSet() TemplateSet {
|
||||
},
|
||||
},
|
||||
},
|
||||
Path: "templates/Default_A4_Quer/page3_spell.html",
|
||||
Path: "templates/Default_A4_Quer/page_3.html",
|
||||
},
|
||||
{
|
||||
Metadata: TemplateMetadata{
|
||||
Name: "page4_equip.html",
|
||||
Name: "page_4.html",
|
||||
PageType: "equip",
|
||||
Description: "Ausrüstungsseite",
|
||||
Blocks: []BlockMetadata{
|
||||
@@ -204,7 +204,7 @@ func getHardcodedTemplateSet() TemplateSet {
|
||||
},
|
||||
},
|
||||
},
|
||||
Path: "templates/Default_A4_Quer/page4_equip.html",
|
||||
Path: "templates/Default_A4_Quer/page_4.html",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -17,17 +17,17 @@ func TestLoadTemplateSetFromFiles(t *testing.T) {
|
||||
t.Fatal("Expected templates, got none")
|
||||
}
|
||||
|
||||
// Find page1_stats.html and verify its metadata matches the HTML comments
|
||||
// Find page_1.html and verify its metadata matches the HTML comments
|
||||
var page1 *TemplateWithMeta
|
||||
for i := range templateSet.Templates {
|
||||
if templateSet.Templates[i].Metadata.Name == "page1_stats.html" {
|
||||
if templateSet.Templates[i].Metadata.Name == "page_1.html" {
|
||||
page1 = &templateSet.Templates[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if page1 == nil {
|
||||
t.Fatal("page1_stats.html not found in template set")
|
||||
t.Fatal("page_1.html not found in template set")
|
||||
}
|
||||
|
||||
// Check that blocks were parsed from HTML
|
||||
@@ -36,7 +36,7 @@ func TestLoadTemplateSetFromFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
// Verify skills_column1 block - read expected value directly from template file
|
||||
templateContent, err := os.ReadFile("../templates/Default_A4_Quer/page1_stats.html")
|
||||
templateContent, err := os.ReadFile("../templates/Default_A4_Quer/page_1.html")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read template file: %v", err)
|
||||
}
|
||||
@@ -85,7 +85,7 @@ func TestDefaultA4QuerTemplateSet_LoadsFromFiles(t *testing.T) {
|
||||
|
||||
// Verify metadata comes from template files, not hardcoded
|
||||
// Read expected value directly from template file
|
||||
templateContent, err := os.ReadFile("../templates/Default_A4_Quer/page3_spell.html")
|
||||
templateContent, err := os.ReadFile("../templates/Default_A4_Quer/page_3.html")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read template file: %v", err)
|
||||
}
|
||||
@@ -105,14 +105,14 @@ func TestDefaultA4QuerTemplateSet_LoadsFromFiles(t *testing.T) {
|
||||
|
||||
var page3 *TemplateWithMeta
|
||||
for i := range templateSet.Templates {
|
||||
if templateSet.Templates[i].Metadata.Name == "page3_spell.html" {
|
||||
if templateSet.Templates[i].Metadata.Name == "page_3.html" {
|
||||
page3 = &templateSet.Templates[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if page3 == nil {
|
||||
t.Fatal("page3_spell.html not found")
|
||||
t.Fatal("page_3.html not found")
|
||||
}
|
||||
|
||||
var spellsLeft *BlockMetadata
|
||||
|
||||
@@ -68,8 +68,8 @@ func (tl *TemplateLoader) LoadTemplates() error {
|
||||
}
|
||||
|
||||
// RenderTemplate renders a specific template with the given data
|
||||
// For continuation templates (e.g., "page1.2_stats.html"), it automatically
|
||||
// falls back to the base template (e.g., "page1_stats.html") if the continuation
|
||||
// For continuation templates (e.g., "page_1.2.html"), it automatically
|
||||
// falls back to the base template (e.g., "page_1.html") if the continuation
|
||||
// template doesn't exist as a physical file
|
||||
func (tl *TemplateLoader) RenderTemplate(templateName string, data interface{}) (string, error) {
|
||||
if tl.templates == nil {
|
||||
@@ -79,7 +79,7 @@ func (tl *TemplateLoader) RenderTemplate(templateName string, data interface{})
|
||||
tmpl := tl.templates.Lookup(templateName)
|
||||
if tmpl == nil {
|
||||
// Try to extract base template name for continuation pages
|
||||
// e.g., "page1.2_stats.html" -> "page1_stats.html"
|
||||
// e.g., "page_1.2.html" -> "page_1.html"
|
||||
baseTemplateName := ExtractBaseTemplateName(templateName)
|
||||
if baseTemplateName != templateName {
|
||||
tmpl = tl.templates.Lookup(baseTemplateName)
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestRenderTemplate_BasicData(t *testing.T) {
|
||||
}
|
||||
|
||||
// Act
|
||||
html, err := loader.RenderTemplate("page1_stats.html", data)
|
||||
html, err := loader.RenderTemplate("page_1.html", data)
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
@@ -75,7 +75,7 @@ func TestGetTemplateMetadata(t *testing.T) {
|
||||
}
|
||||
|
||||
// Act
|
||||
metadata := loader.GetTemplateMetadata("page3_spell.html")
|
||||
metadata := loader.GetTemplateMetadata("page_3.html")
|
||||
|
||||
// Assert
|
||||
if len(metadata) == 0 {
|
||||
@@ -86,13 +86,13 @@ func TestGetTemplateMetadata(t *testing.T) {
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
var page3Template *TemplateWithMeta
|
||||
for i := range templateSet.Templates {
|
||||
if templateSet.Templates[i].Metadata.Name == "page3_spell.html" {
|
||||
if templateSet.Templates[i].Metadata.Name == "page_3.html" {
|
||||
page3Template = &templateSet.Templates[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if page3Template == nil {
|
||||
t.Fatal("page3_spell.html template not found")
|
||||
t.Fatal("page_3.html template not found")
|
||||
}
|
||||
|
||||
// Get expected values from template
|
||||
@@ -150,7 +150,7 @@ func TestRenderTemplate_WithSkills(t *testing.T) {
|
||||
}
|
||||
|
||||
// Act
|
||||
html, err := loader.RenderTemplate("page1_stats.html", data)
|
||||
html, err := loader.RenderTemplate("page_1.html", data)
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
|
||||
@@ -1,102 +1,11 @@
|
||||
## COMPLETED
|
||||
|
||||
* ✅ Page 2 Weapons Table now shows the following information:
|
||||
- For each Weapon a character has:
|
||||
- Its name
|
||||
- ✅ the Fertigkeitswert (EW): Waffenfertigkeit.Fertigkeitswert + Character.AngriffBonus + Weapon.Anb (if equipped)
|
||||
- ✅ the Schaden (Damage) including character's SchadenBonus and weapon's Schadensbonus
|
||||
- ✅ If it is a ranged weapon, the ranges for near, medium and far
|
||||
|
||||
Implementation notes:
|
||||
- ✅ AngriffBonus and SchadenBonus are now calculated in DerivedValueSet from character attributes
|
||||
- ✅ mapWeapons() function now:
|
||||
- Calculates EW = Waffenfertigkeit.Fertigkeitswert + Character.AngriffBonus + Weapon.Anb
|
||||
- Matches Waffenfertigkeiten with equipped Waffen by name
|
||||
- Adds weapon attack bonus (Anb) if weapon is equipped
|
||||
- ✅ Added test TestMapWeapons_WithEWCalculation to verify correct EW calculation
|
||||
- ✅ Used TDD approach: wrote failing test first, then implemented solution
|
||||
|
||||
* ✅ Template MaxItems expectations are now dynamic:
|
||||
- Tests now read MaxItems values directly from template HTML comments
|
||||
- Updated TestLoadTemplateSetFromFiles to parse templates dynamically
|
||||
- Updated TestDefaultA4QuerTemplateSet_LoadsFromFiles to use dynamic expectations
|
||||
- Updated TestPaginationUsesTemplateMetadata to read from template files
|
||||
- Tests will automatically adapt when template capacities change
|
||||
|
||||
* ✅ Weapon model enhancements:
|
||||
- ✅ Added RangeNear, RangeMiddle, RangeFar integer fields to gsm_weapons table
|
||||
- ✅ Added IsRanged() method that returns true if at least one range value > 0
|
||||
- ✅ Damage field already exists in gsm_weapons table
|
||||
- ✅ EqWaffe already contains bonus values: Anb (Attack), Schb (Damage), Abwb (Defense)
|
||||
- ✅ All tests pass with new fields
|
||||
|
||||
* ✅ Page 2 Weapons Table - Complete implementation:
|
||||
- ✅ Changed TestVisualInspection_AllPages to load character Fanjo Vetrani (ID 18) from test database
|
||||
- ✅ Damage calculation implemented:
|
||||
- Calculates total damage: Base Weapon Damage + Character SchadenBonus + Weapon Schb
|
||||
- Format: "1W6+3" where +3 = SchadenBonus + Schb
|
||||
- Implemented calculateWeaponDamage() function
|
||||
- Added TestMapWeapons_WithDamageCalculation test
|
||||
- ✅ Ranged weapon ranges implemented:
|
||||
- Shows ranges for ranged weapons (Bogen, Armbrust, etc.)
|
||||
- Format: "Nah/Mittel/Fern" (e.g., "10/30/100")
|
||||
- Implemented calculateWeaponRange() function
|
||||
- Added TestMapWeapons_WithRangedWeaponRanges test
|
||||
- Marks weapons as ranged using IsRanged field
|
||||
- ✅ All tests pass
|
||||
|
||||
* ✅ Continuation pages for overflow items:
|
||||
- When items exceed template capacity, continuation pages are automatically created
|
||||
- Continuation pages follow naming pattern: page1.2_stats.html, page1.3_stats.html, etc.
|
||||
- Template loader automatically falls back to base template for continuation pages
|
||||
- No physical continuation template files needed - reuses base template structure
|
||||
- **NEW: RenderPageWithContinuations() function generates actual PDF files**
|
||||
- Each continuation page is rendered as a separate PDF
|
||||
- PDFs can be merged into a single combined file
|
||||
- Implemented using TDD:
|
||||
- Created comprehensive tests in continuation_test.go
|
||||
- Added GenerateContinuationTemplateName() function
|
||||
- Added ExtractBaseTemplateName() function
|
||||
- Updated paginateList() to generate continuation template names
|
||||
- Updated RenderTemplate() to handle continuation template fallback
|
||||
- **Created RenderPageWithContinuations() to actually render multiple PDFs**
|
||||
- **Created integration test that saves real PDF files to disk**
|
||||
- All existing tests updated to work with dynamic template capacities
|
||||
- Fully tested and working end-to-end
|
||||
- **VERIFIED: 5 continuation pages generated for 50 skills, saved to /tmp/bamort_continuation_test/**
|
||||
|
||||
|
||||
* ✅ 1. create API endpoint for listing available export templates
|
||||
* Endpoint: GET /api/pdf/templates
|
||||
* Returns: JSON array of TemplateInfo objects [{id, name, description}]
|
||||
* Test: TestListTemplates passes
|
||||
* Configuration: Uses config.Cfg.TemplatesDir (default: "./templates")
|
||||
* ✅ 2. create API endpoint for exporting character to PDF.
|
||||
* Endpoint: GET /api/pdf/export/:id?template=xxx&showUserName=true
|
||||
* Endpoint takes parameter "template", "show user name".
|
||||
* Return combined PDF file for download or display in Browser
|
||||
* Renders all 4 pages (stats, play, spells, equipment) with continuation pages
|
||||
* Merges all PDFs into single combined file
|
||||
* Returns PDF with proper headers: Content-Type: application/pdf, Content-Disposition
|
||||
* Tests: TestExportCharacterToPDF, TestExportCharacterToPDF_WithTemplate, TestExportCharacterToPDF_CharacterNotFound all pass
|
||||
* Configuration: Uses config.Cfg.TemplatesDir for template path resolution
|
||||
* Status: ✅ Deployed and running in Docker container, verified with logs
|
||||
* ✅ 3. create exporting function in Frontend
|
||||
* ✅ The UI element to start the export function is to the left side of the character's name (CharacterDetails.vue)
|
||||
* ✅ Template selection dropdown implemented - auto-selects first template
|
||||
* ✅ Export button with loading state (disabled while exporting)
|
||||
* ✅ PDF opens in new browser tab using window.open()
|
||||
* ✅ Translations added for German and English
|
||||
* ✅ API integration: Fetches templates on component load, calls export endpoint with selected template
|
||||
* ✅ Error handling with user-friendly alerts
|
||||
* Status: ✅ Deployed with HMR, ready for testing
|
||||
|
||||
## TODO (Remaining)
|
||||
* 1. create a directory xporttemp in the backend
|
||||
* 2. save the PDF to file during the ExportCharacterToPDF call and return the filename charname+timestamp.pdf. Make shure the filename contains no spaces or special chars that might disturb the download
|
||||
* 3. create an API endpoint to load the file from xporttemp
|
||||
* 4. create a maintenance endpoint to clean up the xporttemp directory. remove all files that are older than 7 days
|
||||
* 5. change the frontend to get the PDF from the new API endpoint
|
||||
* 1. in RenderPageWithContinuations use a switch to select a templatename and the ncall a paginator function that is designed explicitly for this template page with the current content. This will not work when we add more and different templates. We nned a solution where the code can find out which data is present on the template page and if these datastructures are to be continued on a continuation page or on the next regular page in row. it needs to find out how many elements can be rendered into the page or it's continuation page. It needs to handle if there are multiple datastructures present on one page.
|
||||
for this to be accomplished we need some kind of template metadata or inline datastructure(Block) configuration.
|
||||
I want to change the templateName from page1_stats.html to page_1.html and accordingly the continuation page from page1.2_stats.html to page_1.2.html this makes clear that a page is not tight to one or another datastructure.
|
||||
Page 4 has a complex container-based layout we leave this out for the moment.
|
||||
Plan a refacturation of PaginateSkills, PaginatePage2PlayLists and PaginateSpells into one unified function.
|
||||
|
||||
|
||||
|
||||
### Later
|
||||
* continuation of lists does not work as expected but good enough for a first shot
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
// TestPaginationUsesTemplateMetadata verifies tests use actual template MAX values
|
||||
func TestPaginationUsesTemplateMetadata(t *testing.T) {
|
||||
// Read expected values directly from template file
|
||||
templateContent, err := os.ReadFile("../templates/Default_A4_Quer/page2_play.html")
|
||||
templateContent, err := os.ReadFile("../templates/Default_A4_Quer/page_2.html")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read template file: %v", err)
|
||||
}
|
||||
@@ -26,14 +26,14 @@ func TestPaginationUsesTemplateMetadata(t *testing.T) {
|
||||
// Find page2
|
||||
var page2 *TemplateWithMeta
|
||||
for i := range templateSet.Templates {
|
||||
if templateSet.Templates[i].Metadata.Name == "page2_play.html" {
|
||||
if templateSet.Templates[i].Metadata.Name == "page_2.html" {
|
||||
page2 = &templateSet.Templates[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if page2 == nil {
|
||||
t.Fatal("page2_play.html not found")
|
||||
t.Fatal("page_2.html not found")
|
||||
}
|
||||
|
||||
// Verify blocks exist and have correct MAX from template
|
||||
@@ -64,7 +64,7 @@ func TestPaginationUsesTemplateMetadata(t *testing.T) {
|
||||
|
||||
func TestPage2PaginationWithCorrectCapacities(t *testing.T) {
|
||||
// Read expected values directly from template file
|
||||
templateContent, err := os.ReadFile("../templates/Default_A4_Quer/page2_play.html")
|
||||
templateContent, err := os.ReadFile("../templates/Default_A4_Quer/page_2.html")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read template file: %v", err)
|
||||
}
|
||||
@@ -88,7 +88,7 @@ func TestPage2PaginationWithCorrectCapacities(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
pageData, err := PreparePaginatedPageData(viewModel, "page2_play.html", 2, "2024-01-01")
|
||||
pageData, err := PreparePaginatedPageData(viewModel, "page_2.html", 2, "2024-01-01")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to prepare page data: %v", err)
|
||||
}
|
||||
@@ -109,7 +109,7 @@ func TestPage2PaginationWithCorrectCapacities(t *testing.T) {
|
||||
|
||||
func TestPage3MagicItemsCapacity(t *testing.T) {
|
||||
// Read expected values directly from template file
|
||||
templateContent, err := os.ReadFile("../templates/Default_A4_Quer/page3_spell.html")
|
||||
templateContent, err := os.ReadFile("../templates/Default_A4_Quer/page_3.html")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read template file: %v", err)
|
||||
}
|
||||
@@ -128,7 +128,7 @@ func TestPage3MagicItemsCapacity(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
pageData, err := PreparePaginatedPageData(viewModel, "page3_spell.html", 3, "2024-01-01")
|
||||
pageData, err := PreparePaginatedPageData(viewModel, "page_3.html", 3, "2024-01-01")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to prepare page data: %v", err)
|
||||
}
|
||||
|
||||
@@ -185,14 +185,16 @@ type PageData struct {
|
||||
|
||||
// Lists sliced according to template block metadata
|
||||
Skills []SkillViewModel
|
||||
SkillsColumn1 []SkillViewModel // For two-column skill layout (page1)
|
||||
SkillsColumn2 []SkillViewModel // For two-column skill layout (page1)
|
||||
SkillsLearned []SkillViewModel // Filtered learned skills (page2)
|
||||
SkillsLanguage []SkillViewModel // Filtered language skills (page2)
|
||||
SkillsColumn1 []SkillViewModel // For two-column skill layout (page_1)
|
||||
SkillsColumn2 []SkillViewModel // For two-column skill layout (page_1)
|
||||
SkillsColumn3 []SkillViewModel // For continuation pages (page_1.2)
|
||||
SkillsColumn4 []SkillViewModel // For continuation pages (page_1.2)
|
||||
SkillsLearned []SkillViewModel // Filtered learned skills (page_2)
|
||||
SkillsLanguage []SkillViewModel // Filtered language skills (page_2)
|
||||
Weapons []WeaponViewModel
|
||||
Spells []SpellViewModel
|
||||
SpellsLeft []SpellViewModel // Left column spells (page3)
|
||||
SpellsRight []SpellViewModel // Right column spells (page3)
|
||||
SpellsLeft []SpellViewModel // Left column spells (page_3)
|
||||
SpellsRight []SpellViewModel // Right column spells (page_3)
|
||||
MagicItems []MagicItemViewModel
|
||||
Equipment []EquipmentViewModel
|
||||
GameResults []GameResultViewModel
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# Development Dockerfile für Go Backend mit Live-Reloading
|
||||
#FROM golang:1.23-alpine
|
||||
FROM golang:1.25-alpine
|
||||
|
||||
# Install necessary packages for CGO and SQLite
|
||||
|
||||
Reference in New Issue
Block a user