Skip to content

Commit 7f0e1d8

Browse files
committed
πŸ”„ Templates: Migrate from Mako to Flask's native Jinja2 engine
Replace the unmaintained `flask-mako` package (last updated 2015, incompatible with Flask 3.x) with Jinja2, Flask's built-in template engine. This eliminates two dependencies and removes the last blocker for running on modern Flask. - Convert all 37 `.mak` templates to `.html` Jinja2 equivalents - `master.html`: `<%def>` blocks β†’ `{% block %}`, `${self.body()}` β†’ `{% block body %}` - `blogs.html`: Inline Python moved to view, `loop.index` adjusted for Jinja2's 1-based indexing, `${var|h}` β†’ `{{ var|e }}` - `syllabus.html`: block overrides, unused macro removed - Update `site.py`, `blueprints.py`, `participants.py` to use Flask's native `render_template` - Add `name=` to duplicate blueprint registrations (Flask 3.x) - Update `freeze.py` to use `(endpoint, values)` tuples for namespaced blueprint endpoints - Remove `pyproject.toml` dependencies on `Mako` and `flask-mako` - Delete all 37 old `.mak` template files Assisted-by: Claude Opus 4.6 (1M context) Signed-off-by: Justin Wheeler <git@jwheel.org>
1 parent 565b6f3 commit 7f0e1d8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+437
-384
lines changed

β€ŽCLAUDE.mdβ€Ž

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Project Overview
66

7-
HFLOSSK is the Humanitarian Free/Open Source Software Course website for RIT, built with Flask and Mako templates. It serves course materials (lectures, homework, quizzes) and tracks student participation via YAML files and RSS blog feeds. There is no database β€” all data is file-based.
7+
HFLOSSK is the Humanitarian Free/Open Source Software Course website for RIT, built with Flask and Jinja2 templates. It serves course materials (lectures, homework, quizzes) and tracks student participation via YAML files and RSS blog feeds. There is no database β€” all data is file-based.
88

99
## Commands
1010

@@ -51,8 +51,8 @@ python freeze.py
5151
### Entry Point
5252
`app.py` imports the Flask app from `hflossk/site.py` and runs the Flask dev server in debug mode. For production, use `gunicorn hflossk.site:app`.
5353

54-
### Template Engine: Mako (not Jinja2)
55-
Templates use `.mak` extension and Mako syntax (`${variable}`, `<%inherit>`, `<%def>`, `% for`). The Flask-Mako extension bridges Flask and Mako. All templates live in `hflossk/templates/`.
54+
### Template Engine: Jinja2 (Flask native)
55+
Templates use `.html` extension and Jinja2 syntax (`{{ variable }}`, `{% extends %}`, `{% block %}`, `{% for %}`). All templates live in `hflossk/templates/` and inherit from `master.html`.
5656

