Skip to content

Commit b18a535

Browse files
lukemerrettclaude
andcommitted
refactor(cohort): vendor and inline Tom Select instead of CDN
Per the Doist service-production-readiness standard, the cohort form should not make third-party network calls at runtime. Vendor Tom Select 2.3.1 into bitmapist/cohort/tmpl/vendor/ (verified against the published sha384 hashes) and inline the CSS/JS into the rendered fragment. The assets are inlined rather than referenced by URL because the library has no static-serving mechanism of its own -- consumers just embed the string returned by render_html_form(), so there is no path they could mount. Inlining makes the fragment fully self-contained and CSP/offline safe. render_html_form() reads the vendored files once (module cache) and passes them to the template; the wheel already ships everything under bitmapist/, so the assets are packaged automatically. The library <script> is emitted after the form markup so the browser can render the form before executing ~50KB of JS, and the initializer no-ops with a console warning if TomSelect is somehow unavailable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6d45ad6 commit b18a535

4 files changed

Lines changed: 491 additions & 15 deletions

File tree

bitmapist/cohort/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,24 @@
8181

8282
# --- HTML rendering
8383

84+
# Tom Select is vendored (not loaded from a CDN) so the cohort form makes no
85+
# third-party network calls at runtime and works in offline / CSP-restricted
86+
# deployments. The assets are inlined into the fragment because the library has
87+
# no static-serving mechanism of its own -- consumers just embed the string
88+
# returned by render_html_form().
89+
_TOM_SELECT_CSS = "tmpl/vendor/tom-select-2.3.1.default.min.css"
90+
_TOM_SELECT_JS = "tmpl/vendor/tom-select-2.3.1.complete.min.js"
91+
92+
_VENDOR_CACHE: dict[str, str] = {}
93+
94+
95+
def _read_vendor_asset(relative_path: str) -> str:
96+
if relative_path not in _VENDOR_CACHE:
97+
file_path = path.join(path.dirname(path.abspath(__file__)), relative_path)
98+
with open(file_path, encoding="utf-8") as f:
99+
_VENDOR_CACHE[relative_path] = f.read()
100+
return _VENDOR_CACHE[relative_path]
101+
84102

85103
def render_html_form(
86104
action_url,
@@ -141,6 +159,8 @@ def render_html_form(
141159
num_results=int(num_results),
142160
num_of_rows=int(num_of_rows),
143161
start_date=start_date,
162+
tom_select_css=_read_vendor_asset(_TOM_SELECT_CSS),
163+
tom_select_js=_read_vendor_asset(_TOM_SELECT_JS),
144164
)
145165
)
146166

bitmapist/cohort/tmpl/form_data.mako

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/css/tom-select.default.min.css" rel="stylesheet"
2-
integrity="sha384-86bgzEpZNdMv4V0h2BgSxQUadlX29O0I89TbSGZ4gFkGtpN8U+KAGcuDbqMdbl/M" crossorigin="anonymous" />
3-
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.complete.min.js"
4-
integrity="sha384-cnROoUgVILyibe3J0zhzWoJ9p2WmdnK7j/BOTSWqVDbC1pVw2d+i6Q/1ESKJKCYf" crossorigin="anonymous"></script>
1+
<%doc>
2+
Tom Select 2.3.1 is vendored and inlined (see bitmapist/cohort/tmpl/vendor/)
3+
rather than loaded from a CDN, so the cohort form makes no third-party
4+
network calls at runtime and works in offline / CSP-restricted deployments.
5+
</%doc>
6+
<style>${ tom_select_css | n }</style>
57

68
<style>
79
.cohort_form dd {
@@ -96,22 +98,34 @@
9698
</dl>
9799
</form>
98100

101+
<%doc>
102+
The library is inlined here, after the form markup, so the browser can
103+
render the form before parsing/executing ~50KB of JS.
104+
</%doc>
105+
<script>${ tom_select_js | n }</script>
106+
99107
<script>
100108
// Turn the event dropdowns into searchable selects. With hundreds of
101109
// bitmapist events, scrolling a native <select> is painful; Tom Select
102110
// adds a type-to-filter search box while still submitting the same value.
103-
document.querySelectorAll('.cohort_form .cohort-event-select').forEach(function (el) {
104-
// The form fragment may be rendered more than once on a page, so this
105-
// initializer can run again over selects that are already enhanced.
106-
// Tom Select throws if re-initialized, so skip anything already done.
107-
if (el.tomselect) {
108-
return;
109-
}
110-
new TomSelect(el, {
111-
maxOptions: null, // never truncate the filtered list
112-
searchField: ['text', 'value'],
111+
if (typeof TomSelect === 'undefined') {
112+
// Library failed to load for some reason; leave the native <select>s
113+
// in place rather than throwing.
114+
console.warn('Tom Select unavailable; cohort event dropdowns not enhanced.');
115+
} else {
116+
document.querySelectorAll('.cohort_form .cohort-event-select').forEach(function (el) {
117+
// The form fragment may be rendered more than once on a page, so
118+
// this initializer can run again over selects that are already
119+
// enhanced. Tom Select throws if re-initialized, so skip those.
120+
if (el.tomselect) {
121+
return;
122+
}
123+
new TomSelect(el, {
124+
maxOptions: null, // never truncate the filtered list
125+
searchField: ['text', 'value'],
126+
});
113127
});
114-
});
128+
}
115129
</script>
116130

117131
<%def name="render_options(select_name, selections, current_selection)">

0 commit comments

Comments
 (0)