""" PDF Worksheet Generator for Breakpilot Units ============================================ Generates printable PDF worksheets from unit definitions. Structure: 1. Title + Learning Objectives 2. Vocabulary Table 3. Key Concepts Summary 4. Practice Exercises (Basic) 5. Challenge Exercises (Advanced) 6. Reflection Questions """ import io from dataclasses import dataclass from typing import Any, Optional, Union # Note: In production, use reportlab or weasyprint for actual PDF generation # This module generates an intermediate format that can be converted to PDF @dataclass class WorksheetSection: """A section of the worksheet""" title: str content_type: str # "text", "table", "exercises", "blanks" content: Any difficulty: int = 1 # 1-4 @dataclass class Worksheet: """Complete worksheet structure""" title: str subtitle: str unit_id: str locale: str sections: list[WorksheetSection] footer: str = "" def to_html(self) -> str: """Convert worksheet to HTML (for PDF conversion via weasyprint)""" html_parts = [ "", "", "", "", "", "", "", f"

{self.title}

", f"

{self.subtitle}

", ] for section in self.sections: html_parts.append(self._render_section(section)) html_parts.extend([ f"", "", "" ]) return "\n".join(html_parts) def _get_styles(self) -> str: return """ @page { size: A4; margin: 2cm; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 11pt; line-height: 1.5; color: #333; } header { text-align: center; margin-bottom: 1.5em; border-bottom: 2px solid #2c5282; padding-bottom: 1em; } h1 { color: #2c5282; margin-bottom: 0.25em; font-size: 20pt; } .subtitle { color: #666; font-style: italic; } h2 { color: #2c5282; border-bottom: 1px solid #e2e8f0; padding-bottom: 0.25em; margin-top: 1.5em; font-size: 14pt; } h3 { color: #4a5568; font-size: 12pt; } table { width: 100%; border-collapse: collapse; margin: 1em 0; } th, td { border: 1px solid #e2e8f0; padding: 0.5em; text-align: left; } th { background-color: #edf2f7; font-weight: bold; } .exercise { margin: 1em 0; padding: 1em; background-color: #f7fafc; border-left: 4px solid #4299e1; } .exercise-number { font-weight: bold; color: #2c5282; } .blank { display: inline-block; min-width: 100px; border-bottom: 1px solid #333; margin: 0 0.25em; } .difficulty { font-size: 9pt; color: #718096; } .difficulty-1 { color: #48bb78; } .difficulty-2 { color: #4299e1; } .difficulty-3 { color: #ed8936; } .difficulty-4 { color: #f56565; } .reflection { margin-top: 2em; padding: 1em; background-color: #fffaf0; border: 1px dashed #ed8936; } .write-area { min-height: 80px; border: 1px solid #e2e8f0; margin: 0.5em 0; background-color: #fff; } footer { margin-top: 2em; padding-top: 1em; border-top: 1px solid #e2e8f0; font-size: 9pt; color: #718096; text-align: center; } ul, ol { margin: 0.5em 0; padding-left: 1.5em; } .objectives { background-color: #ebf8ff; padding: 1em; border-radius: 4px; } """ def _render_section(self, section: WorksheetSection) -> str: parts = [f"

{section.title}

"] if section.content_type == "text": parts.append(f"

{section.content}

") elif section.content_type == "objectives": parts.append("
") elif section.content_type == "table": parts.append("") for header in section.content.get("headers", []): parts.append(f"") parts.append("") for row in section.content.get("rows", []): parts.append("") for cell in row: parts.append(f"") parts.append("") parts.append("
{header}
{cell}
") elif section.content_type == "exercises": for i, ex in enumerate(section.content, 1): diff_class = f"difficulty-{ex.get('difficulty', 1)}" diff_stars = "*" * ex.get("difficulty", 1) parts.append(f"""
Aufgabe {i} ({diff_stars})

{ex.get('question', '')}

{self._render_exercise_input(ex)}
""") elif section.content_type == "blanks": text = section.content # Replace *word* with blank import re text = re.sub(r'\*([^*]+)\*', r"", text) parts.append(f"

{text}

") elif section.content_type == "reflection": parts.append("
") parts.append(f"

{section.content.get('prompt', '')}

") parts.append("
") parts.append("
") parts.append("
") return "\n".join(parts) def _render_exercise_input(self, exercise: dict) -> str: ex_type = exercise.get("type", "text") if ex_type == "multiple_choice": options = exercise.get("options", []) parts = ["") return "\n".join(parts) elif ex_type == "matching": left = exercise.get("left", []) right = exercise.get("right", []) parts = [""] for i, item in enumerate(left): right_item = right[i] if i < len(right) else "" parts.append(f"") parts.append("
BegriffZuordnung
{item}
") return "\n".join(parts) elif ex_type == "sequence": items = exercise.get("items", []) parts = ["

Bringe in die richtige Reihenfolge:

    "] for item in items: parts.append(f"
  1. ") parts.append("
") parts.append(f"

Begriffe: {', '.join(items)}

") return "\n".join(parts) else: return "
" class PDFGenerator: """Generates PDF worksheets from unit definitions""" def __init__(self, locale: str = "de-DE"): self.locale = locale def generate_from_unit(self, unit: dict) -> Worksheet: """ Generate a worksheet from a unit definition. Args: unit: Unit definition dictionary Returns: Worksheet object """ unit_id = unit.get("unit_id", "unknown") title = self._get_localized(unit.get("title"), "Arbeitsblatt") objectives = unit.get("learning_objectives", []) stops = unit.get("stops", []) sections = [] # Learning Objectives if objectives: sections.append(WorksheetSection( title="Lernziele", content_type="objectives", content=objectives )) # Vocabulary Table vocab_section = self._create_vocabulary_section(stops) if vocab_section: sections.append(vocab_section) # Key Concepts Summary concepts_section = self._create_concepts_section(stops) if concepts_section: sections.append(concepts_section) # Basic Exercises basic_exercises = self._create_basic_exercises(stops) if basic_exercises: sections.append(WorksheetSection( title="Ubungen - Basis", content_type="exercises", content=basic_exercises, difficulty=1 )) # Challenge Exercises challenge_exercises = self._create_challenge_exercises(stops, unit) if challenge_exercises: sections.append(WorksheetSection( title="Ubungen - Herausforderung", content_type="exercises", content=challenge_exercises, difficulty=3 )) # Reflection sections.append(WorksheetSection( title="Reflexion", content_type="reflection", content={ "prompt": "Erklaere in eigenen Worten, was du heute gelernt hast:" } )) return Worksheet( title=title, subtitle=f"Arbeitsblatt zur Unit: {unit_id}", unit_id=unit_id, locale=self.locale, sections=sections, footer="Generiert mit Breakpilot | www.breakpilot.de" ) def _get_localized(self, obj: Union[dict, str, None], default: str = "") -> str: """Get localized string from object""" if not obj: return default if isinstance(obj, str): return obj if isinstance(obj, dict): if self.locale in obj: return obj[self.locale] alt_locale = self.locale.replace("-", "_") if alt_locale in obj: return obj[alt_locale] if obj: return next(iter(obj.values())) return default def _create_vocabulary_section(self, stops: list) -> Optional[WorksheetSection]: """Create vocabulary table from stops""" rows = [] for stop in stops: vocab = stop.get("vocab", []) for v in vocab: term = self._get_localized(v.get("term")) hint = self._get_localized(v.get("hint")) if term: rows.append([term, hint or ""]) if not rows: return None return WorksheetSection( title="Wichtige Begriffe", content_type="table", content={ "headers": ["Begriff", "Erklarung"], "rows": rows } ) def _create_concepts_section(self, stops: list) -> Optional[WorksheetSection]: """Create concepts summary from stops""" rows = [] for stop in stops: label = self._get_localized(stop.get("label")) concept = stop.get("concept", {}) why = self._get_localized(concept.get("why")) if label and why: rows.append([label, why]) if not rows: return None return WorksheetSection( title="Zusammenfassung", content_type="table", content={ "headers": ["Station", "Was hast du gelernt?"], "rows": rows } ) def _create_basic_exercises(self, stops: list) -> list[dict]: """Create basic difficulty exercises""" exercises = [] # Vocabulary matching vocab_items = [] for stop in stops: for v in stop.get("vocab", []): term = self._get_localized(v.get("term")) hint = self._get_localized(v.get("hint")) if term and hint: vocab_items.append({"term": term, "hint": hint}) if len(vocab_items) >= 3: exercises.append({ "type": "matching", "question": "Ordne die Begriffe den richtigen Erklarungen zu:", "left": [v["term"] for v in vocab_items[:5]], "right": [v["hint"] for v in vocab_items[:5]], "difficulty": 1 }) # True/False from concepts for stop in stops[:3]: concept = stop.get("concept", {}) why = self._get_localized(concept.get("why")) if why: exercises.append({ "type": "multiple_choice", "question": f"Richtig oder Falsch? {why}", "options": ["Richtig", "Falsch"], "difficulty": 1 }) break # Sequence ordering (for FlightPath) if len(stops) >= 4: labels = [self._get_localized(s.get("label")) for s in stops[:6] if self._get_localized(s.get("label"))] if len(labels) >= 4: import random shuffled = labels.copy() random.shuffle(shuffled) exercises.append({ "type": "sequence", "question": "Bringe die Stationen in die richtige Reihenfolge:", "items": shuffled, "difficulty": 2 }) return exercises def _create_challenge_exercises(self, stops: list, unit: dict) -> list[dict]: """Create challenging exercises""" exercises = [] # Misconception identification for stop in stops: concept = stop.get("concept", {}) misconception = self._get_localized(concept.get("common_misconception")) why = self._get_localized(concept.get("why")) label = self._get_localized(stop.get("label")) if misconception and why: exercises.append({ "type": "multiple_choice", "question": f"Welche Aussage uber {label} ist RICHTIG?", "options": [why, misconception], "difficulty": 3 }) if len(exercises) >= 2: break # Transfer/Application question exercises.append({ "type": "text", "question": "Erklaere einem Freund in 2-3 Satzen, was du gelernt hast:", "difficulty": 3 }) # Critical thinking exercises.append({ "type": "text", "question": "Was moechtest du noch mehr uber dieses Thema erfahren?", "difficulty": 4 }) return exercises def generate_worksheet_html(unit_definition: dict, locale: str = "de-DE") -> str: """ Generate HTML worksheet from unit definition. Args: unit_definition: The unit JSON definition locale: Target locale for content Returns: HTML string ready for PDF conversion """ generator = PDFGenerator(locale=locale) worksheet = generator.generate_from_unit(unit_definition) return worksheet.to_html() def generate_worksheet_pdf(unit_definition: dict, locale: str = "de-DE") -> bytes: """ Generate PDF worksheet from unit definition. Requires weasyprint to be installed: pip install weasyprint Args: unit_definition: The unit JSON definition locale: Target locale for content Returns: PDF bytes """ try: from weasyprint import HTML except ImportError: raise ImportError("weasyprint is required for PDF generation. Install with: pip install weasyprint") html = generate_worksheet_html(unit_definition, locale) pdf_bytes = HTML(string=html).write_pdf() return pdf_bytes