5757
### Core Modules
5858
- **`hflossk/site.py`** β€” Flask app creation, route definitions, context processor that injects `site.yaml` config into all templates. Gravatar/Libravatar helper. Routes for pages, syllabus, blog JSON endpoint, participant profiles, resources.
@@ -66,7 +66,7 @@ Templates use `.mak` extension and Mako syntax (`${variable}`, `<%inherit>`, `<%
6666
3. **Blog tracking**: `/blog/<username>` endpoint parses student RSS feeds and returns JSON post count. Used via AJAX in the participants page.
6767

6868
### Content as Templates
69-
Course content (lectures, homework, quizzes) are Mako template files in `hflossk/templates/hw/`, `hflossk/templates/lectures/`, `hflossk/templates/quiz/`. They inherit from `master.mak`.
69+
Course content (lectures, homework, quizzes) are Jinja2 template files in `hflossk/templates/hw/`, `hflossk/templates/lectures/`, `hflossk/templates/quiz/`. They inherit from `master.html`.
7070

7171
### Static Assets
7272
`hflossk/static/` contains Bootstrap CSS/JS, course PDFs (`books/`), slide decks (`decks/`), and challenge descriptions (`challenges/`).
@@ -82,7 +82,6 @@ Course content (lectures, homework, quizzes) are Mako template files in `hflossk
8282
- **Static site generation**: `freeze.py` uses Frozen-Flask to crawl the app and generate static HTML. Dynamic routes like `/blog/<username>` (live RSS parsing) are excluded.
8383

8484
## Key Constraints
85-
- Flask-Mako pins the project to Mako templating; migration to Jinja2 would require rewriting 30+ template files
8685
- No database or ORM β€” all persistence is YAML files in the repo
8786
- Python 3.14 on Fedora 43 is the target runtime
8887

β€Žfreeze.pyβ€Ž

Lines changed: 34 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -18,69 +18,60 @@
1818

1919
@freezer.register_generator
2020
def simple_page():
21-
"""Generate URLs for all top-level .mak templates."""
21+
"""Generate URLs for all top-level .html templates."""
2222
templates_dir = os.path.join(base_dir, 'templates')
2323
for fname in os.listdir(templates_dir):
24-
if fname.endswith('.mak') and fname not in ('master.mak', 'ohno.mak',
25-
'participant.mak',
26-
'blogs.mak'):
27-
yield {'page': fname.replace('.mak', '')}
24+
if fname.endswith('.html') and fname not in ('master.html', 'ohno.html',
25+
'participant.html',
26+
'blogs.html'):
27+
yield {'page': fname.replace('.html', '')}
2828

2929

30-
def _hw_pages():
31-
"""Generate page values for homework templates."""
30+
@freezer.register_generator
31+
def blueprint_pages():
32+
"""Generate URLs for all blueprint-served pages.
33+
34+
Yields (endpoint, values) tuples for namespaced blueprint endpoints.
35+
Flask 3.x requires unique names for duplicate blueprint registrations,
36+
so endpoints are namespaced as '<name>.<view_function>'.
37+
"""
3238
hw_dir = os.path.join(base_dir, 'templates', 'hw')
3339
if os.path.isdir(hw_dir):
34-
yield {'page': 'index'}
35-
for fname in os.listdir(hw_dir):
36-
if fname.endswith('.mak') and fname != 'index.mak':
37-
yield {'page': fname.replace('.mak', '')}
40+
pages = ['index']
41+
pages.extend(f.replace('.html', '') for f in os.listdir(hw_dir)
42+
if f.endswith('.html') and f != 'index.html')
43+
for page in pages:
44+
yield 'hw.display_homework', {'page': page}
45+
yield 'assignments.display_homework', {'page': page}
3846

39-
40-
def _lecture_pages():
41-
"""Generate page values for lecture templates."""
4247
lectures_dir = os.path.join(base_dir, 'templates', 'lectures')
4348
if os.path.isdir(lectures_dir):
44-
yield {'page': 'index'}
45-
for fname in os.listdir(lectures_dir):
46-
if fname.endswith('.mak') and fname != 'index.mak':
47-
yield {'page': fname.replace('.mak', '')}
48-
49+
pages = ['index']
50+
pages.extend(f.replace('.html', '') for f in os.listdir(lectures_dir)
51+
if f.endswith('.html') and f != 'index.html')
52+
for page in pages:
53+
yield 'lectures.display_lecture', {'page': page}
4954

50-
def _quiz_pages():
51-
"""Generate quiz_num values for quiz templates."""
5255
quiz_dir = os.path.join(base_dir, 'templates', 'quiz')
5356
if os.path.isdir(quiz_dir):
5457
for fname in os.listdir(quiz_dir):
55-
if fname.endswith('.mak'):
56-
yield {'quiz_num': fname.replace('.mak', '')}
58+
if fname.endswith('.html'):
59+
quiz_num = fname.replace('.html', '')
60+
yield 'quizzes.show_quiz', {'quiz_num': quiz_num}
61+
yield 'quiz.show_quiz', {'quiz_num': quiz_num}
5762

58-
59-
def _participant_pages():
60-
"""Generate URLs for all participant profile pages."""
6163
people_dir = os.path.join('scripts', 'people')
6264
for dirpath, dirnames, files in os.walk(people_dir):
6365
for fname in files:
6466
if fname.endswith('.yaml'):
6567
parts = dirpath.split(os.sep)
6668
if len(parts) >= 4:
67-
year = parts[-2]
68-
term = parts[-1]
69-
username = fname.replace('.yaml', '')
70-
yield {'year': year, 'term': term, 'username': username}
71-
72-
73-
# Register generators for each named blueprint registration.
74-
# Flask 3.x requires unique names for duplicate blueprint registrations,
75-
# so endpoints are namespaced as '<name>.<view_function>'.
76-
freezer.register_generator(lambda: _hw_pages(), 'assignments.display_homework')
77-
freezer.register_generator(lambda: _hw_pages(), 'hw.display_homework')
78-
freezer.register_generator(lambda: _lecture_pages(), 'lectures.display_lecture')
79-
freezer.register_generator(lambda: _quiz_pages(), 'quizzes.show_quiz')
80-
freezer.register_generator(lambda: _quiz_pages(), 'quiz.show_quiz')
81-
freezer.register_generator(lambda: _participant_pages(), 'participants.participant_page')
82-
freezer.register_generator(lambda: _participant_pages(), 'blogs.participant_page')
83-
freezer.register_generator(lambda: _participant_pages(), 'checkblogs.participant_page')
69+
values = {
70+
'year': parts[-2],
71+
'term': parts[-1],
72+
'username': fname.replace('.yaml', ''),
73+
}
74+
yield 'participant_page', values
8475

8576

8677
if __name__ == '__main__':

β€Žhflossk/blueprints.pyβ€Ž

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import os
22

3-
from flask import Blueprint
4-
from flask_mako import render_template
3+
from flask import Blueprint, render_template
54

65
homework = Blueprint('homework', __name__, template_folder='templates')
76
lectures = Blueprint('lectures', __name__, template_folder='templates')
@@ -16,11 +15,11 @@ def display_homework(page):
1615
'static', 'hw'))
1716
hws.extend(os.listdir(os.path.join(os.path.split(__file__)[0],
1817
'templates', 'hw')))
19-
hws = [hw for hw in sorted(hws) if not hw == "index.mak"]
18+
hws = [hw for hw in sorted(hws) if not hw == "index.html"]
2019
else:
2120
hws = None
2221

