From fc671ea5c375076925b7f57bc2d68f1496f3f197 Mon Sep 17 00:00:00 2001 From: Joey Lu Date: Tue, 5 May 2026 17:17:36 -0700 Subject: [PATCH 1/8] Save landscape orientation backend logic --- backend/api/latex_utils.py | 29 +++++++----- backend/api/views.py | 49 +++++++------------- frontend/src/components/CreateCheatSheet.jsx | 20 +++++++- 3 files changed, 52 insertions(+), 46 deletions(-) diff --git a/backend/api/latex_utils.py b/backend/api/latex_utils.py index 6e85dc5..53ae527 100644 --- a/backend/api/latex_utils.py +++ b/backend/api/latex_utils.py @@ -30,7 +30,6 @@ "large": ("1.2pt", "1.2pt"), } - FONT_SIZE_PATTERN = re.compile(r"^(\d+(?:\.\d+)?)pt$") SPACING_PATTERN = re.compile(r"^(\d+(?:\.\d+)?)pt$") BODY_FONT_COMMAND_PATTERN = re.compile( @@ -47,7 +46,7 @@ LEGACY_ANSWER_LABEL_PATTERN = re.compile(r"\\textbf\{Answer:\}\s*") APP_LAYOUT_COMMENT_LINE_PATTERN = re.compile(r"(?m)^% @cheatsheet-layout .*\n?") APP_LAYOUT_COMMENT_BLOCK_PATTERN = re.compile( - r"(?m)(?:^% @cheatsheet-layout .*\n){4}^%\n?" + r"(?m)(?:^% @cheatsheet-layout .*\n){5}^%\n?" ) @@ -130,27 +129,33 @@ def append_text_heading(lines, text): lines.append(r"\noindent " + text + r"\par") -def build_layout_comment_block(columns=2, font_size="10pt", margins="0.25in", spacing="large"): +def build_layout_comment_block(columns=2, font_size="10pt", margins="0.25in", spacing="large", orientation="portrait"): return [ f"% @cheatsheet-layout columns: {columns} | change layout options up top to update columns", f"% @cheatsheet-layout font_size: {font_size} | change layout options up top to update text size", f"% @cheatsheet-layout spacing: {spacing} | change layout options up top to update spacing", f"% @cheatsheet-layout margins: {margins} | change layout options up top to update margins", + f"% @cheatsheet-layout orientation: {orientation} | change layout options up top to update orientation", "%", ] -def build_dynamic_header(columns=2, font_size="10pt", margins="0.25in", spacing="large"): +def build_dynamic_header(columns=2, font_size="10pt", margins="0.25in", spacing="large", orientation="portrait"): """ Build a dynamic LaTeX header based on user-selected options. """ size_command = get_body_font_command(font_size) spacing_values = get_spacing_values(spacing, font_size) doc_class, doc_class_size = get_document_class(font_size) + + # Inject landscape orientation if selected + geometry_options = f"margin={margins}" + if orientation == "landscape": + geometry_options += ", landscape" header_lines = [ f"\\documentclass[{doc_class_size},fleqn]{{{doc_class}}}", - f"\\usepackage[margin={margins}]{{geometry}}", + f"\\usepackage[{geometry_options}]{{geometry}}", "\\usepackage{amsmath, amssymb}", "\\usepackage{enumitem}", "\\usepackage{multicol}", @@ -188,12 +193,12 @@ def build_dynamic_footer(columns=2): return "\n".join(footer_lines) -def normalize_latex_layout(content, columns=2, font_size="10pt", margins="0.25in", spacing="large"): +def normalize_latex_layout(content, columns=2, font_size="10pt", margins="0.25in", spacing="large", orientation="portrait"): """Rebuild document wrappers so current layout controls apply to existing LaTeX content.""" if not content: return content - header = build_dynamic_header(columns, font_size, margins, spacing) + header = build_dynamic_header(columns, font_size, margins, spacing, orientation) footer = build_dynamic_footer(columns) if r"\begin{document}" not in content or r"\end{document}" not in content: @@ -219,18 +224,18 @@ def normalize_latex_layout(content, columns=2, font_size="10pt", margins="0.25in body = re.sub(r"(?m)^\\vspace\{[^}]+\}\s*$", rf"\\vspace{{{formula_gap}}}", body) body = body.strip("\n") - layout_comment_block = "\n".join(build_layout_comment_block(columns, font_size, margins, spacing)) + layout_comment_block = "\n".join(build_layout_comment_block(columns, font_size, margins, spacing, orientation)) body = layout_comment_block + ("\n" + body if body else "") return header + body + ("\n" if body else "") + footer -def build_latex_for_formulas(selected_formulas, columns=2, font_size="10pt", margins="0.25in", spacing="large"): +def build_latex_for_formulas(selected_formulas, columns=2, font_size="10pt", margins="0.25in", spacing="large", orientation="portrait"): """ Given a list of selected formulas (each with class_name, category, name, latex), build a complete LaTeX document. """ - header = build_dynamic_header(columns, font_size, margins, spacing) + header = build_dynamic_header(columns, font_size, margins, spacing, orientation) footer = build_dynamic_footer(columns) formula_gap = get_spacing_values(spacing, font_size)["formula_gap"] @@ -238,7 +243,7 @@ def build_latex_for_formulas(selected_formulas, columns=2, font_size="10pt", mar return header + footer body_lines = [] - body_lines.extend(build_layout_comment_block(columns, font_size, margins, spacing)) + body_lines.extend(build_layout_comment_block(columns, font_size, margins, spacing, orientation)) current_class = None current_category = None in_flushleft = False @@ -334,4 +339,4 @@ def compile_latex_to_pdf(content): # Read and return the PDF bytes before the temporary directory is removed with open(pdf_file_path, "rb") as pdf_file: - return pdf_file.read() + return pdf_file.read() \ No newline at end of file diff --git a/backend/api/views.py b/backend/api/views.py index f92e0cd..0ba1e2a 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -23,6 +23,7 @@ VALID_FONT_SIZES = {"8pt", "9pt", "10pt", "11pt", "12pt"} VALID_SPACING = {"tiny", "small", "medium", "large"} VALID_MARGINS = {"0.15in", "0.25in", "0.5in", "0.75in", "1in", "1.5in", "2in"} +VALID_ORIENTATION = {"portrait", "landscape"} def is_valid_custom_pt(value, min_value, max_value): @@ -43,7 +44,7 @@ def is_truthy(value): return value.strip().lower() in {"1", "true", "yes", "on"} return bool(value) -def validate_layout_params(columns, font_size, margins, spacing): +def validate_layout_params(columns, font_size, margins, spacing, orientation="portrait"): try: columns = max(1, min(5, int(columns))) except (TypeError, ValueError): @@ -57,8 +58,11 @@ def validate_layout_params(columns, font_size, margins, spacing): if spacing not in VALID_SPACING and not is_valid_custom_pt(spacing, 0, 6): spacing = "large" + + if orientation not in VALID_ORIENTATION: + orientation = "portrait" - return columns, font_size, margins, spacing + return columns, font_size, margins, spacing, orientation # ------------------------------------------------------------------ # API endpoints @@ -93,21 +97,18 @@ def get_classes(request): def generate_sheet(request): """ POST /api/generate-sheet/ - Accepts { "formulas": [...], "columns": 2, "font_size": "10pt", "margins": "0.25in", "spacing": "large" } - Each formula: { "class": "ALGEBRA I", "category": "Linear Equations", "name": "Slope Formula" } - Or for special classes (like UNIT CIRCLE): { "class": "UNIT CIRCLE", "name": "Unit Circle (Key Angles)" } - Returns { "tex_code": "..." } """ selected = request.data.get("formulas", []) columns = request.data.get("columns", 2) font_size = request.data.get("font_size", "10pt") margins = request.data.get("margins", "0.25in") spacing = request.data.get("spacing", "large") + orientation = request.data.get("orientation", "portrait") - columns, font_size, margins, spacing = validate_layout_params(columns, font_size, margins, spacing) + columns, font_size, margins, spacing, orientation = validate_layout_params(columns, font_size, margins, spacing, orientation) if not selected: - tex_code = build_latex_for_formulas([], columns, font_size, margins, spacing) + tex_code = build_latex_for_formulas([], columns, font_size, margins, spacing, orientation) return Response({"tex_code": tex_code}) formula_data = get_formula_data() @@ -154,7 +155,7 @@ def generate_sheet(request): if not selected_formulas: return Response({"error": "No valid formulas found"}, status=400) - tex_code = build_latex_for_formulas(selected_formulas, columns, font_size, margins, spacing) + tex_code = build_latex_for_formulas(selected_formulas, columns, font_size, margins, spacing, orientation) return Response({"tex_code": tex_code}) @@ -163,10 +164,6 @@ def generate_sheet(request): def compile_latex(request): """ POST /api/compile/ - Accepts either: - - { "content": "...full LaTeX code..." } - - { "cheat_sheet_id": 123 } - Compiles with Tectonic on the backend and returns the PDF. """ content = request.data.get("content", "") cheat_sheet_id = request.data.get("cheat_sheet_id") @@ -175,9 +172,10 @@ def compile_latex(request): font_size = request.data.get("font_size", "10pt") margins = request.data.get("margins", "0.25in") spacing = request.data.get("spacing", "large") - columns, font_size, margins, spacing = validate_layout_params(columns, font_size, margins, spacing) + orientation = request.data.get("orientation", "portrait") # <-- Extract orientation + + columns, font_size, margins, spacing, orientation = validate_layout_params(columns, font_size, margins, spacing, orientation) - # If cheat_sheet_id is provided, get content from the cheat sheet if cheat_sheet_id: cheatsheet = get_object_or_404(CheatSheet, pk=cheat_sheet_id, user=request.user) content = cheatsheet.build_full_latex() @@ -185,7 +183,7 @@ def compile_latex(request): if not content: return Response({"error": "No LaTeX content provided"}, status=400) - content = normalize_latex_layout(content, columns, font_size, margins, spacing) + content = normalize_latex_layout(content, columns, font_size, margins, spacing, orientation) if normalize_only: return Response({ @@ -195,6 +193,7 @@ def compile_latex(request): "font_size": font_size, "margins": margins, "spacing": spacing, + "orientation": orientation, }, }) @@ -233,10 +232,6 @@ def compile_latex(request): # ------------------------------------------------------------------ class TemplateViewSet(viewsets.ModelViewSet): - """ - CRUD API for Templates - Get/Post/Put/Delete /api/templates/ - """ queryset = Template.objects.all() serializer_class = TemplateSerializer @@ -249,10 +244,6 @@ def get_queryset(self): class CheatSheetViewSet(viewsets.ModelViewSet): - """ - CRUD API for CheatSheets - Get/Post/Put/Delete /api/cheatsheets/ - """ queryset = CheatSheet.objects.all() serializer_class = CheatSheetSerializer permission_classes = [IsAuthenticated] @@ -265,10 +256,6 @@ def perform_create(self, serializer): @action(detail=False, methods=['post'], url_path='from-template') def from_template(self, request): - """ - POST /api/cheatsheets/from-template/ - Create cheat sheet from template - """ template_id = request.data.get("template_id") title = request.data.get("title", "Untitled") @@ -291,10 +278,6 @@ def from_template(self, request): class PracticeProblemViewSet(viewsets.ModelViewSet): - """ - CRUD API for Practice Problems - Get/Post/Put/Delete /api/problems/ - """ queryset = PracticeProblem.objects.all() serializer_class = PracticeProblemSerializer @@ -303,4 +286,4 @@ def get_queryset(self): cheat_sheet_id = self.request.query_params.get('cheat_sheet') if cheat_sheet_id: queryset = queryset.filter(cheat_sheet=cheat_sheet_id) - return queryset + return queryset \ No newline at end of file diff --git a/frontend/src/components/CreateCheatSheet.jsx b/frontend/src/components/CreateCheatSheet.jsx index a7a8b1f..c60f431 100644 --- a/frontend/src/components/CreateCheatSheet.jsx +++ b/frontend/src/components/CreateCheatSheet.jsx @@ -522,7 +522,7 @@ const ActionToolbar = ({ handleDownloadTex, handleDownloadPDF, isLoading, isSavi const FONT_SIZE_PRESETS = ['8pt', '9pt', '10pt', '11pt', '12pt']; const SPACING_PRESETS = ['tiny', 'small', 'medium', 'large']; -const LayoutOptions = ({ columns, setColumns, fontSize, setFontSize, spacing, setSpacing, margins, setMargins }) => { +const LayoutOptions = ({ columns, setColumns, fontSize, setFontSize, spacing, setSpacing, margins, setMargins, orientation, setOrientation }) => { const fontSizeMode = FONT_SIZE_PRESETS.includes(fontSize) ? fontSize : 'custom'; const spacingMode = SPACING_PRESETS.includes(spacing) ? spacing : 'custom'; @@ -610,6 +610,20 @@ const LayoutOptions = ({ columns, setColumns, fontSize, setFontSize, spacing, se + {/* NEW ORIENTATION CONTROL */} +
+ + +
+ {/* END NEW CONTROL */} ); @@ -648,6 +662,8 @@ const CreateCheatSheet = ({ onSave, onReset, initialData, isSaving = false }) => setSpacing, margins, setMargins, + orientation, + setOrientation, pdfBlob, isGenerating, isCompiling, @@ -766,6 +782,8 @@ const CreateCheatSheet = ({ onSave, onReset, initialData, isSaving = false }) => setSpacing={setSpacing} margins={margins} setMargins={setMargins} + orientation={orientation} + setOrientation={setOrientation} /> From 664489c10af8fb3a8b1c1607efce2c87c360773d Mon Sep 17 00:00:00 2001 From: Joey Lu Date: Tue, 5 May 2026 18:16:36 -0700 Subject: [PATCH 2/8] feat: add landscape orientation support and fix layout regex --- backend/api/latex_utils.py | 41 +++++++++++-------------------- backend/api/views.py | 40 ++++++------------------------ frontend/src/hooks/latex.js | 41 ++++++++++++++++++++++--------- frontend/src/hooks/latex.test.jsx | 7 ++++-- 4 files changed, 57 insertions(+), 72 deletions(-) diff --git a/backend/api/latex_utils.py b/backend/api/latex_utils.py index 78cbaa7..e186b59 100644 --- a/backend/api/latex_utils.py +++ b/backend/api/latex_utils.py @@ -46,7 +46,7 @@ LEGACY_ANSWER_LABEL_PATTERN = re.compile(r"\\textbf\{Answer:\}\s*") APP_LAYOUT_COMMENT_LINE_PATTERN = re.compile(r"(?m)^% @cheatsheet-layout .*\n?") APP_LAYOUT_COMMENT_BLOCK_PATTERN = re.compile( - r"(?m)(?:^% @cheatsheet-layout .*\n){5}^%\n?" + r"(?m)(?:^% @cheatsheet-layout .*\n)+^%\n?" ) @@ -129,11 +129,7 @@ def append_text_heading(lines, text): lines.append(r"\noindent " + text + r"\par") -<<<<<<< HEAD -def build_layout_comment_block(columns=2, font_size="10pt", margins="0.25in", spacing="large", orientation="portrait"): -======= -def build_layout_comment_block(columns=4, font_size="9pt", margins="0.15in", spacing="small"): ->>>>>>> af1ff138475768f9f924bbb5507570998035711a +def build_layout_comment_block(columns=4, font_size="9pt", margins="0.15in", spacing="small", orientation="portrait"): return [ f"% @cheatsheet-layout columns: {columns} | change layout options up top to update columns", f"% @cheatsheet-layout font_size: {font_size} | change layout options up top to update text size", @@ -144,11 +140,7 @@ def build_layout_comment_block(columns=4, font_size="9pt", margins="0.15in", spa ] -<<<<<<< HEAD -def build_dynamic_header(columns=2, font_size="10pt", margins="0.25in", spacing="large", orientation="portrait"): -======= -def build_dynamic_header(columns=4, font_size="9pt", margins="0.15in", spacing="small"): ->>>>>>> af1ff138475768f9f924bbb5507570998035711a +def build_dynamic_header(columns=4, font_size="9pt", margins="0.15in", spacing="small", orientation="portrait"): """ Build a dynamic LaTeX header based on user-selected options. """ @@ -156,18 +148,23 @@ def build_dynamic_header(columns=4, font_size="9pt", margins="0.15in", spacing=" spacing_values = get_spacing_values(spacing, font_size) doc_class, doc_class_size = get_document_class(font_size) - # Inject landscape orientation if selected - geometry_options = f"margin={margins}" + # 1. Force the PDF driver to rotate by passing landscape and letterpaper to the document class + doc_options = f"{doc_class_size},fleqn,letterpaper" if orientation == "landscape": - geometry_options += ", landscape" + doc_options += ",landscape" + + # 2. Also pass them to the geometry package + geometry_options = f"letterpaper,margin={margins}" + if orientation == "landscape": + geometry_options += ",landscape" header_lines = [ - f"\\documentclass[{doc_class_size},fleqn]{{{doc_class}}}", + f"\\documentclass[{doc_options}]{{{doc_class}}}", f"\\usepackage[{geometry_options}]{{geometry}}", "\\usepackage{amsmath, amssymb}", "\\usepackage{enumitem}", "\\usepackage{multicol}", - "\\usepackage{adjustbox}", # For auto-scaling equations to fit column width + "\\usepackage{adjustbox}", "", "\\setlength{\\mathindent}{0pt}", "\\setlist[itemize]{noitemsep, topsep=0pt, leftmargin=*}", @@ -201,11 +198,7 @@ def build_dynamic_footer(columns=2): return "\n".join(footer_lines) -<<<<<<< HEAD -def normalize_latex_layout(content, columns=2, font_size="10pt", margins="0.25in", spacing="large", orientation="portrait"): -======= -def normalize_latex_layout(content, columns=4, font_size="9pt", margins="0.15in", spacing="small"): ->>>>>>> af1ff138475768f9f924bbb5507570998035711a +def normalize_latex_layout(content, columns=4, font_size="9pt", margins="0.15in", spacing="small", orientation="portrait"): """Rebuild document wrappers so current layout controls apply to existing LaTeX content.""" if not content: return content @@ -242,11 +235,7 @@ def normalize_latex_layout(content, columns=4, font_size="9pt", margins="0.15in" return header + body + ("\n" if body else "") + footer -<<<<<<< HEAD -def build_latex_for_formulas(selected_formulas, columns=2, font_size="10pt", margins="0.25in", spacing="large", orientation="portrait"): -======= -def build_latex_for_formulas(selected_formulas, columns=4, font_size="9pt", margins="0.15in", spacing="small"): ->>>>>>> af1ff138475768f9f924bbb5507570998035711a +def build_latex_for_formulas(selected_formulas, columns=4, font_size="9pt", margins="0.15in", spacing="small", orientation="portrait"): """ Given a list of selected formulas (each with class_name, category, name, latex), build a complete LaTeX document. diff --git a/backend/api/views.py b/backend/api/views.py index f987a75..4be731a 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -37,15 +37,11 @@ VALID_FONT_SIZES = {"8pt", "9pt", "10pt", "11pt", "12pt"} VALID_SPACING = {"tiny", "small", "medium", "large"} VALID_MARGINS = {"0.15in", "0.25in", "0.5in", "0.75in", "1in", "1.5in", "2in"} -<<<<<<< HEAD VALID_ORIENTATION = {"portrait", "landscape"} -======= DEFAULT_COLUMNS = 4 DEFAULT_FONT_SIZE = "9pt" DEFAULT_SPACING = "small" DEFAULT_MARGINS = "0.15in" ->>>>>>> af1ff138475768f9f924bbb5507570998035711a - def is_valid_custom_pt(value, min_value, max_value): normalized = str(value or "").strip() @@ -78,14 +74,10 @@ def validate_layout_params(columns, font_size, margins, spacing, orientation="po margins = DEFAULT_MARGINS if spacing not in VALID_SPACING and not is_valid_custom_pt(spacing, 0, 6): -<<<<<<< HEAD - spacing = "large" - + spacing = DEFAULT_SPACING + if orientation not in VALID_ORIENTATION: orientation = "portrait" -======= - spacing = DEFAULT_SPACING ->>>>>>> af1ff138475768f9f924bbb5507570998035711a return columns, font_size, margins, spacing, orientation @@ -291,17 +283,7 @@ def get_classes(request): @api_view(["POST"]) def generate_sheet(request): """ - POST /api/generate-sheet/ -<<<<<<< HEAD - """ - selected = request.data.get("formulas", []) - columns = request.data.get("columns", 2) - font_size = request.data.get("font_size", "10pt") - margins = request.data.get("margins", "0.25in") - spacing = request.data.get("spacing", "large") - orientation = request.data.get("orientation", "portrait") -======= - Accepts { "formulas": [...], "columns": 4, "font_size": "9pt", "margins": "0.15in", "spacing": "small" } + Accepts { "formulas": [...], "columns": 4, "font_size": "9pt", "margins": "0.15in", "spacing": "small", "orientation": "portrait" } Each formula: { "class": "ALGEBRA I", "category": "Linear Equations", "name": "Slope Formula" } Or for special classes (like UNIT CIRCLE): { "class": "UNIT CIRCLE", "name": "Unit Circle (Key Angles)" } Returns { "tex_code": "..." } @@ -311,7 +293,7 @@ def generate_sheet(request): font_size = request.data.get("font_size", DEFAULT_FONT_SIZE) margins = request.data.get("margins", DEFAULT_MARGINS) spacing = request.data.get("spacing", DEFAULT_SPACING) ->>>>>>> af1ff138475768f9f924bbb5507570998035711a + orientation = request.data.get("orientation", "portrait") columns, font_size, margins, spacing, orientation = validate_layout_params(columns, font_size, margins, spacing, orientation) @@ -376,21 +358,13 @@ def compile_latex(request): content = request.data.get("content", "") cheat_sheet_id = request.data.get("cheat_sheet_id") normalize_only = is_truthy(request.data.get("normalize_only")) -<<<<<<< HEAD - columns = request.data.get("columns", 2) - font_size = request.data.get("font_size", "10pt") - margins = request.data.get("margins", "0.25in") - spacing = request.data.get("spacing", "large") - orientation = request.data.get("orientation", "portrait") # <-- Extract orientation - - columns, font_size, margins, spacing, orientation = validate_layout_params(columns, font_size, margins, spacing, orientation) -======= columns = request.data.get("columns", DEFAULT_COLUMNS) font_size = request.data.get("font_size", DEFAULT_FONT_SIZE) margins = request.data.get("margins", DEFAULT_MARGINS) spacing = request.data.get("spacing", DEFAULT_SPACING) - columns, font_size, margins, spacing = validate_layout_params(columns, font_size, margins, spacing) ->>>>>>> af1ff138475768f9f924bbb5507570998035711a + orientation = request.data.get("orientation", "portrait") + + columns, font_size, margins, spacing, orientation = validate_layout_params(columns, font_size, margins, spacing, orientation) if cheat_sheet_id: cheatsheet = get_object_or_404(CheatSheet, pk=cheat_sheet_id, user=request.user) diff --git a/frontend/src/hooks/latex.js b/frontend/src/hooks/latex.js index f5df0ea..9b1f13e 100644 --- a/frontend/src/hooks/latex.js +++ b/frontend/src/hooks/latex.js @@ -10,6 +10,7 @@ const DEFAULT_LAYOUT = { fontSize: '9pt', spacing: 'small', margins: '0.15in', + orientation: 'portrait', }; function getInitialContentSource(data) { @@ -84,6 +85,7 @@ export function useLatex(initialData) { const [fontSize, setFontSize] = useState(initialData?.fontSize ?? DEFAULT_LAYOUT.fontSize); const [spacing, setSpacing] = useState(initialData?.spacing ?? DEFAULT_LAYOUT.spacing); const [margins, setMargins] = useState(initialData?.margins ?? DEFAULT_LAYOUT.margins); + const [orientation, setOrientation] = useState(initialData?.orientation ?? DEFAULT_LAYOUT.orientation); const [pdfBlob, setPdfBlob] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isCompiling, setIsCompiling] = useState(false); @@ -103,6 +105,7 @@ export function useLatex(initialData) { fontSize: initialData?.fontSize ?? DEFAULT_LAYOUT.fontSize, spacing: initialData?.spacing ?? DEFAULT_LAYOUT.spacing, margins: initialData?.margins ?? DEFAULT_LAYOUT.margins, + orientation: initialData?.orientation ?? DEFAULT_LAYOUT.orientation, }); // Revoke the object URL when the component unmounts to prevent memory leaks @@ -172,11 +175,13 @@ export function useLatex(initialData) { setFontSize(saved.fontSize ?? DEFAULT_LAYOUT.fontSize); setSpacing(saved.spacing ?? DEFAULT_LAYOUT.spacing); setMargins(saved.margins ?? DEFAULT_LAYOUT.margins); + setOrientation(saved.orientation ?? DEFAULT_LAYOUT.orientation); lastCompiledLayoutRef.current = { columns: saved.columns ?? DEFAULT_LAYOUT.columns, fontSize: saved.fontSize ?? DEFAULT_LAYOUT.fontSize, spacing: saved.spacing ?? DEFAULT_LAYOUT.spacing, margins: saved.margins ?? DEFAULT_LAYOUT.margins, + orientation: saved.orientation ?? DEFAULT_LAYOUT.orientation, }; } else if (initialData) { initialLoaded.current = true; @@ -187,11 +192,13 @@ export function useLatex(initialData) { setFontSize(initialData.fontSize ?? DEFAULT_LAYOUT.fontSize); setSpacing(initialData.spacing ?? DEFAULT_LAYOUT.spacing); setMargins(initialData.margins ?? DEFAULT_LAYOUT.margins); + setOrientation(initialData.orientation ?? DEFAULT_LAYOUT.orientation); lastCompiledLayoutRef.current = { columns: initialData.columns ?? DEFAULT_LAYOUT.columns, fontSize: initialData.fontSize ?? DEFAULT_LAYOUT.fontSize, spacing: initialData.spacing ?? DEFAULT_LAYOUT.spacing, margins: initialData.margins ?? DEFAULT_LAYOUT.margins, + orientation: initialData.orientation ?? DEFAULT_LAYOUT.orientation, }; } }, [initialData]); @@ -208,12 +215,12 @@ export function useLatex(initialData) { useEffect(() => { if (saveTimerRef.current) clearTimeout(saveTimerRef.current); saveTimerRef.current = setTimeout(() => { - saveLatexStorage({ title, content, contentSource, columns, fontSize, spacing, margins }); + saveLatexStorage({ title, content, contentSource, columns, fontSize, spacing, margins, orientation }); }, SAVE_DEBOUNCE_MS); return () => { if (saveTimerRef.current) clearTimeout(saveTimerRef.current); }; - }, [title, content, contentSource, columns, fontSize, spacing, margins]); + }, [title, content, contentSource, columns, fontSize, spacing, margins, orientation]); const compileLatexContent = useCallback(async (latexContent, layoutOptions = {}) => { const response = await fetch('/api/compile/', { @@ -248,6 +255,7 @@ export function useLatex(initialData) { font_size: fontSize, spacing, margins, + orientation, }), }); @@ -258,7 +266,7 @@ export function useLatex(initialData) { const data = await response.json(); return data.tex_code; - }, [columns, fontSize, spacing, margins]); + }, [columns, fontSize, spacing, margins, orientation]); const normalizeLatexContent = useCallback(async (latexContent) => { const response = await fetch('/api/compile/', { @@ -273,6 +281,7 @@ export function useLatex(initialData) { font_size: fontSize, spacing, margins, + orientation, normalize_only: true, }), }); @@ -284,13 +293,15 @@ export function useLatex(initialData) { const data = await response.json(); return data.tex_code || latexContent; - }, [authTokens, columns, fontSize, margins, spacing]); + }, [authTokens, columns, fontSize, margins, spacing, orientation]); const hasLayoutChanges = lastCompiledLayoutRef.current.columns !== columns || lastCompiledLayoutRef.current.fontSize !== fontSize || lastCompiledLayoutRef.current.spacing !== spacing || - lastCompiledLayoutRef.current.margins !== margins; + lastCompiledLayoutRef.current.margins !== margins || + lastCompiledLayoutRef.current.orientation !== orientation; + const canRegenerateFromSelections = !content.trim() || contentSource === 'generated'; const handleCompileOnly = useCallback(async (selectedList = []) => { @@ -328,8 +339,9 @@ export function useLatex(initialData) { font_size: fontSize, spacing, margins, + orientation, }); - lastCompiledLayoutRef.current = { columns, fontSize, spacing, margins }; + lastCompiledLayoutRef.current = { columns, fontSize, spacing, margins, orientation }; setContentModified(false); } catch (error) { setCompileError(error.message); @@ -337,7 +349,7 @@ export function useLatex(initialData) { setIsCompiling(false); isCompilingRef.current = false; } - }, [clearAutoCompileTimer, columns, compileLatexContent, content, fontSize, generateLatexContent, hasLayoutChanges, margins, normalizeLatexContent, saveToHistory, spacing]); + }, [clearAutoCompileTimer, columns, compileLatexContent, content, fontSize, generateLatexContent, hasLayoutChanges, margins, normalizeLatexContent, saveToHistory, spacing, orientation]); useEffect(() => { if (!initialLoaded.current) return; @@ -372,7 +384,8 @@ export function useLatex(initialData) { columns: regenerateOptions.columns, font_size: regenerateOptions.fontSize, spacing: regenerateOptions.spacing, - margins: margins + margins: margins, + orientation: orientation }), }); if (response.ok) { @@ -401,8 +414,9 @@ export function useLatex(initialData) { font_size: fontSize, spacing, margins, + orientation, }); - lastCompiledLayoutRef.current = { columns, fontSize, spacing, margins }; + lastCompiledLayoutRef.current = { columns, fontSize, spacing, margins, orientation }; setContentModified(false); } catch (error) { setCompileError(error.message); @@ -410,7 +424,7 @@ export function useLatex(initialData) { setIsCompiling(false); isCompilingRef.current = false; } - }, [clearAutoCompileTimer, columns, compileLatexContent, content, fontSize, hasLayoutChanges, margins, normalizeLatexContent, saveToHistory, spacing]); + }, [clearAutoCompileTimer, columns, compileLatexContent, content, fontSize, hasLayoutChanges, margins, normalizeLatexContent, saveToHistory, spacing, orientation]); useEffect(() => { if (!initialLoaded.current || hasRestoredPreviewRef.current) return; @@ -472,6 +486,7 @@ export function useLatex(initialData) { font_size: fontSize, spacing, margins, + orientation, }), }); if (!response.ok) { @@ -560,6 +575,7 @@ export function useLatex(initialData) { setFontSize(initialData?.fontSize ?? DEFAULT_LAYOUT.fontSize); setSpacing(initialData?.spacing ?? DEFAULT_LAYOUT.spacing); setMargins(initialData?.margins ?? DEFAULT_LAYOUT.margins); + setOrientation(initialData?.orientation ?? DEFAULT_LAYOUT.orientation); setHistory([]); setHistoryIndex(-1); lastCompiledLayoutRef.current = { @@ -567,6 +583,7 @@ export function useLatex(initialData) { fontSize: initialData?.fontSize ?? DEFAULT_LAYOUT.fontSize, spacing: initialData?.spacing ?? DEFAULT_LAYOUT.spacing, margins: initialData?.margins ?? DEFAULT_LAYOUT.margins, + orientation: initialData?.orientation ?? DEFAULT_LAYOUT.orientation, }; if (pdfBlobUrlRef.current) { URL.revokeObjectURL(pdfBlobUrlRef.current); @@ -595,6 +612,8 @@ export function useLatex(initialData) { setSpacing, margins, setMargins, + orientation, + setOrientation, pdfBlob, isGenerating, isCompiling, @@ -612,4 +631,4 @@ export function useLatex(initialData) { handlePrintPDF, clearLatex }; -} +} \ No newline at end of file diff --git a/frontend/src/hooks/latex.test.jsx b/frontend/src/hooks/latex.test.jsx index bc850fd..522884a 100644 --- a/frontend/src/hooks/latex.test.jsx +++ b/frontend/src/hooks/latex.test.jsx @@ -45,6 +45,7 @@ describe('useLatex hook', () => { expect(result.current.fontSize).toBe('9pt'); expect(result.current.spacing).toBe('small'); expect(result.current.margins).toBe('0.15in'); + expect(result.current.orientation).toBe('portrait'); // <-- Added orientation default expect(result.current.pdfBlob).toBeNull(); expect(result.current.compileError).toBeNull(); }); @@ -56,7 +57,8 @@ describe('useLatex hook', () => { columns: 3, fontSize: '12pt', spacing: 'medium', - margins: '0.5in' + margins: '0.5in', + orientation: 'landscape' // <-- Added orientation custom data }; const { result } = renderHook(() => useLatex(initialData), { wrapper }); @@ -67,6 +69,7 @@ describe('useLatex hook', () => { expect(result.current.fontSize).toBe('12pt'); expect(result.current.spacing).toBe('medium'); expect(result.current.margins).toBe('0.5in'); + expect(result.current.orientation).toBe('landscape'); // <-- Added orientation assertion }); test('treats persisted generated sheets as safe to regenerate', () => { @@ -242,4 +245,4 @@ describe('useLatex hook', () => { expect(mockClick).toHaveBeenCalled(); expect(mockElement.download).toBe('FileTitle.tex'); }); -}); +}); \ No newline at end of file From de70d22135ba218b0e0a7da32e412c65f68810eb Mon Sep 17 00:00:00 2001 From: Joey Lu Date: Tue, 5 May 2026 18:39:42 -0700 Subject: [PATCH 3/8] test: update backend tests to match new orientation and letterpaper layout --- backend/api/tests.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/backend/api/tests.py b/backend/api/tests.py index 0599af9..d98ff6b 100644 --- a/backend/api/tests.py +++ b/backend/api/tests.py @@ -36,7 +36,7 @@ def sample_template(db): subject="algebra", description="A test template", latex_content="\\section*{Test}\nHello World", - default_margins="0.5in", + default_margins="0.5in",assert "\\documentclass[8pt,fleqn]{extarticle}" in normalized default_columns=2, ) @@ -239,7 +239,7 @@ def test_normalize_latex_layout_rewraps_existing_document_with_current_settings( normalized = normalize_latex_layout(raw, columns=4, font_size="8pt", margins="0.5in", spacing="tiny") - assert "\\documentclass[8pt,fleqn]{extarticle}" in normalized + assert "\\documentclass[8pt,fleqn,letterpaper]{extarticle}" in normalized assert "margin=0.5in" in normalized assert "\\begin{multicols}{4}" in normalized assert "\\begin{multicols}{2}" not in normalized @@ -383,7 +383,7 @@ def test_build_dynamic_header_keeps_headers_close_to_body_size(self): def test_build_dynamic_header_accepts_custom_font_and_spacing(self): header = build_dynamic_header(columns=5, font_size="10.5pt", margins="0.25in", spacing="0.6pt") - assert "\\documentclass[10pt,fleqn]{article}" in header + assert "\\documentclass[10pt,fleqn,letterpaper]{article}" in header assert "\\fontsize{10.5pt}{11.3pt}\\selectfont" in header assert "\\setlength{\\baselineskip}{11.1pt}" in header assert "\\setlength{\\parskip}{0.6pt}" in header @@ -402,6 +402,7 @@ def test_build_latex_for_formulas_includes_editable_layout_comments(self): assert "% @cheatsheet-layout font_size: 10.5pt | change layout options up top to update text size" in tex assert "% @cheatsheet-layout spacing: 0.6pt | change layout options up top to update spacing" in tex assert "% @cheatsheet-layout margins: 0.5in | change layout options up top to update margins" in tex + assert "% @cheatsheet-layout orientation: portrait | change layout options up top to update orientation" in tex # ── API Tests ──────────────────────────────────────────────────────── @@ -1375,6 +1376,7 @@ def test_compile_normalize_only_refreshes_layout_comments_from_request_values(se "font_size": "8pt", "spacing": "tiny", "margins": "0.25in", + "orientation": "portrait", }, format="json", ) @@ -1387,6 +1389,7 @@ def test_compile_normalize_only_refreshes_layout_comments_from_request_values(se "font_size": "8pt", "spacing": "tiny", "margins": "0.25in", + "orientation": "portrait", } assert "\\begin{multicols}{2}" in tex assert "\\fontsize{8pt}{8.8pt}\\selectfont" in tex @@ -1395,8 +1398,9 @@ def test_compile_normalize_only_refreshes_layout_comments_from_request_values(se assert "% @cheatsheet-layout font_size: 8pt | change layout options up top to update text size" in tex assert "% @cheatsheet-layout spacing: tiny | change layout options up top to update spacing" in tex assert "% @cheatsheet-layout margins: 0.25in | change layout options up top to update margins" in tex + assert "orientation: portrait" in tex - +def test_normalize_latex_layout_rewraps_existing_document_with_current_settings(self): # ── Auth Endpoint Tests ────────────────────────────────────────────── From 7a7af9e9dd59d1543e30ce65e80a3ff177c244ca Mon Sep 17 00:00:00 2001 From: Joey Lu Date: Tue, 5 May 2026 19:01:32 -0700 Subject: [PATCH 4/8] feat: add orientation field to CheatSheet model --- .../migrations/0008_cheatsheet_orientation.py | 18 ++++++++++++++++++ backend/api/models.py | 1 + 2 files changed, 19 insertions(+) create mode 100644 backend/api/migrations/0008_cheatsheet_orientation.py diff --git a/backend/api/migrations/0008_cheatsheet_orientation.py b/backend/api/migrations/0008_cheatsheet_orientation.py new file mode 100644 index 0000000..0f392d9 --- /dev/null +++ b/backend/api/migrations/0008_cheatsheet_orientation.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.5 on 2026-05-06 02:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0007_cheatsheet_content_source'), + ] + + operations = [ + migrations.AddField( + model_name='cheatsheet', + name='orientation', + field=models.CharField(default='portrait', max_length=20), + ), + ] diff --git a/backend/api/models.py b/backend/api/models.py index a16dea9..9d7e45d 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -39,6 +39,7 @@ class CheatSheet(models.Model): margins = models.CharField(max_length=20, default="0.15in") font_size = models.CharField(max_length=10, default="9pt") spacing = models.CharField(max_length=10, default="small") + orientation = models.CharField(max_length=20, default="portrait") # Stores selected formulas with user-defined order: [{"class": "...", "category": "...", "name": "..."}] selected_formulas = models.JSONField(default=list, blank=True) created_at = models.DateTimeField(auto_now_add=True) From 5948d568f18909aa36b0ebbfdcec9d50e9b6f25a Mon Sep 17 00:00:00 2001 From: Joey Lu Date: Tue, 5 May 2026 19:06:38 -0700 Subject: [PATCH 5/8] fix: resolve syntax errors in backend tests --- backend/api/tests.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/api/tests.py b/backend/api/tests.py index d98ff6b..2812276 100644 --- a/backend/api/tests.py +++ b/backend/api/tests.py @@ -36,7 +36,7 @@ def sample_template(db): subject="algebra", description="A test template", latex_content="\\section*{Test}\nHello World", - default_margins="0.5in",assert "\\documentclass[8pt,fleqn]{extarticle}" in normalized + default_margins="0.5in", default_columns=2, ) @@ -91,6 +91,7 @@ def test_build_full_latex_wraps_content(self): columns=1, font_size="10pt", user=self.user, + orientation="portrait", ) full = sheet.build_full_latex() assert "\\begin{document}" in full @@ -237,7 +238,7 @@ def test_normalize_latex_layout_rewraps_existing_document_with_current_settings( "\\end{document}" ) - normalized = normalize_latex_layout(raw, columns=4, font_size="8pt", margins="0.5in", spacing="tiny") + normalized = normalize_latex_layout(raw, columns=4, font_size="8pt", margins="0.5in", spacing="tiny", orientation="portrait") assert "\\documentclass[8pt,fleqn,letterpaper]{extarticle}" in normalized assert "margin=0.5in" in normalized @@ -1400,7 +1401,7 @@ def test_compile_normalize_only_refreshes_layout_comments_from_request_values(se assert "% @cheatsheet-layout margins: 0.25in | change layout options up top to update margins" in tex assert "orientation: portrait" in tex -def test_normalize_latex_layout_rewraps_existing_document_with_current_settings(self): + # ── Auth Endpoint Tests ────────────────────────────────────────────── From 096616a020c093f703d926fc8f899a9e7bd32f76 Mon Sep 17 00:00:00 2001 From: Joey Lu Date: Tue, 5 May 2026 19:12:39 -0700 Subject: [PATCH 6/8] test: update remaining generate sheet tests for letterpaper --- backend/api/tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/api/tests.py b/backend/api/tests.py index 2812276..3ab4cc0 100644 --- a/backend/api/tests.py +++ b/backend/api/tests.py @@ -1248,7 +1248,7 @@ def test_generate_sheet_8pt_uses_extarticle(self, auth_client): ) assert resp.status_code == 200 tex = resp.json()["tex_code"] - assert "\\documentclass[8pt,fleqn]{extarticle}" in tex + assert "\\documentclass[8pt,fleqn,letterpaper]{extarticle}" in tex assert "\\documentclass[8pt,fleqn]{article}" not in tex def test_generate_sheet_9pt_uses_extarticle(self, auth_client): @@ -1263,7 +1263,7 @@ def test_generate_sheet_9pt_uses_extarticle(self, auth_client): ) assert resp.status_code == 200 tex = resp.json()["tex_code"] - assert "\\documentclass[9pt,fleqn]{extarticle}" in tex + assert "\\documentclass[9pt,fleqn,letterpaper]{extarticle}" in tex assert "\\documentclass[9pt,fleqn]{article}" not in tex def test_generate_sheet_10pt_uses_article(self, auth_client): @@ -1278,7 +1278,7 @@ def test_generate_sheet_10pt_uses_article(self, auth_client): ) assert resp.status_code == 200 tex = resp.json()["tex_code"] - assert "\\documentclass[10pt,fleqn]{article}" in tex + assert "\\documentclass[10pt,fleqn,letterpaper]{article}" in tex assert "extarticle" not in tex def test_generate_sheet_latex_injection_blocked(self, auth_client): From 8b349dd14867aa520562437a41b860626aeb97de Mon Sep 17 00:00:00 2001 From: Joey Lu Date: Tue, 5 May 2026 22:42:01 -0700 Subject: [PATCH 7/8] feat: add linear algebra I and II formulas and included videos --- backend/api/formula_data/__init__.py | 8 ++++++++ backend/api/formula_data/linear_algebra_i.py | 20 +++++++++++++++++++ backend/api/formula_data/linear_algebra_ii.py | 19 ++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 backend/api/formula_data/linear_algebra_i.py create mode 100644 backend/api/formula_data/linear_algebra_ii.py diff --git a/backend/api/formula_data/__init__.py b/backend/api/formula_data/__init__.py index c008519..5fc8406 100644 --- a/backend/api/formula_data/__init__.py +++ b/backend/api/formula_data/__init__.py @@ -17,6 +17,8 @@ from .physics_ii import FORMULAS as PHYSICS_II, CLASS_NAME as PHYSICS_II_NAME from .statistics_i import FORMULAS as STATISTICS_I, CLASS_NAME as STATISTICS_I_NAME from .statistics_ii import FORMULAS as STATISTICS_II, CLASS_NAME as STATISTICS_II_NAME +from .linear_algebra_i import FORMULAS as LINEAR_ALGEBRA_I, CLASS_NAME as LINEAR_ALGEBRA_I_NAME +from .linear_algebra_ii import FORMULAS as LINEAR_ALGEBRA_II, CLASS_NAME as LINEAR_ALGEBRA_II_NAME AVAILABLE_CLASSES = [ PRE_ALGEBRA_NAME, @@ -33,6 +35,8 @@ PHYSICS_II_NAME, STATISTICS_I_NAME, STATISTICS_II_NAME, + LINEAR_ALGEBRA_I_NAME, + LINEAR_ALGEBRA_II_NAME, ] # Classes that have categories (normal structure) @@ -50,6 +54,8 @@ PHYSICS_II_NAME, STATISTICS_I_NAME, STATISTICS_II_NAME, + LINEAR_ALGEBRA_I_NAME, + LINEAR_ALGEBRA_II_NAME, ] # Special classes with no categories (single toggle) @@ -71,6 +77,8 @@ PHYSICS_II_NAME: PHYSICS_II, STATISTICS_I_NAME: STATISTICS_I, STATISTICS_II_NAME: STATISTICS_II, + LINEAR_ALGEBRA_I_NAME: LINEAR_ALGEBRA_I, + LINEAR_ALGEBRA_II_NAME: LINEAR_ALGEBRA_II, } diff --git a/backend/api/formula_data/linear_algebra_i.py b/backend/api/formula_data/linear_algebra_i.py new file mode 100644 index 0000000..9ae39be --- /dev/null +++ b/backend/api/formula_data/linear_algebra_i.py @@ -0,0 +1,20 @@ +""" LINEAR ALGEBRA I formulas """ +CLASS_NAME = "LINEAR ALGEBRA I" + +FORMULAS = { + "Vector Basics": [ + {"name": "Vector Addition", "latex": r"\mathbf{u} + \mathbf{v} = \langle u_1+v_1, u_2+v_2 \rangle"}, + {"name": "Scalar Multiplication", "latex": r"k\mathbf{u} = \langle ku_1, ku_2 \rangle"}, + {"name": "Magnitude (L2 Norm)", "latex": r"\|\mathbf{v}\| = \sqrt{v_1^2 + v_2^2 + \dots + v_n^2}"}, + {"name": "Dot Product", "latex": r"\mathbf{u} \cdot \mathbf{v} = u_1v_1 + u_2v_2 + \dots + u_nv_n"}, + ], + "Matrix Operations": [ + {"name": "Matrix Addition", "latex": r"A + B = [a_{ij} + b_{ij}]"}, + {"name": "Matrix-Vector Multiplication", "latex": r"A\mathbf{x} = \mathbf{b}"}, + {"name": "Transpose", "latex": r"(A^T)_{ij} = A_{ji}"}, + ], + "2x2 Systems": [ + {"name": "2x2 Determinant", "latex": r"\det(A) = ad - bc"}, + {"name": "2x2 Inverse", "latex": r"A^{-1} = \frac{1}{ad-bc} \begin{bmatrix} d & -b \\ -c & a \end{bmatrix}"}, + ] +} \ No newline at end of file diff --git a/backend/api/formula_data/linear_algebra_ii.py b/backend/api/formula_data/linear_algebra_ii.py new file mode 100644 index 0000000..add5433 --- /dev/null +++ b/backend/api/formula_data/linear_algebra_ii.py @@ -0,0 +1,19 @@ +""" LINEAR ALGEBRA II formulas """ +CLASS_NAME = "LINEAR ALGEBRA II" + +FORMULAS = { + "Matrix Properties": [ + {"name": "Invertibility Condition", "latex": r"A \text{ is invertible iff } \det(A) \neq 0"}, + {"name": "Matrix Multiplication Property", "latex": r"(AB)^{-1} = B^{-1}A^{-1}"}, + {"name": "Orthogonality", "latex": r"Q^T Q = I"}, + ], + "Eigenvalues & Eigenvectors": [ + {"name": "Characteristic Equation", "latex": r"\det(A - \lambda I) = 0"}, + {"name": "Eigenvector Definition", "latex": r"A\mathbf{v} = \lambda\mathbf{v}"}, + ], + "Decompositions & Spaces": [ + {"name": "Diagonalization", "latex": r"A = PDP^{-1}"}, + {"name": "Rank-Nullity Theorem", "latex": r"\text{rank}(A) + \text{nullity}(A) = n"}, + {"name": "Projection Formula", "latex": r"\text{proj}_{\mathbf{u}}(\mathbf{v}) = \frac{\mathbf{u} \cdot \mathbf{v}}{\|\mathbf{u}\|^2}\mathbf{u}"}, + ] +} \ No newline at end of file From 8a449ba45953764893dd9075d2d77c59f8b07657 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 18:36:34 +0000 Subject: [PATCH 8/8] fix: add orientation to onSave payloads, serializer, build_full_latex, and landscape test Agent-Logs-Url: https://github.com/ChicoState/cheat-sheet/sessions/feaa4cd4-4709-4685-85f7-54a23f49946e Co-authored-by: Davictory2003 <68972845+Davictory2003@users.noreply.github.com> --- backend/api/models.py | 13 +++++++++++-- backend/api/serializers.py | 1 + backend/api/tests.py | 10 ++++++++-- frontend/src/components/CreateCheatSheet.jsx | 5 ++++- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/backend/api/models.py b/backend/api/models.py index 9d7e45d..19ef163 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -101,12 +101,21 @@ def build_full_latex(self): # Build document header document_class, document_class_size = get_document_class(self.font_size) spacing_values = get_spacing_values(self.spacing, self.font_size) + + doc_options = f"{document_class_size},fleqn,letterpaper" + if self.orientation == "landscape": + doc_options += ",landscape" + + geometry_options = f"letterpaper,margin={self.margins}" + if self.orientation == "landscape": + geometry_options += ",landscape" + header = [ - f"\\documentclass[{document_class_size}]{{{document_class}}}", + f"\\documentclass[{doc_options}]{{{document_class}}}", "\\usepackage[utf8]{inputenc}", "\\usepackage{amsmath, amssymb}", "\\usepackage{adjustbox}", - f"\\usepackage[a4paper, margin={self.margins}]{{geometry}}", + f"\\usepackage[{geometry_options}]{{geometry}}", f"\\setlength{{\\baselineskip}}{{{spacing_values['baseline_skip']}}}", f"\\setlength{{\\parskip}}{{{spacing_values['paragraph_skip']}}}", ] diff --git a/backend/api/serializers.py b/backend/api/serializers.py index edd0acd..663440a 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -83,6 +83,7 @@ class Meta: "columns", "font_size", "spacing", + "orientation", "selected_formulas", "problems", "full_latex", diff --git a/backend/api/tests.py b/backend/api/tests.py index 3ab4cc0..a5f2532 100644 --- a/backend/api/tests.py +++ b/backend/api/tests.py @@ -193,7 +193,7 @@ def test_build_full_latex_8pt_uses_extarticle(self): full = sheet.build_full_latex() - assert "\\documentclass[8pt]{extarticle}" in full + assert "\\documentclass[8pt,fleqn,letterpaper]{extarticle}" in full def test_build_full_latex_custom_font_size_uses_supported_wrapper(self): sheet = CheatSheet.objects.create( @@ -205,7 +205,7 @@ def test_build_full_latex_custom_font_size_uses_supported_wrapper(self): full = sheet.build_full_latex() - assert "\\documentclass[10pt]{article}" in full + assert "\\documentclass[10pt,fleqn,letterpaper]{article}" in full assert "\\fontsize{10.5pt}{11.3pt}\\selectfont" in full def test_build_full_latex_includes_saved_spacing(self): @@ -390,6 +390,12 @@ def test_build_dynamic_header_accepts_custom_font_and_spacing(self): assert "\\setlength{\\parskip}{0.6pt}" in header assert "\\begin{multicols}{5}" in header + def test_build_dynamic_header_landscape_includes_landscape_options(self): + header = build_dynamic_header(columns=4, font_size="9pt", margins="0.15in", spacing="small", orientation="landscape") + assert "landscape" in header + assert "\\documentclass[9pt,fleqn,letterpaper,landscape]{extarticle}" in header + assert "letterpaper,margin=0.15in,landscape" in header + def test_build_latex_for_formulas_includes_editable_layout_comments(self): tex = build_latex_for_formulas( [{"class_name": "ALGEBRA I", "category": "Linear Equations", "name": "Slope Formula", "latex": "m=\\frac{y_2-y_1}{x_2-x_1}"}], diff --git a/frontend/src/components/CreateCheatSheet.jsx b/frontend/src/components/CreateCheatSheet.jsx index 9bd2850..7d8ea4e 100644 --- a/frontend/src/components/CreateCheatSheet.jsx +++ b/frontend/src/components/CreateCheatSheet.jsx @@ -1219,6 +1219,7 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS fontSize, spacing, margins, + orientation, selectedFormulas: getSelectedFormulasList(), compileSnapshot: { title, @@ -1228,13 +1229,14 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS fontSize, spacing, margins, + orientation, selectedFormulas: getSelectedFormulasList(), compiledAt: new Date().toISOString(), }, }, false).catch((error) => { console.error('Failed to autosave compiled sheet', error); }); - }, [columns, compileError, content, contentSource, fontSize, getSelectedFormulasList, margins, onSave, pdfBlob, spacing, title]); + }, [columns, compileError, content, contentSource, fontSize, getSelectedFormulasList, margins, onSave, orientation, pdfBlob, spacing, title]); const startResize = useCallback((panel) => (event) => { event.preventDefault(); @@ -1353,6 +1355,7 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS fontSize, spacing, margins, + orientation, selectedFormulas: getSelectedFormulasList(), }); };