Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ A full-stack web application for generating LaTeX-based cheat sheets. Users sele
| Frontend | React 18 + Vite |
| Backend | Django 6 + Django REST Framework |
| LaTeX Engine | Tectonic |
| Database | SQLite (dev) / MariaDB (prod) |
| Database | SQLite (dev) / PostgreSQL (Docker/prod) |
| Container | Docker Compose |

## Project Structure
Expand Down Expand Up @@ -141,7 +141,7 @@ The frontend will be available at `http://localhost:5173/`.
docker compose up --build
```

This builds and starts the Django backend, React frontend, and MariaDB database.
This builds and starts the Django backend, React frontend, and PostgreSQL database.

## Running Tests

Expand Down
61 changes: 47 additions & 14 deletions backend/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,44 @@ class CheatSheet(models.Model):

def __str__(self):
return self.title

def _build_practice_problems_section(self):
problems = list(self.problems.all())
if not problems:
return ""

section_lines = ["\\section*{Practice Problems}"]
for problem in problems:
section_lines.append(
f"\\textbf{{Problem {problem.order}:}} {problem.question_latex}"
)
if problem.answer_latex:
section_lines.append(f"\\textbf{{Answer:}} {problem.answer_latex}")
section_lines.append("")

return "\n".join(section_lines).rstrip()

def _inject_practice_problems_into_document(self, content):
practice_problems = self._build_practice_problems_section()
if not practice_problems:
return content

end_document = r"\end{document}"
end_multicols = r"\end{multicols}"

insert_before = end_document
if end_multicols in content and content.rfind(end_multicols) < content.rfind(end_document):
insert_before = end_multicols

insert_index = content.rfind(insert_before)
if insert_index == -1:
return content

return (
f"{content[:insert_index].rstrip()}\n\n"
f"{practice_problems}\n"
f"{content[insert_index:]}"
)

def build_full_latex(self):
"""
Expand All @@ -40,21 +78,22 @@ def build_full_latex(self):
"""
content = self.latex_content or ""

# If it's already a complete document, return as-is
if r"\begin{document}" in content:
return content
# If it's already a complete document, keep its layout and inject problems if needed
if r"\begin{document}" in content and r"\end{document}" in content:
return self._inject_practice_problems_into_document(content)

# Build document header
document_class = "extarticle" if self.font_size in {"8pt", "9pt"} else "article"
header = [
"\\documentclass{article}",
f"\\documentclass{{{document_class}}}",
"\\usepackage[utf8]{inputenc}",
"\\usepackage{amsmath, amssymb}",
f"\\usepackage[a4paper, margin={self.margins}]{{geometry}}",
]

# Add font size if specified
if self.font_size and self.font_size != "10pt":
header[0] = f"\\documentclass[{self.font_size}]{{article}}"
header[0] = f"\\documentclass[{self.font_size}]{{{document_class}}}"

# Add multicolumn support if needed
if self.columns > 1:
Expand All @@ -75,15 +114,9 @@ def build_full_latex(self):
# Add main content
document_parts.append(content)

# Add practice problems if they exist
problems = self.problems.all()
if problems:
document_parts.append("\\section*{Practice Problems}")
for problem in problems:
document_parts.append(f"\\textbf{{Problem {problem.order}:}} {problem.question_latex}")
if problem.answer_latex:
document_parts.append(f"\\textbf{{Answer:}} {problem.answer_latex}")
document_parts.append("") # Add spacing
practice_problems = self._build_practice_problems_section()
if practice_problems:
document_parts.append(practice_problems)

# Close multicolumn environment if needed
if self.columns > 1:
Expand Down
55 changes: 55 additions & 0 deletions backend/api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,50 @@ def test_build_full_latex_passthrough(self):
)
assert sheet.build_full_latex() == raw

def test_build_full_latex_passthrough_inserts_problems_before_document_end(self):
raw = "\\documentclass{article}\n\\begin{document}\nCustom\n\\end{document}"
sheet = CheatSheet.objects.create(
title="Raw With Problems",
latex_content=raw,
)
PracticeProblem.objects.create(
cheat_sheet=sheet,
question_latex="Show that $x^2 \\ge 0$.",
answer_latex="Because squares are nonnegative.",
order=1,
)

full = sheet.build_full_latex()

assert "Practice Problems" in full
assert "Show that $x^2 \\ge 0$." in full
assert full.index("Practice Problems") < full.index("\\end{document}")

def test_build_full_latex_passthrough_inserts_problems_before_end_multicols(self):
raw = (
"\\documentclass{article}\n"
"\\usepackage{multicol}\n"
"\\begin{document}\n"
"\\begin{multicols}{2}\n"
"Custom\n"
"\\end{multicols}\n"
"\\end{document}"
)
sheet = CheatSheet.objects.create(
title="Raw Multi",
latex_content=raw,
)
PracticeProblem.objects.create(
cheat_sheet=sheet,
question_latex="Integrate $x$.",
answer_latex="$x^2 / 2 + C$",
order=1,
)

full = sheet.build_full_latex()

assert full.index("Practice Problems") < full.index("\\end{multicols}")

def test_build_full_latex_with_problems(self):
sheet = CheatSheet.objects.create(
title="With Problems",
Expand All @@ -111,6 +155,17 @@ def test_build_full_latex_with_problems(self):
assert "What is $1+1$?" in full
assert "$2$" in full

def test_build_full_latex_8pt_uses_extarticle(self):
sheet = CheatSheet.objects.create(
title="Small Font",
latex_content="Content",
font_size="8pt",
)

full = sheet.build_full_latex()

assert "\\documentclass[8pt]{extarticle}" in full


# ── API Tests ────────────────────────────────────────────────────────

Expand Down
2 changes: 1 addition & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ dj-database-url>=2.1
psycopg2-binary>=2.9

# Testing
pytest>=8.0
pytest>=9.0.3
pytest-django>=4.8

# Development (linting, security)
Expand Down
11 changes: 11 additions & 0 deletions frontend/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';

export default [
{
ignores: ['dist/**', 'node_modules/**'],
},
js.configs.recommended,
{
files: ['**/*.{js,jsx}'],
Expand Down Expand Up @@ -41,5 +44,13 @@ export default [
version: 'detect'
}
}
},
{
files: ['vite.config.js'],
languageOptions: {
globals: {
process: 'readonly'
}
}
}
];
14 changes: 7 additions & 7 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
"eslint": "^9.0.0",
"eslint-plugin-react": "^7.34.0",
"eslint-plugin-react-hooks": "^5.0.0",
"vite": "^6.0.0"
"vite": "^6.4.2"
},
"overrides": {
"picomatch": "^4.0.4"
}
}
Loading
Loading