23-
return render_template('hw/{}.mak'.format(page), name='mako', hws=hws)
22+
return render_template('hw/{}.html'.format(page), hws=hws)
2423

2524

2625
@lectures.route('/', defaults={'page': 'index'})
@@ -30,14 +29,14 @@ def display_lecture(page):
3029
lecture_notes = os.listdir(os.path.join(os.path.split(__file__)[0],
3130
'templates', 'lectures'))
3231
lecture_notes = [note for note in sorted(lecture_notes)
33-
if not note == "index.mak"]
32+
if not note == "index.html"]
3433
else:
3534
lecture_notes = None
3635

37-
return render_template('lectures/{}.mak'.format(page), name='mako',
36+
return render_template('lectures/{}.html'.format(page),
3837
lectures=lecture_notes)
3938

4039

4140
@quizzes.route('/<quiz_num>')
4241
def show_quiz(quiz_num):
43-
return render_template('quiz/{}.mak'.format(quiz_num), name='mako')
42+
return render_template('quiz/{}.html'.format(quiz_num))

β€Žhflossk/participants.pyβ€Ž

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33

44
import hflossk
55
import yaml
6-
from flask import Blueprint
7-
from flask_mako import render_template
6+
from flask import Blueprint, render_template
87

98

109
participants_bp = Blueprint('participants_bp',
@@ -85,6 +84,9 @@ def participants(root_dir):
8584
)
8685
contents['isActive'] = (currentYear in year_term_data
8786
and currentTerm in year_term_data)
87+
# Ensure hw dict exists for template iteration
88+
if 'hw' not in contents:
89+
contents['hw'] = {}
8890

8991
student_data.append(contents)
9092

@@ -97,10 +99,8 @@ def participants(root_dir):
9799
len(assignments))
98100

99101
return render_template(
100-
'blogs.mak', name='mako',
102+
'blogs.html',
101103
student_data=student_data,
102104
gravatar=hflossk.site.gravatar,
103105
target_number=target_number
104106
)
105-
106-
#

β€Žhflossk/site.pyβ€Ž

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111
from datetime import datetime
1212

