Add tariffs from the meter form via a modal - #9
Open
tescalada wants to merge 11 commits into
Open
Conversation
Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
tescalada
force-pushed
the
tariff-modal-form
branch
2 times, most recently
from
July 22, 2026 19:43
7322d49 to
f4ad95b
Compare
Coverage report
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
tescalada
force-pushed
the
tariff-modal-form
branch
from
July 31, 2026 11:47
f4ad95b to
b5a7b54
Compare
Extract the tariff form fields into a shared partial included by both the standalone tariff page and a new modal-only template. Add GET|POST /tariff/add-modal, guarded by the tariff:add permission. GET returns the form fragment with no page chrome. A valid POST creates the tariff and returns its id and name as JSON; an invalid POST returns 400 and re-renders the fragment with inline field errors. On the customer meter form, the tariff select starts blank and carries an <Add New> option that loads the modal form, submits it, and inserts the created tariff into the dropdown.
A worktree's .git is a file rather than a directory, so hatch-vcs cannot read it from the build context and the version was pinned to a bare 0.0.0. sparkmeter.__version__ derives git_version from the PEP 440 local segment, which 0.0.0 does not have, leaving it empty. The page tests scrub GIT_VERSION out of every snapshot with str.replace(), and replacing the empty string inserts the marker between every character, so every page test failed.
The block rate, TOU and scheduled load limit validators store the raw ValueError rather than its message. render_modal_form serialized form.errors directly, and the JSON encoder has no branch for exceptions, so any of those three validation failures raised a TypeError and turned the 400 into a 500. BaseForm.render already coerced every error to str and logged the result. Move that into set_form_errors_header and use it from both, so the modal and the standalone page report errors identically. Build the modal form from request.form on GET as well, matching /tariff/add. With no formdata the JSON collection fields keep None, which renders as null in the params element and as an empty value in the hidden inputs, and posting an untouched modal back then fails to parse. Use http.client.OK for render_modal_form's default status, consistent with the rest of the module.
The behavior lived in a ~190 line inline <script> in meter-form.html: not linted, not covered by the jest suite, not bundled, and baked into 19 page snapshots. Move it to meter/js/meter-tariff-modal.js, registered as the meter-form page loader, and cover it with a spec. Because the module can require tariff/js/tariff-form.js, the tariff form behavior now runs inside the modal. Previously TariffForm was registered only for the tariff-form page, so in the modal the block rate and scheduled load limit toggles, the plan, TOU and daily energy limit checkboxes, and the three table editors all did nothing. Namespace TariffForm's delegated handlers so re-rendering the fragment after a 400 rebinds them instead of stacking a second copy on document. The fragment is a real <form> and the save control sits outside it in the modal footer, so pressing Enter in any field did a full page POST and navigated away from the half-filled meter form. Handle submit as well as the save button click. The shared partial emits the block rate, TOU and load limit editors as top-level modals. Injected into the modal body they became nested modals, which Bootstrap 3 does not support, and their hidden.bs.modal bubbled to the outer handler. Relocate them to <body>, stack their z-indexes, keep the body scroll lock while a modal is still open, and have the outer handler react only to its own modal. An expired session redirects to the login page, and jQuery follows the redirect, so the login page arrived as a 200 and was injected into the modal body; on POST it reached the success path and threw on data.tariff.id. Check for the tariff form and for the tariff payload, and tell the user to reload and sign in. Cancelling restored the select with `this.previousValue || ''`, and '' matches no option, so the select rendered empty. Restore the blank option's actual value, __None. Guard save against a second request while one is in flight, since tariff.name has no unique constraint, and drop a response that arrives after the modal was dismissed. Define form_tariff_error once, in the partial that calls it, instead of in each of the two including templates. Move the modal's layout rules from a <style> block in the fetched fragment, which accumulated an element per open and per re-render, into modal.scss, and let .modal-lg set the dialog width. Carry the modal's strings in data attributes rather than interpolating them into JavaScript string literals, wrap <Add New> in _(), and extract the new strings into messages.pot. flask-babel's LazyString defines __html__, so translated text is emitted unescaped; forceescape is needed for the attribute values. Reject __add_new__ server-side. It is a client-side sentinel with no server counterpart, and allow_blank made a posted __add_new__ report "Please select a tariff or add a new one." rather than an invalid choice. Drop validate_tariff's meter_type check, which cannot be false: __init__ deletes the field for totalizers. Add tests for the meter form consumer, validate_tariff, the blank option and its text, the tariff error markup, and the tariff:add permission boundary on /tariff/add-modal.
base.flash builds its alert with jQuery's .html(), so the message the server returns in the add-modal JSON reached the page as markup. The inline implementation the module replaced used .text(). Add base.flashText, which builds the same alert from .text(), and have the tariff modal use it for all four of its messages. base.flash is shared by the rest of the application and keeps its behavior.
The <style> block that moved into modal.scss set the dialog to 900px with a max-width of calc(100vw - 40px) at every viewport width. `.modal-lg` applies its 900px only from 992px up, so between 768px and 991px the dialog fell back to Bootstrap's 600px -- where the column overrides already assume the wider dialog -- and the max-width clamp had no replacement at all. Set the width in modal.scss and drop `.modal-lg` from the dialog, leaving one source for it. Drop ensureModal's adopt branch as well. It returned before setting the modal title and the cancel and save labels, so reaching it would have shipped an unlabelled modal; MeterTariffModal is constructed once per page, so nothing does.
The editor ids come from a shared partial, so clearing them out of <body> by selector on every render could take out same-id elements owned by anything else on the page. Keep the elements that were relocated and remove those, and reach for their time pickers the same way. BLANK_VALUE's comment described a selectedIndex of -1 the code cannot produce: a rendered select's val() returns the blank option's value, so the `|| BLANK_VALUE` fallback could only fire for a null val(). Drop the fallback and say what the constant is for.
The stacking the module does on show.bs.modal had no test coverage at all: deleting the binding left the whole suite green. Open an editor modal on top of the tariff modal and assert the resulting dialog and backdrop order. Bootstrap unwinds the body scroll lock as it hides a modal, without regard for another one still being open: it drops modal-open and hands back the padding that compensates for the scrollbar the lock hides. The handler put modal-open back but not the padding, so closing a block rate, TOU or load limit editor with the tariff modal still open could shift the page sideways by the scrollbar width. Record the padding on hide.bs.modal and restore it together with the class. Drop the event.target check in the tariff modal's own hidden.bs.modal handler. The three editor modals are relocated to <body> as siblings and they are the only modals the tariff form partial emits, so nothing inside the tariff modal can raise the event. The test that appeared to cover the check asserted that DOM layout rather than the check, and passed with the check deleted.
QuerySelectField.pre_validate reports every primary key it cannot resolve as "Not a valid choice", and WTForms carries on running the validation chain after a pre_validate ValidationError. A posted __add_new__ therefore collected that message and validate_tariff's accurate one, and the field rendered both stacked together. Reject the sentinel in the field's own pre_validate with StopValidation, which carries the accurate message and ends the chain.
The vendored Bootstrap is 3.0.0, whose only body handling is adding and removing the `modal-open` class. Both the scroll lock `overflow: hidden` and the `margin-right: 15px` that stands in for the hidden scrollbar are stylesheet rules keyed on that class, so nothing ever wrote an inline `padding-right` on <body>. `rememberScrollbarPad` therefore always read the empty string, and re-applying it was a no-op. Re-adding `modal-open` when an inner editor closes over a still-open tariff modal restores both rules, so that stays. The removed test manufactured the padding reset it then asserted was undone, so it exercised its own handler rather than Bootstrap.
tescalada
force-pushed
the
tariff-modal-form
branch
from
July 31, 2026 18:14
b5a7b54 to
5f42228
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a tariff from inside the customer meter form, without navigating away
and losing the in-progress meter.
The tariff select starts blank and carries an
<Add New>option. Choosing itopens a modal containing the tariff form, loaded from a new endpoint. On save
the created tariff is inserted into the select and selected; on validation
failure the modal re-renders with inline field errors. Cancelling restores the
previous selection.
Shared form fields
The tariff form fields move into
tariff/_tariff-form-fields.html, included byboth the standalone tariff page and the modal template, so the two stay in step.
The
form_tariff_errormacro is defined once, in the partial that calls it.Endpoint
GET|POST /tariff/add-modal, guarded by thetariff:addpermission.GETreturns the form fragment with no page chrome.POSTcreates the tariff and returns{"message", "tariff": {"id", "name"}}.POSTreturns400, re-renders the fragment with inline errors, andsets
X-Form-Errorsto a JSON map of field to messages.X-Form-Errorsis now built by one helper shared withBaseForm.render, whichcoerces error values to strings. The tariff collection validators store raw
ValueErrorobjects, which are not JSON-serializable, so serializing themdirectly returned a 500 instead of the 400.
Modal behavior
The behavior lives in
sparkmeter/meter/js/meter-tariff-modal.js, registeredthrough the existing page-loader mechanism and covered by a jest spec.
The tariff form's own JavaScript is initialized against the injected fragment,
so the block-rate, time-of-use, scheduled-load-limit, monthly-plan and daily-
energy-limit controls work in the modal as they do on the standalone page. Its
delegated handlers are namespaced and rebound rather than stacked when the
fragment re-renders after a 400.
Bootstrap 3 gives every modal the same z-index and drops
modal-openoff<body>whenever any modal closes. The block-rate, time-of-use and load-limiteditors are relocated out of the modal body, stacked above it, and the body
scroll lock is held while any modal is still open.
Submitting is guarded against double-submit, and a response arriving after the
modal is dismissed is ignored.
Meter form validation
A customer meter now requires a tariff, reported as "Please select a tariff or
add a new one." The
<Add New>sentinel is rejected by the field itself so itsurfaces one accurate message rather than also collecting "Not a valid choice".
Test tooling
scripts/run_coverage.shgives the test image a version carrying a+g<short hash>local segment. Without itsparkmeter.__version__.git_versionis empty, and the page tests scrub
GIT_VERSIONfrom snapshots withstr.replace()— replacing the empty string inserts the marker between everycharacter of every snapshot and fails the suite.