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:
2025-12-21 22:07:46 +01:00
parent 61ebcea3b3
commit 59fe69d35d
28 changed files with 784 additions and 397 deletions
@@ -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,
+20 -20
View File
@@ -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)
}
}
+3 -3
View File
@@ -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 {
+4 -4
View File
@@ -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
+17 -17
View File
@@ -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
View File
@@ -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{}{}
}
+4 -4
View File
@@ -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
+9 -9
View File
@@ -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)
}
+337 -29
View File
@@ -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 {
+17 -17
View File
@@ -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)
+97 -114
View File
@@ -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,
+13 -13
View File
@@ -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
+3 -3
View File
@@ -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)
+5 -5
View File
@@ -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 {
+7 -98
View File
@@ -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
+7 -7
View File
@@ -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)
}
+8 -6
View File
@@ -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
View File
@@ -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