1313
import yaml
14-
from flask import Flask, jsonify
15-
from flask_mako import MakoTemplates, render_template
14+
from flask import Flask, jsonify, render_template
1615
from werkzeug.exceptions import NotFound
1716

1817
from hflossk.blueprints import homework, lectures, quizzes
@@ -21,7 +20,6 @@
2120

2221
app = Flask(__name__)
2322
app.template_folder = "templates"
24-
mako = MakoTemplates(app)
2523
base_dir = os.path.split(__file__)[0]
2624

2725

@@ -32,7 +30,6 @@ def inject_yaml():
3230
site_config = yaml.safe_load(site_yaml)
3331
return site_config
3432

35-
app.config['MAKO_TRANSLATE_EXCEPTIONS'] = False
3633
config = inject_yaml()
3734
COURSE_START = datetime.combine(config['course']['start'], datetime.min.time())
3835
COURSE_END = datetime.combine(config['course']['end'], datetime.max.time())
@@ -59,13 +56,13 @@ def gravatar(email):
5956
@app.route('/<page>')
6057
def simple_page(page):
6158
"""
62-
Render a simple page. Looks for a .mak template file
59+
Render a simple page. Looks for a .html template file
6360
with the name of the page parameter that was passed in.
6461
By default, this just shows the homepage.
6562
6663
"""
6764

68-
return render_template('{}.mak'.format(page), name='mako')
65+
return render_template('{}.html'.format(page))
6966

7067

7168
@app.route('/static/manifest.webapp')
@@ -86,7 +83,7 @@ def syllabus():
8683

8784
with open(os.path.join(base_dir, 'schedule.yaml')) as schedule_yaml:
8885
schedule = yaml.safe_load(schedule_yaml)
89-
return render_template('syllabus.mak', schedule=schedule, name='mako')
86+
return render_template('syllabus.html', schedule=schedule)
9087

9188

9289
@app.route('/blog/<username>')
@@ -125,7 +122,7 @@ def participant_page(year, term, username):
125122
year, term, username + '.yaml'))
126123
with open(person_yaml) as participant_file:
127124
return render_template(
128-
'participant.mak', name='make',
125+
'participant.html',
129126
participant_data=yaml.safe_load(participant_file),
130127
gravatar=gravatar
131128
)
@@ -142,14 +139,14 @@ def resources():
142139
res['Videos'] = os.listdir(os.path.join(
143140
base_dir, 'static', 'videos'))
144141

145-
return render_template('resources.mak', name='mako', resources=res)
142+
return render_template('resources.html', resources=res)
146143

147144

148-
app.register_blueprint(homework, url_prefix='/assignments')
149-
app.register_blueprint(homework, url_prefix='/hw')
145+
app.register_blueprint(homework, url_prefix='/assignments', name='assignments')
146+
app.register_blueprint(homework, url_prefix='/hw', name='hw')
150147
app.register_blueprint(lectures, url_prefix='/lectures')
151-
app.register_blueprint(quizzes, url_prefix='/quizzes')
152-
app.register_blueprint(quizzes, url_prefix='/quiz')
153-
app.register_blueprint(participants_bp, url_prefix='/participants')
154-
app.register_blueprint(participants_bp, url_prefix='/blogs')
155-
app.register_blueprint(participants_bp, url_prefix='/checkblogs')
148+
app.register_blueprint(quizzes, url_prefix='/quizzes', name='quizzes')
149+
app.register_blueprint(quizzes, url_prefix='/quiz', name='quiz')
150+
app.register_blueprint(participants_bp, url_prefix='/participants', name='participants')
151+
app.register_blueprint(participants_bp, url_prefix='/blogs', name='blogs')
152+
app.register_blueprint(participants_bp, url_prefix='/checkblogs', name='checkblogs')
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
<%inherit file="master.mak"/>
1+
{% extends "master.html" %}
2+
3+
{% block body %}
24

35
<div class="jumbotron">
46
<h1>Hello, world!</h1>
@@ -25,3 +27,4 @@ <h2>Mako</h2>
2527
<p><a class="btn" target="_blank" href="http://www.makotemplates.org/">View details &raquo;</a></p>
2628
</div><!--/span-->
2729
</div><!--/row-->
30+
{% endblock %}

0 commit comments

Comments
Β (0)