diff --git a/assets/javascripts/base.js b/assets/javascripts/base.js index a7a6d94..5a106fb 100644 --- a/assets/javascripts/base.js +++ b/assets/javascripts/base.js @@ -16,9 +16,7 @@ function format_currency(number) { } exports.format_currency = format_currency; -function flash(msg, category, delay) { - var flash = $('
'); - flash.html(msg); +function showAlert(flash, category, delay) { flash.addClass('alert'); flash.hide(); if (category) { @@ -30,8 +28,19 @@ function flash(msg, category, delay) { flash.delay(delay).fadeOut('slow'); } } + +function flash(msg, category, delay) { + showAlert($('
').html(msg), category, delay); +} exports.flash = flash; +// Same alert as flash(), for a message that is not markup -- notably one that +// came back from the server, which must not be able to inject elements. +function flashText(msg, category, delay) { + showAlert($('
').text(msg), category, delay); +} +exports.flashText = flashText; + var _pageLoaders = {}; function registerPageLoader(pageName, loader) { diff --git a/assets/javascripts/base.spec.js b/assets/javascripts/base.spec.js index 0e48e20..38a0996 100644 --- a/assets/javascripts/base.spec.js +++ b/assets/javascripts/base.spec.js @@ -45,6 +45,27 @@ describe('base', function() { base.flash("testing", "category", 1000); expect($(".alerts").html()).toMatchSnapshot(); }); + test('should render its message as markup', function() { + base.flash("bold"); + expect($(".alerts b").length).toBe(1); + }); + }); + + describe('flashText', function() { + test('should add a div to .alerts', function() { + base.flashText("testing"); + expect($(".alerts").html()).toBe( + '
testing
'); + }); + test('should add an alert-category class', function() { + base.flashText("testing", "category", 1000); + expect($(".alerts div").hasClass('alert-category')).toBe(true); + }); + test('should render its message as text', function() { + base.flashText("bold"); + expect($(".alerts b").length).toBe(0); + expect($(".alerts").text()).toBe("bold"); + }); }); describe('getMetaItemProps', function() { diff --git a/assets/stylesheets/modal.scss b/assets/stylesheets/modal.scss index c512b50..b24fe42 100644 --- a/assets/stylesheets/modal.scss +++ b/assets/stylesheets/modal.scss @@ -29,6 +29,47 @@ border-bottom: 1px solid #CDCDCD; } +// The tariff form is a horizontal form laid out for a full-width page. Inside +// the tariff modal its Bootstrap column widths have to be narrowed to fit the +// dialog. +#meter-tariff-modal { + // Bootstrap's own dialog widths are 600px from 768px up and, with `.modal-lg`, + // 900px from 992px up. The tariff form needs the wider dialog at every + // viewport, clamped so it still fits a narrow one. + .modal-dialog { + width: 900px; + max-width: calc(100vw - 40px); + } + + .control-label.col-lg-2 { + width: 18%; + } + + .input-group.col-lg-3, + .col-md-5 { + width: 52%; + } + + .input-group.col-lg-10, + .btn-group.col-lg-10 { + width: 70%; + } + + .input-group .form-control { + min-width: 0; + } + + @media (max-width: 991px) { + .control-label.col-lg-2, + .input-group.col-lg-3, + .input-group.col-lg-10, + .btn-group.col-lg-10, + .col-md-5 { + width: 100%; + } + } +} + .delete-text { span { text-align: center; diff --git a/messages.pot b/messages.pot index 5172ecb..7a93394 100644 --- a/messages.pot +++ b/messages.pot @@ -990,3 +990,39 @@ msgstr "" msgid "Saved custom settings file." msgstr "" + +msgid "" +msgstr "" + +msgid "Add a New Tariff" +msgstr "" + +msgid "Loading tariff form..." +msgstr "" + +msgid "Saving..." +msgstr "" + +msgid "Cancel" +msgstr "" + +msgid "Tariff created." +msgstr "" + +msgid "Could not load the tariff form." +msgstr "" + +msgid "Could not save the tariff." +msgstr "" + +msgid "Your session has expired. Please reload the page and sign in again." +msgstr "" + +msgid "Select a tariff" +msgstr "" + +msgid "Please select a tariff or add a new one." +msgstr "" + +msgid "No tariff was created. Please select a tariff or add a new one." +msgstr "" diff --git a/scripts/run_coverage.sh b/scripts/run_coverage.sh index 0c822d1..ce3eb91 100644 --- a/scripts/run_coverage.sh +++ b/scripts/run_coverage.sh @@ -8,11 +8,18 @@ compare_branch="${DIFF_COVER_COMPARE_BRANCH:-origin/main}" fail_under="${DIFF_COVER_FAIL_UNDER:-90}" # A git worktree's .git is a file, not a directory, and is unreadable inside the -# build context, so hatch-vcs cannot derive the version. Supply a placeholder in -# that case; a normal checkout leaves it empty and reads .git as usual. +# build context, so hatch-vcs cannot derive the version. Build one from the +# current commit in that case; a normal checkout leaves it empty and reads .git +# as usual. +# +# The local segment (`+g`) is not optional. `sparkmeter.__version__` +# derives `git_version` from it and yields "" when it is absent, and the page +# tests scrub `GIT_VERSION` out of every snapshot with `str.replace()`. Replacing +# the empty string inserts the marker between every character of every snapshot, +# which fails the whole page-test suite. version="" if [ -f .git ]; then - version="0.0.0" + version="0.0.0+g$(git rev-parse --short HEAD)" fi docker compose -f docker-compose.test.yml build \ diff --git a/sparkmeter/meter/js/meter-pages.js b/sparkmeter/meter/js/meter-pages.js index 9ea2b00..bb33555 100644 --- a/sparkmeter/meter/js/meter-pages.js +++ b/sparkmeter/meter/js/meter-pages.js @@ -10,6 +10,11 @@ base.registerPageLoader('meter-chart', function() { new MeterChart.MeterChart(); }); +base.registerPageLoader('meter-form', function() { + var MeterTariffModal = require('meter/js/meter-tariff-modal.js'); + new MeterTariffModal.MeterTariffModal(); +}); + base.registerPageLoader('meter-view', function() { var MeterView = require('meter/js/meter-view.js'); new MeterView.MeterView(); diff --git a/sparkmeter/meter/js/meter-tariff-modal.js b/sparkmeter/meter/js/meter-tariff-modal.js new file mode 100644 index 0000000..ace993f --- /dev/null +++ b/sparkmeter/meter/js/meter-tariff-modal.js @@ -0,0 +1,358 @@ +// -*- coding: utf-8 -*- +// Copyright © 2013-2026 EarthSpark International Corp. +// SPDX-License-Identifier: Apache-2.0 +// +// Lets the customer meter form create a tariff without leaving the page: the +// tariff select carries an extra option that loads /tariff/add-modal into a +// modal, posts it over AJAX, and inserts the created tariff into the select. + +var base = require('base.js'); +var TariffForm = require('tariff/js/tariff-form.js'); + +var MODAL_ID = 'meter-tariff-modal'; +var MODAL_SELECTOR = '#' + MODAL_ID; +var FORM_SELECTOR = 'form#tariff-modal-form'; + +// QuerySelectField(allow_blank=True) renders its blank option with this value, +// so this is what "no tariff chosen" means to the select. Restoring the select +// to '' instead would match no option and render it empty. +var BLANK_VALUE = '__None'; + +// The tariff form partial emits one editor modal per collection field. Bootstrap +// 3 does not support a modal nested inside another modal, so these are moved out +// of the fragment and appended to as siblings of the tariff modal. +var EDITOR_MODAL_SELECTOR = '#loadLimitModal, #blockrateModal, #touModal'; + +// Bootstrap 3 gives every modal the same z-index, so a second modal opened on +// top of the first lands under the first one's backdrop. Each stacked modal is +// raised above the one below it, with its own backdrop just underneath. +// The first modal keeps Bootstrap's own values (1050 dialog / 1040 backdrop). +var BASE_MODAL_Z_INDEX = 1050; +var MODAL_Z_INDEX_STEP = 20; +var BACKDROP_Z_INDEX_OFFSET = 10; + +// Bootstrap 3 marks an open modal with `in`. `:visible` would be equivalent in +// a browser, but it is layout-dependent, and `in` is what Bootstrap itself +// toggles around the `show`/`hidden` events. +var OPEN_MODAL_SELECTOR = '.modal.in'; + +function MeterTariffModal() { + this._init(); +} + +exports.MeterTariffModal = MeterTariffModal; + +MeterTariffModal.prototype = { + _init: function() { + this.params = $('#meter-tariff-modal-params'); + this.select = $('select#tariff'); + if (!this.select.length || !this.params.length) { + return; + } + + this.addNewValue = this.params.attr('data-add-new-value'); + this.previousValue = this.select.val(); + this.requestSeq = 0; + this.pendingRequestId = null; + this.editorModals = $(); + + this.ensureSelectOptions(); + this.ensureModal(); + this.bindEvents(); + }, + + text: function(name) { + return this.params.attr('data-text-' + name); + }, + + bindEvents: function() { + var self = this; + + this.select.on('focusin', function() { + // Focus can land here again while the modal is open; the sentinel + // is not a value worth remembering. + if ($(this).val() === self.addNewValue) { + return; + } + self.previousValue = self.currentTariffValue(); + }); + + this.select.on('change', function() { + if ($(this).val() === self.addNewValue) { + self.openModal(); + } else { + self.previousValue = self.currentTariffValue(); + } + }); + + this.modal.on('hidden.bs.modal', function() { + self.abortPendingRequest(); + self.restoreTariffSelection(); + }); + + this.modal.on('click', '#meter-tariff-modal-save', function(event) { + event.preventDefault(); + self.submitModal(); + }); + + // The fragment is a real
, so Enter in any of its fields would + // otherwise submit it as a full-page POST and navigate away from the + // half-filled meter form. + this.modal.on('submit', FORM_SELECTOR, function(event) { + event.preventDefault(); + self.submitModal(); + }); + + // Bootstrap 3 gives every modal the same z-index and unwinds the whole + // body scroll lock whenever any modal closes, which breaks the tariff + // modal while one of its editors is open on top of it. + $(document).on('show.bs.modal.metertariff', '.modal', function(event) { + self.onAnyModalShow(event.currentTarget); + }); + $(document).on('hidden.bs.modal.metertariff', '.modal', function() { + self.onAnyModalHidden(); + }); + }, + + onAnyModalShow: function(modal) { + // Bootstrap adds `in` after this event, so this counts the modals that + // are already open underneath the one being shown. + var zIndex = BASE_MODAL_Z_INDEX + MODAL_Z_INDEX_STEP * $(OPEN_MODAL_SELECTOR).length; + $(modal).css('z-index', zIndex); + // The backdrop for this modal does not exist yet; Bootstrap appends it + // while handling the same event. `modal-stack` marks the ones already + // placed, so each new backdrop is the only unmarked one. + setTimeout(function() { + $('.modal-backdrop:not(.modal-stack)') + .css('z-index', zIndex - BACKDROP_Z_INDEX_OFFSET) + .addClass('modal-stack'); + }, 0); + }, + + onAnyModalHidden: function() { + // Bootstrap drops `modal-open` off whenever any modal closes, + // and adding and removing that class is its only body handling -- the + // vendored Bootstrap is 3.0.0, which never touches body padding from + // JavaScript. What the class carries lives in the stylesheet + // (assets/stylesheets/bootstrap/_modals.scss): the scroll lock + // `overflow: hidden`, and the flat `margin-right: 15px` that stands in + // for the scrollbar the lock hides. So losing the class while a modal + // is still open unlocks scrolling behind it and shifts the page 15px + // sideways. Putting the class back restores all of it. + if (!$(OPEN_MODAL_SELECTOR).length) { + return; + } + $(document.body).addClass('modal-open'); + }, + + ensureSelectOptions: function() { + if (!this.select.find('option[value="' + this.addNewValue + '"]').length) { + this.select.append( + $('') + .val(this.addNewValue) + .text(this.params.attr('data-add-new-label')) + ); + } + }, + + ensureModal: function() { + $('body').append( + $('
') + .addClass('modal fade') + .attr({id: MODAL_ID, tabindex: '-1', role: 'dialog', 'aria-hidden': 'true'}) + .html( + '' + ) + ); + this.modal = $(MODAL_SELECTOR); + this.saveButton = this.modal.find('#meter-tariff-modal-save'); + this.modal.find('.modal-title').text(this.text('title')); + this.modal.find('.modal-body').html($('

').text(this.text('loading'))); + this.modal.find('.modal-footer .btn-default').text(this.text('cancel')); + this.saveButton.text(this.text('save')); + }, + + openModal: function() { + var self = this; + $.get(this.params.attr('data-modal-url')) + .done(function(html) { + if (!self.renderModal(html)) { + self.sessionExpired(); + return; + } + self.modal.modal('show'); + }) + .fail(function() { + base.flashText(self.text('load-error'), 'danger'); + self.restoreTariffSelection(); + }); + }, + + /** + * Inject a tariff form fragment into the modal body. + * + * @returns {boolean} false when the response is not a tariff form -- an + * expired session redirects to the login page, and jQuery follows the + * redirect, so the login page arrives here with a 200. + */ + renderModal: function(html) { + var fragment = $('
').html(html); + if (!fragment.find(FORM_SELECTOR).length) { + return false; + } + + this.discardEditorModals(); + this.modal.find('.modal-body').html(fragment.contents()); + // Move the collection editors out of the tariff modal so Bootstrap can + // stack them instead of nesting them. Hold on to them: the ids come + // from a shared partial, so nothing else identifies them as ours. + this.editorModals = this.modal.find(EDITOR_MODAL_SELECTOR).appendTo('body'); + this.setupModalWidgets(); + // Wire up the type toggles and the collection editors; TariffForm's + // delegated handlers are namespaced and rebound, so re-rendering the + // fragment after a 400 does not double-bind them. + new TariffForm.TariffForm(); + return true; + }, + + discardEditorModals: function() { + this.editorModals.remove(); + this.editorModals = $(); + }, + + setupModalWidgets: function() { + this.modal.find('select.select2').select2(); + this.modal.find('input.numeric').numeric(); + this.modal.find('.iButton').iButton(); + this.modal.find('.iButton-icons').iButton({ + labelOn: "", + labelOff: "", + handleWidth: 30 + }); + this.editorModals.find('input.timepicker').datetimepicker({ + datepicker: false, + closeOnTimeSelect: true, + format: 'H:i', + mask: '29:00', + allowTimes: [ + '00:00', '01:00', '02:00', '03:00', '04:00', '05:00', + '06:00', '07:00', '08:00', '09:00', '10:00', '11:00', + '12:00', '13:00', '14:00', '15:00', '16:00', '17:00', + '18:00', '19:00', '20:00', '21:00', '22:00', '23:00', '00:00' + ] + }); + }, + + submitModal: function() { + var self = this; + var form = this.modal.find(FORM_SELECTOR); + // `tariff.name` has no unique constraint, so a second request would + // happily create a duplicate tariff. + if (!form.length || this.pendingRequestId !== null) { + return; + } + + var requestId = ++this.requestSeq; + this.pendingRequestId = requestId; + this.setSaving(true); + $.ajax({ + url: form.attr('action'), + method: 'POST', + data: form.serialize() + }).done(function(data) { + if (!self.claimResponse(requestId)) { + return; + } + if (!data || !data.tariff) { + self.sessionExpired(); + return; + } + self.addTariffOption(data.tariff.id, data.tariff.name); + self.previousValue = String(data.tariff.id); + self.select.val(self.previousValue); + self.modal.modal('hide'); + base.flashText(data.message || self.text('created'), 'success'); + }).fail(function(xhr) { + if (!self.claimResponse(requestId)) { + return; + } + if (xhr.status === 400 && xhr.responseText) { + if (!self.renderModal(xhr.responseText)) { + self.sessionExpired(); + } + return; + } + base.flashText(self.text('save-error'), 'danger'); + }); + }, + + /** + * Decide whether a settled request may still touch the meter form. + * + * @returns {boolean} false when the modal was dismissed while this request + * was in flight, in which case it must not touch the meter form. + */ + claimResponse: function(requestId) { + if (this.pendingRequestId !== requestId) { + return false; + } + this.pendingRequestId = null; + this.setSaving(false); + return true; + }, + + abortPendingRequest: function() { + this.pendingRequestId = null; + this.setSaving(false); + }, + + setSaving: function(saving) { + this.saveButton.prop('disabled', saving); + this.saveButton.text(saving ? this.text('saving') : this.text('save')); + }, + + sessionExpired: function() { + this.modal.modal('hide'); + this.restoreTariffSelection(); + base.flashText(this.text('session-expired'), 'danger'); + }, + + addTariffOption: function(value, label) { + var option = this.select.find('option[value="' + value + '"]'); + if (!option.length) { + option = $('').val(value).text(label); + var addNewOption = this.select.find('option[value="' + this.addNewValue + '"]'); + if (addNewOption.length) { + addNewOption.before(option); + } else { + this.select.append(option); + } + } else { + option.text(label); + } + }, + + restoreTariffSelection: function() { + if (this.select.val() !== this.addNewValue) { + return; + } + this.select.val(this.previousValue); + }, + + currentTariffValue: function() { + var value = this.select.val(); + return value === this.addNewValue ? BLANK_VALUE : value; + } +}; diff --git a/sparkmeter/meter/js/meter-tariff-modal.spec.js b/sparkmeter/meter/js/meter-tariff-modal.spec.js new file mode 100644 index 0000000..976f503 --- /dev/null +++ b/sparkmeter/meter/js/meter-tariff-modal.spec.js @@ -0,0 +1,447 @@ +// -*- coding: utf-8 -*- +// Copyright © 2013-2026 EarthSpark International Corp. +// SPDX-License-Identifier: Apache-2.0 +// +/* global afterEach,beforeEach,describe,expect,it,jest */ +'use strict'; + +const MeterTariffModal = require('meter/js/meter-tariff-modal.js'); + +const ADD_NEW = '__add_new__'; +const BLANK = '__None'; +const MODAL_URL = '/tariff/add-modal'; + +const TARIFF_ID = '4a2d8cf2-3d61-4a3f-9c4e-0f0f9b1f6c11'; + +// Cut-down stand-in for the fragment /tariff/add-modal returns: the form, the +// params element TariffForm reads, one type toggle with its section, and the +// block-rate editor modal the shared partial emits inside the fragment. +function fragment() { + return ( + '' + + '
' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '
flat price
' + + '
' + + '
' + + '
' + + ' ' + + ' ' + + '
flat load limit
' + + '
' + + '
' + + '
' + + ' ' + + '
' + + ' ' + + '
plan price
' + + '
plan fixed fee
' + + ' ' + + '
value
' + + '
reset hour
' + + ' ' + + ' ' + + ' ' + + '
' + ); +} + +const LOGIN_PAGE = '
'; + +function pageHtml() { + return ( + '
' + + '
' + + '
' + + ' ' + + '
' + ); +} + +describe('MeterTariffModal', () => { + let getDeferred; + let ajaxDeferred; + + beforeEach(() => { + document.body.innerHTML = pageHtml(); + document.body.className = ''; + + // Widget plugins are attached by startup.js in the browser; the modal + // only ever calls them, so no-ops are enough here. + ['select2', 'numeric', 'iButton', 'datetimepicker'].forEach((plugin) => { + $.fn[plugin] = jest.fn(function() { return this; }); + }); + + getDeferred = $.Deferred(); + ajaxDeferred = $.Deferred(); + $.get = jest.fn(() => getDeferred.promise()); + $.ajax = jest.fn(() => ajaxDeferred.promise()); + }); + + afterEach(() => { + $(document).off('.metertariff'); + $(document).off('.tariffform'); + document.body.innerHTML = ''; + }); + + function open() { + const modal = new MeterTariffModal.MeterTariffModal(); + $('select#tariff').val(ADD_NEW).trigger('change'); + return modal; + } + + function openWithFragment() { + const modal = open(); + getDeferred.resolve(fragment()); + return modal; + } + + function alerts() { + return $('.alerts').text(); + } + + it('appends the add-new option with its decoded label', () => { + new MeterTariffModal.MeterTariffModal(); + + const option = $('select#tariff option[value="' + ADD_NEW + '"]'); + expect(option.length).toBe(1); + expect(option.text()).toBe(''); + }); + + it('does nothing on a page without the params element', () => { + $('#meter-tariff-modal-params').remove(); + new MeterTariffModal.MeterTariffModal(); + + expect($('#meter-tariff-modal').length).toBe(0); + }); + + it('loads the fragment into the modal when add-new is picked', () => { + openWithFragment(); + + expect($.get).toHaveBeenCalledWith(MODAL_URL); + expect($('#meter-tariff-modal form#tariff-modal-form').length).toBe(1); + }); + + describe('tariff form behavior inside the modal (F2)', () => { + it('reveals the block-rate section when block rate is selected', () => { + openWithFragment(); + + expect($('#meter-tariff-modal div.tariff_type.blockrate').hasClass('hide')).toBe(true); + + $('#meter-tariff-modal input:radio[value="blockrate"]').prop('checked', true).trigger('change'); + + expect($('#meter-tariff-modal div.tariff_type.blockrate').hasClass('hide')).toBe(false); + expect($('#meter-tariff-modal div.tariff_type.flat').hasClass('hide')).toBe(true); + }); + + it('reveals the scheduled load limit section', () => { + openWithFragment(); + + $('#meter-tariff-modal input:radio[name="load_limit_type"][value="scheduled"]') + .prop('checked', true).trigger('change'); + + expect($('#meter-tariff-modal div.load_limit_type.scheduled').hasClass('hide')).toBe(false); + expect($('#meter-tariff-modal div.load_limit_type.flat').hasClass('hide')).toBe(true); + }); + + [ + ['tou_enabled', '.tou'], + ['plan_enabled', '.plan-price'], + ['plan_enabled', '.plan-fixed-fee'], + ['daily_energy_limit_enabled', '.daily-energy-limit-value'], + ['daily_energy_limit_enabled', '.daily-energy-limit-reset-hour'] + ].forEach((toggle) => { + const checkbox = toggle[0]; + const section = toggle[1]; + + it('reveals ' + section + ' when ' + checkbox + ' is checked', () => { + openWithFragment(); + + $('#meter-tariff-modal #' + checkbox).prop('checked', true).trigger('change'); + + expect($('#meter-tariff-modal ' + section).hasClass('hide')).toBe(false); + }); + }); + + it('populates the hidden collection field from the editor', () => { + openWithFragment(); + + $('#blockrateModal input#id').val('a-block-rate'); + $('#blockrateModal input#lower').val('0'); + $('#blockrateModal input#upper').val('20'); + $('#blockrateModal input#value').val('1.5'); + $('#blockrateModal button#save').trigger('click'); + + expect($('#meter-tariff-modal #tariff-blockrates tbody tr').length).toBe(1); + const blockrates = JSON.parse($('#meter-tariff-modal #blockrates').val()); + expect(blockrates).toEqual([{id: 'a-block-rate', lower: 0, upper: 20, value: 1.5}]); + }); + + it('binds the toggles once when the fragment is re-rendered', () => { + openWithFragment(); + + // A 400 re-renders the fragment; the delegated handlers must not + // stack up on `document`. + const handlers = () => $._data(document, 'events').change.length; + const before = handlers(); + $('#meter-tariff-modal-save').trigger('click'); + ajaxDeferred.reject({status: 400, responseText: fragment()}); + + expect(handlers()).toBe(before); + }); + }); + + describe('nested editors (F5)', () => { + it('moves the collection editors out of the tariff modal', () => { + openWithFragment(); + + ['blockrateModal', 'touModal', 'loadLimitModal'].forEach((id) => { + expect($('#meter-tariff-modal #' + id).length).toBe(0); + expect($('body').children('#' + id).length).toBe(1); + }); + }); + + it('drops editors from a previous render instead of stacking them', () => { + openWithFragment(); + $('#meter-tariff-modal-save').trigger('click'); + ajaxDeferred.reject({status: 400, responseText: fragment()}); + + expect($('body').children('#blockrateModal').length).toBe(1); + }); + + it('leaves an editor it did not relocate alone', () => { + openWithFragment(); + // The ids come from a shared partial, so anything else on the page + // may carry them too. + $('body').append(''); + + $('#meter-tariff-modal-save').trigger('click'); + ajaxDeferred.reject({status: 400, responseText: fragment()}); + + expect($('body').children('#touModal[data-owner="page"]').length).toBe(1); + }); + + it('keeps the body scroll lock while the tariff modal is still open', () => { + openWithFragment(); + $('#blockrateModal').modal('show'); + + $('#blockrateModal').modal('hide'); + + expect($(document.body).hasClass('modal-open')).toBe(true); + }); + }); + + describe('stacked modals (F5)', () => { + beforeEach(() => { + // The backdrop does not exist yet when the module reacts to + // `show.bs.modal`, so it is placed from a zero-delay timeout. + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + function zIndexes(selector) { + return $(selector).map(function() { return this.style.zIndex; }).get(); + } + + function openEditorOnTop() { + openWithFragment(); + jest.runAllTimers(); + $('#blockrateModal').modal('show'); + jest.runAllTimers(); + } + + it('raises an editor opened on top of the tariff modal above it', () => { + openEditorOnTop(); + + expect(zIndexes('#meter-tariff-modal')).toEqual(['1050']); + expect(zIndexes('#blockrateModal')).toEqual(['1070']); + }); + + it('gives each stacked modal its own backdrop just underneath it', () => { + openEditorOnTop(); + + // Document order: the tariff modal's backdrop, then the editor's. + expect(zIndexes('.modal-backdrop')).toEqual(['1040', '1060']); + }); + + it('leaves a backdrop that was already placed where it is', () => { + openEditorOnTop(); + $('#blockrateModal').modal('hide'); + $('#touModal').modal('show'); + jest.runAllTimers(); + + expect(zIndexes('.modal-backdrop')).toEqual(['1040', '1060']); + }); + }); + + describe('submitting (F4, F8)', () => { + it('submits through AJAX when the form is submitted with Enter', () => { + openWithFragment(); + + const event = $.Event('submit'); + $('#tariff-modal-form').trigger(event); + + expect(event.isDefaultPrevented()).toBe(true); + expect($.ajax).toHaveBeenCalledTimes(1); + expect($.ajax.mock.calls[0][0].url).toBe(MODAL_URL); + expect($.ajax.mock.calls[0][0].method).toBe('POST'); + }); + + it('refuses a second request while one is in flight', () => { + openWithFragment(); + + $('#meter-tariff-modal-save').trigger('click'); + $('#meter-tariff-modal-save').trigger('click'); + $('#tariff-modal-form').trigger('submit'); + + expect($.ajax).toHaveBeenCalledTimes(1); + expect($('#meter-tariff-modal-save').prop('disabled')).toBe(true); + expect($('#meter-tariff-modal-save').text()).toBe('Saving...'); + }); + + it('adds the created tariff and selects it', () => { + openWithFragment(); + $('#meter-tariff-modal-save').trigger('click'); + + ajaxDeferred.resolve({ + message: 'Tariff created.', + tariff: {id: TARIFF_ID, name: 'MODAL TARIFF'} + }); + + const option = $('select#tariff option[value="' + TARIFF_ID + '"]'); + expect(option.text()).toBe('MODAL TARIFF'); + // The created tariff sorts before the add-new option. + expect(option.next().val()).toBe(ADD_NEW); + expect($('select#tariff').val()).toBe(TARIFF_ID); + expect(alerts()).toContain('Tariff created.'); + expect($('#meter-tariff-modal-save').prop('disabled')).toBe(false); + }); + + it('ignores a response that arrives after the modal was dismissed', () => { + openWithFragment(); + $('#meter-tariff-modal-save').trigger('click'); + + $('#meter-tariff-modal').trigger( + $.Event('hidden.bs.modal', {target: $('#meter-tariff-modal')[0]}) + ); + ajaxDeferred.resolve({tariff: {id: TARIFF_ID, name: 'MODAL TARIFF'}}); + + expect($('select#tariff option[value="' + TARIFF_ID + '"]').length).toBe(0); + expect($('select#tariff').val()).toBe(BLANK); + }); + + it('re-renders the fragment with its errors on a 400', () => { + openWithFragment(); + $('#meter-tariff-modal-save').trigger('click'); + + const invalid = fragment().replace( + 'Please set a name for this tariff { + openWithFragment(); + $('#meter-tariff-modal-save').trigger('click'); + + ajaxDeferred.reject({status: 500, responseText: ''}); + + expect(alerts()).toContain('Could not save the tariff.'); + }); + }); + + describe('expired session (F6)', () => { + it('does not inject the login page the GET redirected to', () => { + open(); + getDeferred.resolve(LOGIN_PAGE); + + expect($('#meter-tariff-modal #login_user_form').length).toBe(0); + expect(alerts()).toContain('Session expired.'); + expect($('select#tariff').val()).toBe(BLANK); + }); + + it('does not throw when the POST comes back as a login page', () => { + openWithFragment(); + $('#meter-tariff-modal-save').trigger('click'); + + ajaxDeferred.resolve(LOGIN_PAGE); + + expect(alerts()).toContain('Session expired.'); + expect($('select#tariff').val()).toBe(BLANK); + }); + + it('reports a failed load', () => { + open(); + getDeferred.reject(); + + expect(alerts()).toContain('Could not load the tariff form.'); + expect($('select#tariff').val()).toBe(BLANK); + }); + }); + + describe('restoring the selection (F7)', () => { + it('restores the blank option when nothing was selected', () => { + open(); + getDeferred.reject(); + + const select = $('select#tariff')[0]; + expect(select.value).toBe(BLANK); + expect(select.selectedIndex).toBe(0); + }); + + it('restores the previously selected tariff', () => { + const existing = '11111111-1111-1111-1111-111111111111'; + new MeterTariffModal.MeterTariffModal(); + $('select#tariff').val(existing).trigger('focusin').trigger('change'); + + $('select#tariff').val(ADD_NEW).trigger('focusin').trigger('change'); + getDeferred.resolve(fragment()); + $('#meter-tariff-modal').trigger( + $.Event('hidden.bs.modal', {target: $('#meter-tariff-modal')[0]}) + ); + + expect($('select#tariff').val()).toBe(existing); + }); + }); +}); diff --git a/sparkmeter/meter/meterform.py b/sparkmeter/meter/meterform.py index 666bc54..bf42e42 100644 --- a/sparkmeter/meter/meterform.py +++ b/sparkmeter/meter/meterform.py @@ -10,7 +10,7 @@ from markupsafe import Markup from werkzeug.utils import redirect from wtforms.fields import BooleanField, SelectField, SelectMultipleField, StringField, SubmitField, TelField -from wtforms.validators import ValidationError +from wtforms.validators import StopValidation, ValidationError from wtforms_sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField from sparkmeter.config.configdict import config @@ -71,6 +71,24 @@ def iter_choices(self): yield (pk, self.get_label(obj), self.get_label(obj) in self.data, {}) +class TariffSelectField(QuerySelectField): + """Tariff select that reports the modal's add-new sentinel itself. + + ``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``, so the + sentinel would collect that message on top of the accurate one. Raising + ``StopValidation`` instead carries the accurate message and ends the chain, + leaving the field with exactly one error. + """ + + def pre_validate(self, form): + """Reject the add-new sentinel before the choice lookup can mislabel it.""" + if self.raw_data and self.raw_data[0] == form.ADD_NEW_TARIFF: + raise StopValidation(_("No tariff was created. Please select a tariff or add a new one.")) + super(TariffSelectField, self).pre_validate(form) + + class BaseMeterForm(BaseForm): """Base Meter Form.""" @@ -78,11 +96,17 @@ class BaseMeterForm(BaseForm): mode = None template_filename = "meter-form.html" - tariff = QuerySelectField( + #: Value of the tariff select's "add a new tariff" option. The option is + #: added by the browser and opens the tariff modal instead of selecting a + #: tariff, so it is never a valid submitted value. + ADD_NEW_TARIFF = "__add_new__" + + tariff = TariffSelectField( _("Tariff"), query_factory=lambda: Tariff.query.filter().order_by("name"), get_label="name", - allow_blank=False, + allow_blank=True, + blank_text=_("Select a tariff"), ) customer_name = StringField(_("Name"), default="new customer") customer_code = StringField(_("Code")) @@ -151,6 +175,16 @@ def validate_customer_national_number(self, field): ) ) + def validate_tariff(self, field): + """Require a tariff with a clear validation message. + + The field only exists on customer meters; ``__init__`` deletes it for + totalizers. The add-new sentinel never reaches here: the field's own + ``pre_validate`` reports it and stops the chain. + """ + if field.data is None: + raise ValidationError(_("Please select a tariff or add a new one.")) + def save(self, view): """Save content of meter form to database.""" if self.meter_type == Meter.TYPE_CUSTOMER and self.customer_national_number.data: diff --git a/sparkmeter/meter/templates/meter-form.html b/sparkmeter/meter/templates/meter-form.html index c5644ec..99fc1aa 100644 --- a/sparkmeter/meter/templates/meter-form.html +++ b/sparkmeter/meter/templates/meter-form.html @@ -1,3 +1,4 @@ +{%- set page_name = "meter-form" -%} {%- if form.mode == 'edit' -%} {%- set title = _('Edit %(meter)s', meter=meter.title()) -%} {%- else -%} @@ -47,6 +48,12 @@ {% call render_box(title, box_content_class=box_content_class) %}
{{- form.hidden_tag() -}} + {{ form_errors(form, hiddens='only') }} + {% if form.errors %} +
+ {{ _('Please correct the highlighted fields and try again.') }} +
+ {% endif %} {%- if form.mode == 'add' -%} {{ addon_form_field(form.serial, form_type="horizontal", horizontal_columns=('lg', 2, 10)) }} {%- endif -%} @@ -58,12 +65,34 @@ {%- if meter_type == 'customer' -%} -
+ {#- The modal's behavior lives in meter/js/meter-tariff-modal.js; its + configuration and user-facing strings are carried here. `forceescape` + is required: flask-babel's LazyString defines `__html__`, so a + translated string is otherwise emitted without escaping and a quote + or an angle bracket in a translation would break the attribute. -#} +
+
+
{{ form.tariff.label(class="control-label col-lg-2") }}
{{ form.tariff(class="form-control") }}
+
+ {{ form_error(form.tariff) }} +
{%- endif -%}
diff --git a/sparkmeter/meter/tests/test_meterviews.py b/sparkmeter/meter/tests/test_meterviews.py index a5b76de..aa49562 100644 --- a/sparkmeter/meter/tests/test_meterviews.py +++ b/sparkmeter/meter/tests/test_meterviews.py @@ -244,6 +244,50 @@ def test_add_duplicated_serial(self, client): assert "Meter serial SM15R-01-0000007B already exists." in response.text self.verify_response(response) + def test_add_customer_meter_tariff_select(self, client): + """The select starts blank and offers the modal, which drives the form's markup.""" + TariffFactory(name="TARIFF") + self.session.commit() + + response = client.get("/meter/add-meter") + + # QuerySelectField(allow_blank=True) renders the blank option with this + # value; the modal's JavaScript restores the select to it. + assert '' in response.text + assert 'data-add-new-value="__add_new__"' in response.text + assert 'data-add-new-label="<Add New>"' in response.text + + def test_add_customer_meter_without_tariff(self, client): + """A customer meter needs a tariff, and says so in the field's error markup.""" + data = { + "serial": "SM15R-01-0000007B", + "state": 0, + } + + response = client.post("/meter/add-meter", data=data) + + assert response.status_code == http.client.OK + assert "Please select a tariff or add a new one." in response.text + assert "has-error" in response.text + assert not list(self.ground.get_meters()) + + def test_add_customer_meter_with_add_new_sentinel(self, client): + """The add-new option is a client-side sentinel, never a submittable tariff.""" + data = { + "serial": "SM15R-01-0000007B", + "state": 0, + "tariff": "__add_new__", + } + + response = client.post("/meter/add-meter", data=data) + + assert response.status_code == http.client.OK + assert "No tariff was created. Please select a tariff or add a new one." in response.text + # The sentinel resolves to no tariff, so the choice lookup would + # otherwise stack its own message on top of the accurate one. + assert "Not a valid choice" not in response.text + assert not list(self.ground.get_meters()) + def test_add_unknown_model(self, client): tariff = TariffFactory() self.session.commit() diff --git a/sparkmeter/tariff/js/tariff-form.js b/sparkmeter/tariff/js/tariff-form.js index e9b6c9a..7fe0d23 100644 --- a/sparkmeter/tariff/js/tariff-form.js +++ b/sparkmeter/tariff/js/tariff-form.js @@ -11,16 +11,23 @@ function TariffForm() { exports.TariffForm = TariffForm; +// Namespace for the delegated handlers below, so that re-initializing the form +// -- which the tariff modal does every time it renders the fragment -- rebinds +// them instead of stacking a second copy on `document`. +var EVENTS = '.tariffform'; + TariffForm.prototype = { _init: function() { - $(document).on('change', 'input:radio[id^="tariff_type"]', function(event) { + $(document).off(EVENTS); + + $(document).on('change' + EVENTS, 'input:radio[id^="tariff_type"]', function(event) { $("div.tariff_type").addClass("hide"); $("div.tariff_type." + $(this).val()).removeClass("hide"); $("input#tariff_type[value='flat']").attr('checked', $(this).val() === 'flat'); $("input#tariff_type[value='blockrate']").attr('checked', $(this).val() === 'blockrate'); }); - $(document).on('change', 'input:radio[id^="load_limit_type"]', function(event) { + $(document).on('change' + EVENTS, 'input:radio[id^="load_limit_type"]', function(event) { $("div.load_limit_type").addClass("hide"); $("div.load_limit_type." + $(this).val()).removeClass("hide"); $("input#load_limit_type[value='flat']").attr('checked', $(this).val() === 'flat'); @@ -32,7 +39,7 @@ TariffForm.prototype = { } }); - $(document).on('change', 'input:checkbox[id^="tou_enabled"]', function(event) { + $(document).on('change' + EVENTS, 'input:checkbox[id^="tou_enabled"]', function(event) { if (this.checked) { $(".tou").removeClass("hide"); } else { @@ -40,7 +47,7 @@ TariffForm.prototype = { } }); - $(document).on('change', 'input:checkbox[id^="plan_enabled"]', function(event) { + $(document).on('change' + EVENTS, 'input:checkbox[id^="plan_enabled"]', function(event) { if (this.checked) { $(".plan-price").removeClass("hide"); $(".plan-fixed-fee").removeClass("hide"); @@ -50,7 +57,7 @@ TariffForm.prototype = { } }); - $(document).on('change', 'input:checkbox[id^="daily_energy_limit_enabled"]', function(event) { + $(document).on('change' + EVENTS, 'input:checkbox[id^="daily_energy_limit_enabled"]', function(event) { if (this.checked) { $(".daily-energy-limit-reset-hour").removeClass("hide"); $(".daily-energy-limit-value").removeClass("hide"); diff --git a/sparkmeter/tariff/tariffview.py b/sparkmeter/tariff/tariffview.py index 797cf59..1c4594f 100644 --- a/sparkmeter/tariff/tariffview.py +++ b/sparkmeter/tariff/tariffview.py @@ -9,22 +9,31 @@ from flask.globals import request from flask.helpers import flash, url_for from flask.templating import render_template +from flask.wrappers import Response from flask_babel import lazy_gettext as _ from markupsafe import Markup from werkzeug.exceptions import abort from werkzeug.utils import redirect from sparkmeter.misc.htmlutils import build_link +from sparkmeter.misc.jsonutils import jsonify from sparkmeter.tariff.tariffdomain import Tariff from sparkmeter.tariff.tariffform import TariffForm from sparkmeter.tariff.tariffutils import add_tariff_from_form, update_tariff_from_form from sparkmeter.web.blueprint import AuthBlueprint +from sparkmeter.web.forms import set_form_errors_header from sparkmeter.web.permission import verify_permission logger = logging.getLogger(__name__) tariff = AuthBlueprint("tariff", __name__) +def render_modal_form(form, status=http.client.OK): + """Render the tariff modal form with optional validation metadata.""" + body = render_template("tariff-modal-form.html", form=form) + return set_form_errors_header(Response(body, status=status), form) + + @tariff.route("/tariff/") @verify_permission("tariff", "view") def index(): @@ -60,6 +69,25 @@ def add(): return form.render(mode="add") +@tariff.route("/tariff/add-modal", methods=["GET", "POST"]) +@verify_permission("tariff", "add") +def add_modal(): + """Add tariff form rendered for the meter modal workflow.""" + form = TariffForm(request.form) + if request.method == "POST": + tariff = add_tariff_from_form(form) + if tariff: + return jsonify( + message=_("Tariff created."), + tariff={ + "id": tariff.id, + "name": tariff.name, + }, + ) + return render_modal_form(form, status=http.client.BAD_REQUEST) + return render_modal_form(form) + + @tariff.route("/tariff//edit", methods=["GET", "POST"]) @verify_permission("tariff", "edit") def edit(tariff_id): diff --git a/sparkmeter/tariff/templates/tariff-form.html b/sparkmeter/tariff/templates/tariff-form.html index 7bff1d7..4cfdb1e 100644 --- a/sparkmeter/tariff/templates/tariff-form.html +++ b/sparkmeter/tariff/templates/tariff-form.html @@ -37,244 +37,12 @@ {% endblock breadcrumbs %} -{% macro form_tariff_error(form, field, class_=None) %} - {% if form.errors[field] %} -
- - - {% for error in form.errors[field] %} - {{ error }} - {% endfor %} - -
- {% endif %} -{% endmacro %} - {% block content %} -
-
{% call render_box(title, box_content_class="padded") %} - {{ form.hidden_tag() }} - - {#- Tariff.name|e #} -
- {{ form.name.label(class_="control-label col-lg-2") }} -
- {{ form.name(class_="form-control") }} -
-
- {{ form_tariff_error(form, 'name') }} - - {# tariff plan duration and start day #} -
- {{ form.plan_duration_and_start_day.label(class_="control-label col-lg-2") }} -
- {{ form.plan_duration_and_start_day(class_="form-control numeric no-negative") }} -
-
- {{ form_tariff_error(form, 'cycle_start_day_of_month', class_='form-group cycle-start-day-of-month') }} - - {#- radio for load limit type, active depending on form data #} -
- {{ form.load_limit_type.label(class_="control-label col-lg-2") }} -
- - -
-
- {{ form_tariff_error(form, 'load_limit_type') }} - - {#- flat load limit text input, only visible when radio is active #} -
- {{ form.flat_load_limit.label(class_="control-label col-lg-2") }} -
- {{ form.flat_load_limit(class_="form-control numeric no-negative") }} - {{ _('in watts', currency=config.CURRENCY) }} -
-
- {% set extra = '' %} - {% if form.data.load_limit_type != 'flat' %} - {% set extra = ' hide' %} - {% endif %} - {{ form_tariff_error(form, 'flat_load_limit', class_='form-group load_limit_type' + extra) }} - - {#- scheduled load limit text input, only visible when radio is active #} -
- -
- {% include "tariff-load-limits-list.html" %} -
-
- {% set extra = '' %} - {% if form.data.load_limit_type != 'scheduled' %} - {% set extra = ' hide' %} - {% endif %} - {{ form_tariff_error(form, 'load_limits', class_='form-group load-limits' + extra) }} - - {#- Low balance threshold #} -
- {{ form.low_balance_threshold.label(class_="control-label col-lg-2") }} -
- {{ form.low_balance_threshold(class_="form-control numeric") }} - {{ _('in %(currency)s', currency=config.CURRENCY) }} -
-
- {{ form_tariff_error(form, 'low_balance_threshold') }} - - {#- Monthly Plan, checkbox/iButton for enabling it #} -
- {{ form.plan_enabled.label(class_="control-label col-lg-2") }} -
-
- -
-
-
- {{ form_tariff_error(form, 'plan_enabled') }} - - {# Hide some fields when plan is disabled #} - {% set extra = '' %} - {% if not form.data.plan_enabled %} - {% set extra = ' hide' %} - {% endif %} - -
- {{ form.plan_fixed_fee.label(class_="control-label col-lg-2") }} -
- {{ form.plan_fixed_fee(class_="form-control numeric no-negative") }} - {{ _('in %(currency)s', currency=config.CURRENCY) }} -
-
- {{ form_tariff_error(form, 'plan_fixed_fee', class_='form-group plan-fixed-fee' + extra) }} - -
- {{ form.plan_price.label(class_="control-label col-lg-2") }} -
- {{ form.plan_price(class_="form-control numeric no-negative") }} - {{ _('in %(currency)s', currency=config.CURRENCY) }} -
-
- {{ form_tariff_error(form, 'plan_price', class_='form-group plan-price' + extra) }} - - - {#- radio for tariff type, active depending on form data #} -
- {{ form.tariff_type.label(class_="control-label col-lg-2") }} -
- - -
-
- {{ form_tariff_error(form, 'tariff_type') }} - - {#- flat price text input, only visible when radio is active #} -
- {{ form.flat_price.label(class_="control-label col-lg-2") }} -
- {{ form.flat_price(class_="form-control numeric no-negative") }} - {{ _('in %(currency)s per kWh', currency=config.CURRENCY) }} -
-
- {% set extra = '' %} - {% if form.data.tariff_type != 'flat' %} - {% set extra = ' hide' %} - {% endif %} - {{ form_tariff_error(form, 'flat_price', class_='form-group tariff_type' + extra) }} - - {#- blockrate table, only visible when radio is active #} -
- -
- {% include "tariff-blockrate-list.html" %} -
-
- {{ form_tariff_error(form, 'blockrates') }} - - {#- time of use, checkbox/iButton for enabling it #} -
- {{ form.tou_enabled.label(class_="control-label col-lg-2") }} -
-
- -
-
-
- {{ form_tariff_error(form, 'tou_enabled') }} - -
- -
- {% include "tariff-tou-list.html" %} -
-
- {% set extra = '' %} - {% if not form.data.tou_enabled %} - {% set extra = ' hide' %} - {% endif %} - {{ form_tariff_error(form, 'tous', class_='form-group tou' + extra) }} - - {#- daily energy limit, checkbox/iButton for enabling it #} -
- {{ form.daily_energy_limit_enabled.label(class_="control-label col-lg-2") }} -
-
- -
-
-
- {{ form_tariff_error(form, 'daily_energy_limit_enabled') }} - - {# daily energy limit reset hour #} -
- {{ form.daily_energy_limit_reset_hour.label(class_="control-label col-lg-2") }} -
- {{ form.daily_energy_limit_reset_hour(class_="form-control") }} -
-
- - {% if form.data.daily_energy_limit_enabled -%} - {{ form_tariff_error(form, 'daily_energy_limit_reset_hour', class_='form-group daily-energy-limit-reset-hour') }} - {%- endif %} - - {# daily energy limit value #} -
- {{ form.daily_energy_limit_value.label(class_="control-label col-lg-2") }} -
- {{ form.daily_energy_limit_value(class_="form-control numeric no-negative") }} - {{ _('in kWh') }} -
-
- {% if form.data.daily_energy_limit_enabled -%} - {{ form_tariff_error(form, 'daily_energy_limit_value', class_='form-group daily-energy-limit-reset-value') }} - {%- endif %} + {% include "tariff/_tariff-form-fields.html" %} {#- Save #}
diff --git a/sparkmeter/tariff/templates/tariff-modal-form.html b/sparkmeter/tariff/templates/tariff-modal-form.html new file mode 100644 index 0000000..d300e18 --- /dev/null +++ b/sparkmeter/tariff/templates/tariff-modal-form.html @@ -0,0 +1,6 @@ +{%- from "_macros.html" import form_errors -%} + + + {{ form_errors(form, hiddens='only') }} + {% include "tariff/_tariff-form-fields.html" %} + diff --git a/sparkmeter/tariff/templates/tariff/_tariff-form-fields.html b/sparkmeter/tariff/templates/tariff/_tariff-form-fields.html new file mode 100644 index 0000000..bcea382 --- /dev/null +++ b/sparkmeter/tariff/templates/tariff/_tariff-form-fields.html @@ -0,0 +1,217 @@ +{% macro form_tariff_error(form, field, class_=None) %} + {% if form.errors[field] %} +
+ + + {% for error in form.errors[field] %} + {{ error }} + {% endfor %} + +
+ {% endif %} +{% endmacro %} + +
+
+ +{{ form.hidden_tag() }} + +
+ {{ form.name.label(class_="control-label col-lg-2") }} +
+ {{ form.name(class_="form-control") }} +
+
+{{ form_tariff_error(form, 'name') }} + +
+ {{ form.plan_duration_and_start_day.label(class_="control-label col-lg-2") }} +
+ {{ form.plan_duration_and_start_day(class_="form-control numeric no-negative") }} +
+
+{{ form_tariff_error(form, 'cycle_start_day_of_month', class_='form-group cycle-start-day-of-month') }} + +
+ {{ form.load_limit_type.label(class_="control-label col-lg-2") }} +
+ + +
+
+{{ form_tariff_error(form, 'load_limit_type') }} + +
+ {{ form.flat_load_limit.label(class_="control-label col-lg-2") }} +
+ {{ form.flat_load_limit(class_="form-control numeric no-negative") }} + {{ _('in watts', currency=config.CURRENCY) }} +
+
+{% set extra = '' %} +{% if form.data.load_limit_type != 'flat' %} +{% set extra = ' hide' %} +{% endif %} +{{ form_tariff_error(form, 'flat_load_limit', class_='form-group load_limit_type' + extra) }} + +
+ +
+ {% include "tariff-load-limits-list.html" %} +
+
+{% set extra = '' %} +{% if form.data.load_limit_type != 'scheduled' %} +{% set extra = ' hide' %} +{% endif %} +{{ form_tariff_error(form, 'load_limits', class_='form-group load-limits' + extra) }} + +
+ {{ form.low_balance_threshold.label(class_="control-label col-lg-2") }} +
+ {{ form.low_balance_threshold(class_="form-control numeric") }} + {{ _('in %(currency)s', currency=config.CURRENCY) }} +
+
+{{ form_tariff_error(form, 'low_balance_threshold') }} + +
+ {{ form.plan_enabled.label(class_="control-label col-lg-2") }} +
+
+ +
+
+
+{{ form_tariff_error(form, 'plan_enabled') }} + +{% set extra = '' %} +{% if not form.data.plan_enabled %} +{% set extra = ' hide' %} +{% endif %} + +
+ {{ form.plan_fixed_fee.label(class_="control-label col-lg-2") }} +
+ {{ form.plan_fixed_fee(class_="form-control numeric no-negative") }} + {{ _('in %(currency)s', currency=config.CURRENCY) }} +
+
+{{ form_tariff_error(form, 'plan_fixed_fee', class_='form-group plan-fixed-fee' + extra) }} + +
+ {{ form.plan_price.label(class_="control-label col-lg-2") }} +
+ {{ form.plan_price(class_="form-control numeric no-negative") }} + {{ _('in %(currency)s', currency=config.CURRENCY) }} +
+
+{{ form_tariff_error(form, 'plan_price', class_='form-group plan-price' + extra) }} + +
+ {{ form.tariff_type.label(class_="control-label col-lg-2") }} +
+ + +
+
+{{ form_tariff_error(form, 'tariff_type') }} + +
+ {{ form.flat_price.label(class_="control-label col-lg-2") }} +
+ {{ form.flat_price(class_="form-control numeric no-negative") }} + {{ _('in %(currency)s per kWh', currency=config.CURRENCY) }} +
+
+{% set extra = '' %} +{% if form.data.tariff_type != 'flat' %} +{% set extra = ' hide' %} +{% endif %} +{{ form_tariff_error(form, 'flat_price', class_='form-group tariff_type' + extra) }} + +
+ +
+ {% include "tariff-blockrate-list.html" %} +
+
+{{ form_tariff_error(form, 'blockrates') }} + +
+ {{ form.tou_enabled.label(class_="control-label col-lg-2") }} +
+
+ +
+
+
+{{ form_tariff_error(form, 'tou_enabled') }} + +
+ +
+ {% include "tariff-tou-list.html" %} +
+
+{% set extra = '' %} +{% if not form.data.tou_enabled %} +{% set extra = ' hide' %} +{% endif %} +{{ form_tariff_error(form, 'tous', class_='form-group tou' + extra) }} + +
+ {{ form.daily_energy_limit_enabled.label(class_="control-label col-lg-2") }} +
+
+ +
+
+
+{{ form_tariff_error(form, 'daily_energy_limit_enabled') }} + +
+ {{ form.daily_energy_limit_reset_hour.label(class_="control-label col-lg-2") }} +
+ {{ form.daily_energy_limit_reset_hour(class_="form-control") }} +
+
+{% if form.data.daily_energy_limit_enabled -%} +{{ form_tariff_error(form, 'daily_energy_limit_reset_hour', class_='form-group daily-energy-limit-reset-hour') }} +{%- endif %} + +
+ {{ form.daily_energy_limit_value.label(class_="control-label col-lg-2") }} +
+ {{ form.daily_energy_limit_value(class_="form-control numeric no-negative") }} + {{ _('in kWh') }} +
+
+{% if form.data.daily_energy_limit_enabled -%} +{{ form_tariff_error(form, 'daily_energy_limit_value', class_='form-group daily-energy-limit-reset-value') }} +{%- endif %} diff --git a/sparkmeter/tariff/tests/test_tariffviews.py b/sparkmeter/tariff/tests/test_tariffviews.py index 828b9b0..d351070 100644 --- a/sparkmeter/tariff/tests/test_tariffviews.py +++ b/sparkmeter/tariff/tests/test_tariffviews.py @@ -13,10 +13,10 @@ from sparkmeter.event.eventdomain import Event from sparkmeter.meter.meterdomain import MeterConfig from sparkmeter.misc.htmlutils import build_link -from sparkmeter.misc.jsonutils import json_dumps +from sparkmeter.misc.jsonutils import json_dumps, json_loads from sparkmeter.tariff.tariffdomain import Tariff from sparkmeter.tests.base import WebViewTestCaseBase -from sparkmeter.tests.test_data_factory import MeterFactory, TariffFactory +from sparkmeter.tests.test_data_factory import MeterFactory, TariffFactory, VendorFactory @pytest.fixture(scope="module", autouse=True) @@ -55,6 +55,124 @@ def test_add(self, client): response = client.get(path) self.verify_response(response) + def test_add_modal_get(self, client): + path = "/tariff/add-modal" + + response = client.get(path) + assert response.status_code == http.client.OK + assert "X-Form-Errors" not in response.headers + self.verify_response(response) + + def test_add_modal_get_renders_empty_collections(self, client): + """A freshly opened modal must match /tariff/add, which posts back cleanly. + + With no formdata the JSON fields keep ``None``, which renders as + ``null``/``""`` instead of ``[]`` and cannot be posted back. + """ + response = client.get("/tariff/add-modal") + standalone = client.get("/tariff/add") + + for attribute in ('data-blockrates="[]"', 'data-tous="[]"', 'data-load-limits="[]"'): + assert attribute in response.text + assert attribute in standalone.text + + def test_add_modal_untouched_post_is_parseable(self, client): + """Submitting an untouched modal must not post unparseable collections.""" + data = dict(name="", blockrates="[]", tous="[]", load_limits="[]") + + response = client.post("/tariff/add-modal", data=data) + + assert response.status_code == http.client.BAD_REQUEST + errors = json_loads(response.headers["X-Form-Errors"]) + assert "blockrates" not in errors + assert "tous" not in errors + assert "load_limits" not in errors + + def test_add_modal_forbidden_without_permission(self, client, vendor_role): + """The modal endpoint is behind the same tariff:add permission as /tariff/add.""" + client.login_as(VendorFactory(roles=[vendor_role])) + + assert client.get("/tariff/add-modal").status_code == http.client.NOT_FOUND + assert client.post("/tariff/add-modal", data={}).status_code == http.client.NOT_FOUND + + def test_add_modal_post_valid(self, client, config): + path = "/tariff/add-modal" + data = dict( + name="MODAL TARIFF", + flat_load_limit=150, + plan_price=0, + cycle_start_day_of_month=1, + tariff_type="flat", + flat_price=4, + tous="", + ) + + config["HEROKU"] = False + response = client.post(path, data=data) + + assert response.status_code == http.client.OK + body = response.json() + assert body["message"] == "Tariff created." + + tariffs = Tariff.get_all() + assert len(tariffs) == 1 + assert body["tariff"]["name"] == "MODAL TARIFF" + assert body["tariff"]["id"] == str(tariffs[0].id) + + def test_add_modal_post_invalid(self, client): + path = "/tariff/add-modal" + data = dict(name="", flat_load_limit=150, flat_price=4) + + response = client.post(path, data=data) + + assert response.status_code == http.client.BAD_REQUEST + errors = json_loads(response.headers["X-Form-Errors"]) + assert errors["name"] == ["Please set a name for this tariff"] + assert "Please set a name for this tariff" in response.text + assert not Tariff.query.scalar() + + @pytest.mark.parametrize( + "field, data, message", + [ + ( + "blockrates", + dict( + tariff_type=Tariff.TYPE_BLOCKRATE, + blockrates=json_dumps([{"lower": "1", "upper": "20", "value": "1"}]), + ), + "Block rates contain at least one gap, between 0 and 65535", + ), + ( + "tous", + dict( + tou_enabled=True, + tous=json_dumps([{"start": "00:00", "end": "12:00", "value": -100}]), + ), + "The TOU period modifier must be a positive number.", + ), + ( + "load_limits", + dict(load_limit_type=Tariff.LOAD_LIMIT_TYPE_SCHEDULED, load_limits=json_dumps([])), + "Please add some Load limit periods.", + ), + ], + ) + def test_add_modal_post_collection_error(self, client, field, data, message): + """These validators store the raw exception, which is not JSON serializable. + + Reporting them used to raise a TypeError out of the error header and + turn the response into a 500. + """ + path = "/tariff/add-modal" + data = dict(data, name="TARIFF", flat_load_limit=150, flat_price=4) + + response = client.post(path, data=data) + + assert response.status_code == http.client.BAD_REQUEST + errors = json_loads(response.headers["X-Form-Errors"]) + assert errors[field] == [message] + assert not Tariff.query.scalar() + def test_add_form(self, client, config): path = "/tariff/add" diff --git a/sparkmeter/web/forms.py b/sparkmeter/web/forms.py index 84b02cd..17144c3 100644 --- a/sparkmeter/web/forms.py +++ b/sparkmeter/web/forms.py @@ -39,6 +39,31 @@ def getall(self, key): return [self[key]] +def set_form_errors_header(response, form): + """Attach a form's validation errors to a response as ``X-Form-Errors``. + + This is for development, and especially so that unittests can show a nicer + error when there is a form error. + + Every error is coerced to ``str``: validators are free to store exception + instances rather than messages, and those are not JSON serializable. + + :param response: the response to annotate. + :param form: the form whose errors should be reported. + :return: the same response, for convenience. + :rtype: Response + """ + if not form.errors: + return response + + error_dict = {} + for name, errors in list(form.errors.items()): + error_dict[name] = list(map(str, errors)) + response.headers["X-Form-Errors"] = json_dumps(error_dict) + logger.warning("{} errors: {} {}".format(type(form).__name__, error_dict, form.data)) + return response + + class BaseForm(FlaskForm): """Base form, used by all other forms in the application.""" @@ -102,15 +127,7 @@ def render(self, **context): :rtype: Response """ body = render_template(self.template_filename, form=self, **context) - response = Response(body) - if self.errors: - error_dict = {} - for name, errors in list(self.errors.items()): - error_dict[name] = list(map(str, errors)) - response.headers["X-Form-Errors"] = json_dumps(error_dict) - logger.warning("{} errors: {} {}".format(type(self).__name__, error_dict, self.data)) - - return response + return set_form_errors_header(Response(body), self) def flatten_json(form, json, parent_key="", separator="-", skip_unknown_keys=True): # pragma: nocoverage diff --git a/test-data/meter/test_meterviews-MeterViewTest.test_add.page b/test-data/meter/test_meterviews-MeterViewTest.test_add.page index 4140e36..abb8f61 100644 --- a/test-data/meter/test_meterviews-MeterViewTest.test_add.page +++ b/test-data/meter/test_meterviews-MeterViewTest.test_add.page @@ -50,7 +50,7 @@ - +
+
@@ -224,11 +225,28 @@
-
+
+
+
- +
+
+
+
diff --git a/test-data/meter/test_meterviews-MeterViewTest.test_add_32_bit_serial.page b/test-data/meter/test_meterviews-MeterViewTest.test_add_32_bit_serial.page index 4140e36..abb8f61 100644 --- a/test-data/meter/test_meterviews-MeterViewTest.test_add_32_bit_serial.page +++ b/test-data/meter/test_meterviews-MeterViewTest.test_add_32_bit_serial.page @@ -50,7 +50,7 @@ - +
+
@@ -224,11 +225,28 @@
-
+
+
+
- +
+
+
+
diff --git a/test-data/meter/test_meterviews-MeterViewTest.test_add_duplicated_serial.page b/test-data/meter/test_meterviews-MeterViewTest.test_add_duplicated_serial.page index 613b330..e48496a 100644 --- a/test-data/meter/test_meterviews-MeterViewTest.test_add_duplicated_serial.page +++ b/test-data/meter/test_meterviews-MeterViewTest.test_add_duplicated_serial.page @@ -54,7 +54,7 @@ - +
+
+ Please correct the highlighted fields and try again. +
+
@@ -235,11 +239,28 @@
-
+
+
+
- +
+
+
+
diff --git a/test-data/meter/test_meterviews-MeterViewTest.test_add_empty_country_code.page b/test-data/meter/test_meterviews-MeterViewTest.test_add_empty_country_code.page index 81758d6..ebbc860 100644 --- a/test-data/meter/test_meterviews-MeterViewTest.test_add_empty_country_code.page +++ b/test-data/meter/test_meterviews-MeterViewTest.test_add_empty_country_code.page @@ -12,7 +12,7 @@ < Set-Cookie: %% COOKIE %% < Set-Cookie: %% COOKIE %% < Vary: Cookie -< X-Form-Errors: {"state": ["Not a valid choice."],"tariff": ["Not a valid choice"]} +< X-Form-Errors: {"state": ["Not a valid choice."],"tariff": ["Please select a tariff or add a new one."]} < @@ -54,7 +54,7 @@ - +
+
+ Please correct the highlighted fields and try again. +
+
@@ -228,11 +232,29 @@
-
+
+
+
- +
+
+
+

Please select a tariff or add a new one.

+
diff --git a/test-data/meter/test_meterviews-MeterViewTest.test_add_empty_phone_number.page b/test-data/meter/test_meterviews-MeterViewTest.test_add_empty_phone_number.page index e7d6b97..ed3ca53 100644 --- a/test-data/meter/test_meterviews-MeterViewTest.test_add_empty_phone_number.page +++ b/test-data/meter/test_meterviews-MeterViewTest.test_add_empty_phone_number.page @@ -12,7 +12,7 @@ < Set-Cookie: %% COOKIE %% < Set-Cookie: %% COOKIE %% < Vary: Cookie -< X-Form-Errors: {"state": ["Not a valid choice."],"tariff": ["Not a valid choice"]} +< X-Form-Errors: {"state": ["Not a valid choice."],"tariff": ["Please select a tariff or add a new one."]} < @@ -54,7 +54,7 @@ - +
+
+ Please correct the highlighted fields and try again. +
+
@@ -228,11 +232,29 @@
-
+
+
+
- +
+
+
+

Please select a tariff or add a new one.

+
diff --git a/test-data/meter/test_meterviews-MeterViewTest.test_add_from_cloud.page b/test-data/meter/test_meterviews-MeterViewTest.test_add_from_cloud.page index eedfa9d..1ee1f00 100644 --- a/test-data/meter/test_meterviews-MeterViewTest.test_add_from_cloud.page +++ b/test-data/meter/test_meterviews-MeterViewTest.test_add_from_cloud.page @@ -50,7 +50,7 @@ - +
+
@@ -224,11 +225,28 @@
-
+
+
+
- +
+
+
+
diff --git a/test-data/meter/test_meterviews-MeterViewTest.test_add_invalid_phone_number.page b/test-data/meter/test_meterviews-MeterViewTest.test_add_invalid_phone_number.page index 71352a4..f9f47ef 100644 --- a/test-data/meter/test_meterviews-MeterViewTest.test_add_invalid_phone_number.page +++ b/test-data/meter/test_meterviews-MeterViewTest.test_add_invalid_phone_number.page @@ -12,7 +12,7 @@ < Set-Cookie: %% COOKIE %% < Set-Cookie: %% COOKIE %% < Vary: Cookie -< X-Form-Errors: {"customer_national_number": ["1 is not a valid national phone number for Brazil"],"state": ["Not a valid choice."],"tariff": ["Not a valid choice"]} +< X-Form-Errors: {"customer_national_number": ["1 is not a valid national phone number for Brazil"],"state": ["Not a valid choice."],"tariff": ["Please select a tariff or add a new one."]} < @@ -54,7 +54,7 @@ - +
+
+ Please correct the highlighted fields and try again. +
+
@@ -228,11 +232,29 @@
-
+
+
+
- +
+
+
+

Please select a tariff or add a new one.

+
diff --git a/test-data/meter/test_meterviews-MeterViewTest.test_add_invalid_serial-invalid-serial.page b/test-data/meter/test_meterviews-MeterViewTest.test_add_invalid_serial-invalid-serial.page index 8c01146..eae472e 100644 --- a/test-data/meter/test_meterviews-MeterViewTest.test_add_invalid_serial-invalid-serial.page +++ b/test-data/meter/test_meterviews-MeterViewTest.test_add_invalid_serial-invalid-serial.page @@ -54,7 +54,7 @@ - +
+
+ Please correct the highlighted fields and try again. +
+
@@ -235,11 +239,28 @@
-
+
+
+
- +
+
+
+
diff --git a/test-data/meter/test_meterviews-MeterViewTest.test_add_invalid_serial.page b/test-data/meter/test_meterviews-MeterViewTest.test_add_invalid_serial.page index 11c5c37..03edcf5 100644 --- a/test-data/meter/test_meterviews-MeterViewTest.test_add_invalid_serial.page +++ b/test-data/meter/test_meterviews-MeterViewTest.test_add_invalid_serial.page @@ -9,7 +9,7 @@ < HTTP/1.1 200 OK < Content-Type: text/html; charset=utf-8 -< X-Form-Errors: {"serial": ["Invalid meter serial, must look like \"SMXXX-XX-XXXXXXXX\"."],"state": ["Not a valid choice."],"tariff": ["Not a valid choice"]} +< X-Form-Errors: {"serial": ["Invalid meter serial, must look like \"SMXXX-XX-XXXXXXXX\"."],"state": ["Not a valid choice."],"tariff": ["Please select a tariff or add a new one."]} < @@ -51,7 +51,7 @@ - +
+
+ Please correct the highlighted fields and try again. +
+
@@ -232,11 +236,29 @@
-
+
+
+
- +
+
+
+

Please select a tariff or add a new one.

+
diff --git a/test-data/meter/test_meterviews-MeterViewTest.test_add_totalizer.page b/test-data/meter/test_meterviews-MeterViewTest.test_add_totalizer.page index 7ae9ec3..9e52a23 100644 --- a/test-data/meter/test_meterviews-MeterViewTest.test_add_totalizer.page +++ b/test-data/meter/test_meterviews-MeterViewTest.test_add_totalizer.page @@ -50,7 +50,7 @@ - +
+
diff --git a/test-data/meter/test_meterviews-MeterViewTest.test_add_totalizer_from_cloud.page b/test-data/meter/test_meterviews-MeterViewTest.test_add_totalizer_from_cloud.page index cf901c9..e2d461e 100644 --- a/test-data/meter/test_meterviews-MeterViewTest.test_add_totalizer_from_cloud.page +++ b/test-data/meter/test_meterviews-MeterViewTest.test_add_totalizer_from_cloud.page @@ -50,7 +50,7 @@ - +
+
diff --git a/test-data/meter/test_meterviews-MeterViewTest.test_add_unknown_model.page b/test-data/meter/test_meterviews-MeterViewTest.test_add_unknown_model.page index f8997c5..32b4b60 100644 --- a/test-data/meter/test_meterviews-MeterViewTest.test_add_unknown_model.page +++ b/test-data/meter/test_meterviews-MeterViewTest.test_add_unknown_model.page @@ -54,7 +54,7 @@ - +
+
+ Please correct the highlighted fields and try again. +
+
@@ -235,11 +239,28 @@
-
+
+
+
- +
+
+
+
diff --git a/test-data/meter/test_meterviews-MeterViewTest.test_edit_customer_state_cloud-customer-state-cloud.page b/test-data/meter/test_meterviews-MeterViewTest.test_edit_customer_state_cloud-customer-state-cloud.page index 5f1c239..ea27690 100644 --- a/test-data/meter/test_meterviews-MeterViewTest.test_edit_customer_state_cloud-customer-state-cloud.page +++ b/test-data/meter/test_meterviews-MeterViewTest.test_edit_customer_state_cloud-customer-state-cloud.page @@ -50,7 +50,7 @@ - +
-
+ +
-
+
+
+
- +
+
+
+
diff --git a/test-data/meter/test_meterviews-MeterViewTest.test_edit_customer_state_ground-customer-state-ground.page b/test-data/meter/test_meterviews-MeterViewTest.test_edit_customer_state_ground-customer-state-ground.page index 5f1c239..ea27690 100644 --- a/test-data/meter/test_meterviews-MeterViewTest.test_edit_customer_state_ground-customer-state-ground.page +++ b/test-data/meter/test_meterviews-MeterViewTest.test_edit_customer_state_ground-customer-state-ground.page @@ -50,7 +50,7 @@ - +
-
+ +
-
+
+
+
- +
+
+
+
diff --git a/test-data/meter/test_meterviews-MeterViewTest.test_edit_customer_tariff_cloud-customer-tariff-cloud.page b/test-data/meter/test_meterviews-MeterViewTest.test_edit_customer_tariff_cloud-customer-tariff-cloud.page index 0cbb242..6ee394a 100644 --- a/test-data/meter/test_meterviews-MeterViewTest.test_edit_customer_tariff_cloud-customer-tariff-cloud.page +++ b/test-data/meter/test_meterviews-MeterViewTest.test_edit_customer_tariff_cloud-customer-tariff-cloud.page @@ -50,7 +50,7 @@ - +
-
+ +
-
+
+
+
- +
+
+
+
diff --git a/test-data/meter/test_meterviews-MeterViewTest.test_edit_totalizer.page b/test-data/meter/test_meterviews-MeterViewTest.test_edit_totalizer.page index f5316f3..897127c 100644 --- a/test-data/meter/test_meterviews-MeterViewTest.test_edit_totalizer.page +++ b/test-data/meter/test_meterviews-MeterViewTest.test_edit_totalizer.page @@ -50,7 +50,7 @@ - +
-
+ +
diff --git a/test-data/meter/test_meterviews-MeterViewTest.test_edit_with_tags-prepopulate.page b/test-data/meter/test_meterviews-MeterViewTest.test_edit_with_tags-prepopulate.page index a53ec3d..0e67388 100644 --- a/test-data/meter/test_meterviews-MeterViewTest.test_edit_with_tags-prepopulate.page +++ b/test-data/meter/test_meterviews-MeterViewTest.test_edit_with_tags-prepopulate.page @@ -49,7 +49,7 @@ - +
-
+ +
-
+
+
+
- +
+
+
+
diff --git a/test-data/meter/test_meterviews-MeterViewTest.test_edit_with_tags.page b/test-data/meter/test_meterviews-MeterViewTest.test_edit_with_tags.page index fd62bce..41b3b26 100644 --- a/test-data/meter/test_meterviews-MeterViewTest.test_edit_with_tags.page +++ b/test-data/meter/test_meterviews-MeterViewTest.test_edit_with_tags.page @@ -50,7 +50,7 @@ - +
-
+ +
-
+
+
+
- +
+
+
+
diff --git a/test-data/meter/test_meterviews-MeterViewTest.test_unknown_server_error.page b/test-data/meter/test_meterviews-MeterViewTest.test_unknown_server_error.page index da31037..7d37cdb 100644 --- a/test-data/meter/test_meterviews-MeterViewTest.test_unknown_server_error.page +++ b/test-data/meter/test_meterviews-MeterViewTest.test_unknown_server_error.page @@ -54,7 +54,7 @@ - +
+
+ Please correct the highlighted fields and try again. +
+
diff --git a/test-data/tariff/test_tariffviews-TariffViewTest.test_add.page b/test-data/tariff/test_tariffviews-TariffViewTest.test_add.page index d82de5f..ea47c62 100644 --- a/test-data/tariff/test_tariffviews-TariffViewTest.test_add.page +++ b/test-data/tariff/test_tariffviews-TariffViewTest.test_add.page @@ -194,14 +194,6 @@
-
-
@@ -210,16 +202,28 @@
- + +
+
+ + -
- -
- -
-
-
+ + +
+ +
+ +
+
+
You must enter a tariff name @@ -227,40 +231,46 @@
-
- -
- -
-
-
- -
- - -
-
-
- -
- - in watts -
-
-
+
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in watts +
+
+
Must be higher than 0
-
- -
+ + +
+ +
@@ -318,67 +328,77 @@ - - -
- -
- - in USD -
-
-
- -
-
- -
-
-
- + + -
- -
- - in USD -
-
- - -
- -
- - in USD -
-
-
- -
- - -
-
-
- -
- - in USD per kWh -
-
-
- -
+
+ +
+ + in USD +
+
+ + +
+ +
+
+ +
+
+
+ + + +
+ +
+ + in USD +
+
+ + +
+ +
+ + in USD +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in USD per kWh +
+
+ + +
+ +
@@ -439,23 +459,25 @@ - - -
- -
-
- -
-
-
- + + + -
- -
+
+ +
+
+ +
+
+
+ + +
+ +
@@ -513,37 +535,38 @@ - - -
- -
-
- -
-
-
- + + -
- -
- -
-
+
+ +
+
+ +
+
+
-
- -
- - in kWh -
-
+ +
+ +
+ +
+
+ +
+ +
+ + in kWh +
+
diff --git a/test-data/tariff/test_tariffviews-TariffViewTest.test_add_form_int_outrange.page b/test-data/tariff/test_tariffviews-TariffViewTest.test_add_form_int_outrange.page index e43923a..9038aac 100644 --- a/test-data/tariff/test_tariffviews-TariffViewTest.test_add_form_int_outrange.page +++ b/test-data/tariff/test_tariffviews-TariffViewTest.test_add_form_int_outrange.page @@ -197,14 +197,6 @@
-
-
@@ -213,50 +205,68 @@
- + +
+
+ + -
- -
- -
-
- + -
- -
- -
-
-
- -
- - -
-
-
- -
- - in watts -
-
-
+
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in watts +
+
+
Load Limit must be less than or equal to 2147483647
-
- -
+ + +
+ +
@@ -314,67 +324,77 @@ - - -
- -
- - in USD -
-
-
- -
-
- -
-
-
- + + -
- -
- - in USD -
-
- - -
- -
- - in USD -
-
-
- -
- - -
-
-
- -
- - in USD per kWh -
-
-
- -
+
+ +
+ + in USD +
+
+ + +
+ +
+
+ +
+
+
+ + + +
+ +
+ + in USD +
+
+ + +
+ +
+ + in USD +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in USD per kWh +
+
+ + +
+ +
@@ -435,23 +455,25 @@ - - -
- -
-
- -
-
-
- + + + -
- -
+
+ +
+
+ +
+
+
+ + +
+ +
@@ -509,37 +531,38 @@ - - -
- -
-
- -
-
-
- + + -
- -
- -
-
+
+ +
+
+ +
+
+
-
- -
- - in kWh -
-
+ +
+ +
+ +
+
+ +
+ +
+ + in kWh +
+
diff --git a/test-data/tariff/test_tariffviews-TariffViewTest.test_add_modal_get.page b/test-data/tariff/test_tariffviews-TariffViewTest.test_add_modal_get.page new file mode 100644 index 0000000..47a85f1 --- /dev/null +++ b/test-data/tariff/test_tariffviews-TariffViewTest.test_add_modal_get.page @@ -0,0 +1,368 @@ +> GET /tariff/add-modal HTTP/1.1 +> Cookie: session=%% SESSION %% +> Host: localhost +> User-Agent: Unittest/1.0 +> + +< HTTP/1.1 200 OK +< Content-Type: text/html; charset=utf-8 +< Set-Cookie: %% COOKIE %% +< Set-Cookie: %% COOKIE %% +< Vary: Cookie +< + + + +
+
+ + + + + + +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in watts +
+
+ + +
+ +
+
+ + + + + + + + + + + + +
StartEndLoad Limit in watts
+ +
+ + +
+
+ + +
+ +
+ + in USD +
+
+ + +
+ +
+
+ +
+
+
+ + + +
+ +
+ + in USD +
+
+ + +
+ +
+ + in USD +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in USD per kWh +
+
+ + +
+ +
+ + + + + + + + + + + + + + +
min total. energy (kWh)max total. energy (kWh)USD per kWh
+ +
+ + +
+
+ + +
+ +
+
+ +
+
+
+ + +
+ +
+ + + + + + + + + + + + + +
StartEndModifier
+ +
+ + +
+
+ + +
+ +
+
+ +
+
+
+ + +
+ +
+ +
+
+ +
+ +
+ + in kWh +
+
+ diff --git a/test-data/tariff/test_tariffviews-TariffViewTest.test_add_with_invalid_blockrates.page b/test-data/tariff/test_tariffviews-TariffViewTest.test_add_with_invalid_blockrates.page index 35dafa4..64a5b40 100644 --- a/test-data/tariff/test_tariffviews-TariffViewTest.test_add_with_invalid_blockrates.page +++ b/test-data/tariff/test_tariffviews-TariffViewTest.test_add_with_invalid_blockrates.page @@ -197,14 +197,6 @@
-
-
@@ -213,44 +205,62 @@
- + +
+
+ + -
- -
- -
-
- + -
- -
- -
-
-
- -
- - -
-
-
- -
- - in watts -
-
-
- -
+
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in watts +
+
+ + +
+ +
@@ -308,67 +318,77 @@ - - -
- -
- - in USD -
-
-
- -
-
- -
-
-
- + + -
- -
- - in USD -
-
- - -
- -
- - in USD -
-
-
- -
- - -
-
-
- -
- - in USD per kWh -
-
-
- -
+
+ +
+ + in USD +
+
+ + +
+ +
+
+ +
+
+
+ + + +
+ +
+ + in USD +
+
+ + +
+ +
+ + in USD +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in USD per kWh +
+
+ + +
+ +
@@ -429,29 +449,31 @@ - - -
+
+ +
Block rates contain at least one gap, between 0 and 65535
-
- -
-
- -
-
-
- -
- -
+ +
+ +
+
+ +
+
+
+ + +
+ +
@@ -509,37 +531,38 @@ - - -
- -
-
- -
-
-
- + + -
- -
- -
-
+
+ +
+
+ +
+
+
-
- -
- - in kWh -
-
+ +
+ +
+ +
+
+ +
+ +
+ + in kWh +
+
diff --git a/test-data/tariff/test_tariffviews-TariffViewTest.test_add_with_invalid_tous.page b/test-data/tariff/test_tariffviews-TariffViewTest.test_add_with_invalid_tous.page index 130741d..4db7654 100644 --- a/test-data/tariff/test_tariffviews-TariffViewTest.test_add_with_invalid_tous.page +++ b/test-data/tariff/test_tariffviews-TariffViewTest.test_add_with_invalid_tous.page @@ -197,14 +197,6 @@
-
-
@@ -213,44 +205,62 @@
- + +
+
+ + -
- -
- -
-
- + -
- -
- -
-
-
- -
- - -
-
-
- -
- - in watts -
-
-
- -
+
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in watts +
+
+ + +
+ +
@@ -308,67 +318,77 @@ - - -
- -
- - in USD -
-
-
- -
-
- -
-
-
- + + -
- -
- - in USD -
-
- - -
- -
- - in USD -
-
-
- -
- - -
-
-
- -
- - in USD per kWh -
-
-
- -
+
+ +
+ + in USD +
+
+ + +
+ +
+
+ +
+
+
+ + + +
+ +
+ + in USD +
+
+ + +
+ +
+ + in USD +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in USD per kWh +
+
+ + +
+ +
@@ -429,23 +449,25 @@ - - -
- -
-
- -
-
-
- + + + -
- -
+
+ +
+
+ +
+
+
+ + +
+ +
@@ -503,43 +525,44 @@ - - -
+
+ +
The TOU period modifier must be a positive number.
-
- -
-
- -
-
-
- -
- -
- -
-
+
+ +
+
+ +
+
+
-
- -
- - in kWh -
-
+ +
+ +
+ +
+
+ +
+ +
+ + in kWh +
+
diff --git a/test-data/tariff/test_tariffviews-TariffViewTest.test_edit-edit-negative-post-error.page b/test-data/tariff/test_tariffviews-TariffViewTest.test_edit-edit-negative-post-error.page index 9746fd8..988d2ec 100644 --- a/test-data/tariff/test_tariffviews-TariffViewTest.test_edit-edit-negative-post-error.page +++ b/test-data/tariff/test_tariffviews-TariffViewTest.test_edit-edit-negative-post-error.page @@ -199,14 +199,6 @@
-
-
@@ -215,44 +207,62 @@
- + +
+
+ + -
- -
- -
-
- + -
- -
- -
-
-
- -
- - -
-
-
- -
- - in watts -
-
-
- -
+
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in watts +
+
+ + +
+ +
@@ -310,73 +320,83 @@ - - -
- -
- - in USD -
-
-
- -
-
- -
-
-
- + + -
- -
- - in USD -
-
- - -
- -
- - in USD -
-
-
- -
- - -
-
-
- -
- - in USD per kWh -
-
-
+
+ +
+ + in USD +
+
+ + +
+ +
+
+ +
+
+
+ + + +
+ +
+ + in USD +
+
+ + +
+ +
+ + in USD +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in USD per kWh +
+
+
Flat Rate cannot be negative
-
- -
+ + +
+ +
@@ -437,23 +457,25 @@ - - -
- -
-
- -
-
-
- + + + -
- -
+
+ +
+
+ +
+
+
+ + +
+ +
@@ -511,37 +533,38 @@ - - -
- -
-
- -
-
-
- + + -
- -
- -
-
+
+ +
+
+ +
+
+
-
- -
- - in kWh -
-
+ +
+ +
+ +
+
+ +
+ +
+ + in kWh +
+
diff --git a/test-data/tariff/test_tariffviews-TariffViewTest.test_edit-edit-post-error.page b/test-data/tariff/test_tariffviews-TariffViewTest.test_edit-edit-post-error.page index 70a55c7..d0ce8fc 100644 --- a/test-data/tariff/test_tariffviews-TariffViewTest.test_edit-edit-post-error.page +++ b/test-data/tariff/test_tariffviews-TariffViewTest.test_edit-edit-post-error.page @@ -199,14 +199,6 @@
-
-
@@ -215,44 +207,62 @@
- + +
+
+ + -
- -
- -
-
- + -
- -
- -
-
-
- -
- - -
-
-
- -
- - in watts -
-
-
- -
+
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in watts +
+
+ + +
+ +
@@ -310,73 +320,83 @@ - - -
- -
- - in USD -
-
-
- -
-
- -
-
-
- + + -
- -
- - in USD -
-
- - -
- -
- - in USD -
-
-
- -
- - -
-
-
- -
- - in USD per kWh -
-
-
+
+ +
+ + in USD +
+
+ + +
+ +
+
+ +
+
+
+ + + +
+ +
+ + in USD +
+
+ + +
+ +
+ + in USD +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in USD per kWh +
+
+
Please set a Flat Rate
-
- -
+ + +
+ +
@@ -437,23 +457,25 @@ - - -
- -
-
- -
-
-
- + + + -
- -
+
+ +
+
+ +
+
+
+ + +
+ +
@@ -511,37 +533,38 @@ - - -
- -
-
- -
-
-
- + + -
- -
- -
-
+
+ +
+
+ +
+
+
-
- -
- - in kWh -
-
+ +
+ +
+ +
+
+ +
+ +
+ + in kWh +
+
diff --git a/test-data/tariff/test_tariffviews-TariffViewTest.test_edit.page b/test-data/tariff/test_tariffviews-TariffViewTest.test_edit.page index eae6086..cc88916 100644 --- a/test-data/tariff/test_tariffviews-TariffViewTest.test_edit.page +++ b/test-data/tariff/test_tariffviews-TariffViewTest.test_edit.page @@ -198,14 +198,6 @@
-
-
@@ -214,45 +206,63 @@
- + +
+
+ + -
- -
- -
-
- + -
- -
- -
-
-
- -
- - -
-
-
- -
- - in watts -
-
-
- -
+
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in watts +
+
+ + +
+ +
@@ -310,67 +320,77 @@ - - -
- -
- - in USD -
-
-
- -
-
- -
-
-
- + + -
- -
- - in USD -
-
- - -
- -
- - in USD -
-
-
- -
- - -
-
-
- -
- - in USD per kWh -
-
-
- -
+
+ +
+ + in USD +
+
+ + +
+ +
+
+ +
+
+
+ + + +
+ +
+ + in USD +
+
+ + +
+ +
+ + in USD +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in USD per kWh +
+
+ + +
+ +
@@ -431,23 +451,25 @@ - - -
- -
-
- -
-
-
- + + + -
- -
+
+ +
+
+ +
+
+
+ + +
+ +
@@ -505,37 +527,38 @@ - - -
- -
-
- -
-
-
- + + -
- -
- -
-
+
+ +
+
+ +
+
+
-
- -
- - in kWh -
-
+ +
+ +
+ +
+
+ +
+ +
+ + in kWh +
+
diff --git a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_blockrate.page b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_blockrate.page index b93f730..03ca7dc 100644 --- a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_blockrate.page +++ b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_blockrate.page @@ -197,14 +197,6 @@
-
-
@@ -213,44 +205,62 @@
- + +
+
+ + -
- -
- -
-
- + -
- -
- -
-
-
- -
- - -
-
-
- -
- - in watts -
-
-
- -
+
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in watts +
+
+ + +
+ +
@@ -308,67 +318,77 @@ - - -
- -
- - in USD -
-
-
- -
-
- -
-
-
- + + -
- -
- - in USD -
-
- - -
- -
- - in USD -
-
-
- -
- - -
-
-
- -
- - in USD per kWh -
-
-
- -
+
+ +
+ + in USD +
+
+ + +
+ +
+
+ +
+
+
+ + + +
+ +
+ + in USD +
+
+ + +
+ +
+ + in USD +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in USD per kWh +
+
+ + +
+ +
@@ -429,29 +449,31 @@ - - -
+
+ +
Please add some block rates.
-
- -
-
- -
-
-
- -
- -
+ +
+ +
+
+ +
+
+
+ + +
+ +
@@ -509,37 +531,38 @@ - - -
- -
-
- -
-
-
- + + -
- -
- -
-
+
+ +
+
+ +
+
+
-
- -
- - in kWh -
-
+ +
+ +
+ +
+
+ +
+ +
+ + in kWh +
+
diff --git a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_duplicate_name.page b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_duplicate_name.page index 49c541d..9b5013b 100644 --- a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_duplicate_name.page +++ b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_duplicate_name.page @@ -194,14 +194,6 @@
-
-
@@ -210,15 +202,27 @@
- + +
+
+ + -
- -
- -
-
-
+ + +
+ +
+ +
+
+
A tariff with the name "TARIFF" already exists @@ -226,34 +230,40 @@
-
- -
- -
-
-
- -
- - -
-
-
- -
- - in watts -
-
-
- -
+
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in watts +
+
+ + +
+ +
@@ -311,67 +321,77 @@ - - -
- -
- - in USD -
-
-
- -
-
- -
-
-
- + + -
- -
- - in USD -
-
- - -
- -
- - in USD -
-
-
- -
- - -
-
-
- -
- - in USD per kWh -
-
-
- -
+
+ +
+ + in USD +
+
+ + +
+ +
+
+ +
+
+
+ + + +
+ +
+ + in USD +
+
+ + +
+ +
+ + in USD +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in USD per kWh +
+
+ + +
+ +
@@ -432,23 +452,25 @@ - - -
- -
-
- -
-
-
- + + + -
- -
+
+ +
+
+ +
+
+
+ + +
+ +
@@ -506,37 +528,38 @@ - - -
- -
-
- -
-
-
- + + -
- -
- -
-
+
+ +
+
+ +
+
+
-
- -
- - in kWh -
-
+ +
+ +
+ +
+
+ +
+ +
+ + in kWh +
+
diff --git a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_empty_name.page b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_empty_name.page index 1bf005a..2652f10 100644 --- a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_empty_name.page +++ b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_empty_name.page @@ -197,14 +197,6 @@
-
-
@@ -213,15 +205,27 @@
- + +
+
+ + -
- -
- -
-
-
+ + +
+ +
+ +
+
+
Please set a name for this tariff @@ -229,34 +233,40 @@
-
- -
- -
-
-
- -
- - -
-
-
- -
- - in watts -
-
-
- -
+
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in watts +
+
+ + +
+ +
@@ -314,67 +324,77 @@ - - -
- -
- - in USD -
-
-
- -
-
- -
-
-
- + + -
- -
- - in USD -
-
- - -
- -
- - in USD -
-
-
- -
- - -
-
-
- -
- - in USD per kWh -
-
-
- -
+
+ +
+ + in USD +
+
+ + +
+ +
+
+ +
+
+
+ + + +
+ +
+ + in USD +
+
+ + +
+ +
+ + in USD +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in USD per kWh +
+
+ + +
+ +
@@ -435,23 +455,25 @@ - - -
- -
-
- -
-
-
- + + + -
- -
+
+ +
+
+ +
+
+
+ + +
+ +
@@ -509,37 +531,38 @@ - - -
- -
-
- -
-
-
- + + -
- -
- -
-
+
+ +
+
+ +
+
+
-
- -
- - in kWh -
-
+ +
+ +
+ +
+
+ +
+ +
+ + in kWh +
+
diff --git a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_enter_load_limit.page b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_enter_load_limit.page index ceefb7c..cc600db 100644 --- a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_enter_load_limit.page +++ b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_enter_load_limit.page @@ -197,14 +197,6 @@
-
-
@@ -213,50 +205,68 @@
- + +
+
+ + -
- -
- -
-
- + -
- -
- -
-
-
- -
- - -
-
-
- -
- - in watts -
-
-
+
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in watts +
+
+
Please enter a Load Limit for this tariff
-
- -
+ + +
+ +
@@ -314,67 +324,77 @@ - - -
- -
- - in USD -
-
-
- -
-
- -
-
-
- + + -
- -
- - in USD -
-
- - -
- -
- - in USD -
-
-
- -
- - -
-
-
- -
- - in USD per kWh -
-
-
- -
+
+ +
+ + in USD +
+
+ + +
+ +
+
+ +
+
+
+ + + +
+ +
+ + in USD +
+
+ + +
+ +
+ + in USD +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in USD per kWh +
+
+ + +
+ +
@@ -435,23 +455,25 @@ - - -
- -
-
- -
-
-
- + + + -
- -
+
+ +
+
+ +
+
+
+ + +
+ +
@@ -509,37 +531,38 @@ - - -
- -
-
- -
-
-
- + + -
- -
- -
-
+
+ +
+
+ +
+
+
-
- -
- - in kWh -
-
+ +
+ +
+ +
+
+ +
+ +
+ + in kWh +
+
diff --git a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_enter_monthly_plan_price.page b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_enter_monthly_plan_price.page index fd1918d..1ab550a 100644 --- a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_enter_monthly_plan_price.page +++ b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_enter_monthly_plan_price.page @@ -197,14 +197,6 @@
-
-
@@ -213,44 +205,62 @@
- + +
+
+ + -
- -
- -
-
- + -
- -
- -
-
-
- -
- - -
-
-
- -
- - in watts -
-
-
- -
+
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in watts +
+
+ + +
+ +
@@ -308,73 +318,83 @@ - - -
- -
- - in USD -
-
-
- -
-
- -
-
-
- + + -
- -
- - in USD -
-
- - -
- -
- - in USD -
-
-
+
+ +
+ + in USD +
+
+ + +
+ +
+
+ +
+
+
+ + + +
+ +
+ + in USD +
+
+ + +
+ +
+ + in USD +
+
+
Number must be at least 0.
-
- -
- - -
-
-
- -
- - in USD per kWh -
-
-
- -
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in USD per kWh +
+
+ + +
+ +
@@ -435,23 +455,25 @@ - - -
- -
-
- -
-
-
- + + -
- -
+ +
+ +
+
+ +
+
+
+ + +
+ +
@@ -509,37 +531,38 @@ - - -
- -
-
- -
-
-
- + + -
- -
- -
-
+
+ +
+
+ +
+
+
-
- -
- - in kWh -
-
+ +
+ +
+ +
+
+ +
+ +
+ + in kWh +
+
diff --git a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_existing_duplicate_names.page b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_existing_duplicate_names.page index 0f1f2aa..7b325ed 100644 --- a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_existing_duplicate_names.page +++ b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_existing_duplicate_names.page @@ -197,14 +197,6 @@
-
-
@@ -213,15 +205,27 @@
- + +
+
+ + -
- -
- -
-
-
+ + +
+ +
+ +
+
+
A tariff with the name "TARIFF" already exists @@ -229,34 +233,40 @@
-
- -
- -
-
-
- -
- - -
-
-
- -
- - in watts -
-
-
- -
+
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in watts +
+
+ + +
+ +
@@ -314,67 +324,77 @@ - - -
- -
- - in USD -
-
-
- -
-
- -
-
-
- + + -
- -
- - in USD -
-
- - -
- -
- - in USD -
-
-
- -
- - -
-
-
- -
- - in USD per kWh -
-
-
- -
+
+ +
+ + in USD +
+
+ + +
+ +
+
+ +
+
+
+ + + +
+ +
+ + in USD +
+
+ + +
+ +
+ + in USD +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in USD per kWh +
+
+ + +
+ +
@@ -435,23 +455,25 @@ - - -
- -
-
- -
-
-
- + + + -
- -
+
+ +
+
+ +
+
+
+ + +
+ +
@@ -509,37 +531,38 @@ - - -
- -
-
- -
-
-
- + + -
- -
- -
-
+
+ +
+
+ +
+
+
-
- -
- - in kWh -
-
+ +
+ +
+ +
+
+ +
+ +
+ + in kWh +
+
diff --git a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_flat_rate.page b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_flat_rate.page index 0189fd9..67234ca 100644 --- a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_flat_rate.page +++ b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_flat_rate.page @@ -197,14 +197,6 @@
-
-
@@ -213,44 +205,62 @@
- + +
+
+ + -
- -
- -
-
- + -
- -
- -
-
-
- -
- - -
-
-
- -
- - in watts -
-
-
- -
+
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in watts +
+
+ + +
+ +
@@ -308,73 +318,83 @@ - - -
- -
- - in USD -
-
-
- -
-
- -
-
-
- + + -
- -
- - in USD -
-
- - -
- -
- - in USD -
-
-
- -
- - -
-
-
- -
- - in USD per kWh -
-
-
+
+ +
+ + in USD +
+
+ + +
+ +
+
+ +
+
+
+ + + +
+ +
+ + in USD +
+
+ + +
+ +
+ + in USD +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in USD per kWh +
+
+
Please set a Flat Rate
-
- -
+ + +
+ +
@@ -435,23 +455,25 @@ - - -
- -
-
- -
-
-
- + + + -
- -
+
+ +
+
+ +
+
+
+ + +
+ +
@@ -509,37 +531,38 @@ - - -
- -
-
- -
-
-
- + + -
- -
- -
-
+
+ +
+
+ +
+
+
-
- -
- - in kWh -
-
+ +
+ +
+ +
+
+ +
+ +
+ + in kWh +
+
diff --git a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_low_balance_empty.page b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_low_balance_empty.page index be646ab..e6b8d80 100644 --- a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_low_balance_empty.page +++ b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_low_balance_empty.page @@ -197,14 +197,6 @@
-
-
@@ -213,44 +205,62 @@
- + +
+
+ + -
- -
- -
-
- + -
- -
- -
-
-
- -
- - -
-
-
- -
- - in watts -
-
-
- -
+
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in watts +
+
+ + +
+ +
@@ -308,79 +318,89 @@ - - -
- -
- - in USD -
-
-
+
+ + + +
+ +
+ + in USD +
+
+
Low Balance cannot be empty.
-
- -
-
- -
-
-
- -
- -
- - in USD -
-
- - -
- -
- - in USD -
-
-
- -
- - -
-
-
- -
- - in USD per kWh -
-
-
+
+ +
+
+ +
+
+
+ + + +
+ +
+ + in USD +
+
+ + +
+ +
+ + in USD +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in USD per kWh +
+
+
Please set a Flat Rate
-
- -
+ + +
+ +
@@ -441,23 +461,25 @@ - - -
- -
-
- -
-
-
- + + + -
- -
+
+ +
+
+ +
+
+
+ + +
+ +
@@ -515,37 +537,38 @@ - - -
- -
-
- -
-
-
- + + -
- -
- -
-
+
+ +
+
+ +
+
+
-
- -
- - in kWh -
-
+ +
+ +
+ +
+
+ +
+ +
+ + in kWh +
+
diff --git a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_low_balance_negative.page b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_low_balance_negative.page index b59e35a..aace06d 100644 --- a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_low_balance_negative.page +++ b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_low_balance_negative.page @@ -197,14 +197,6 @@
-
-
@@ -213,44 +205,62 @@
- + +
+
+ + -
- -
- -
-
- + -
- -
- -
-
-
- -
- - -
-
-
- -
- - in watts -
-
-
- -
+
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in watts +
+
+ + +
+ +
@@ -308,79 +318,89 @@ - - -
- -
- - in USD -
-
-
+
+ + + +
+ +
+ + in USD +
+
+
Low Balance must be higher or equals to 0.
-
- -
-
- -
-
-
- -
- -
- - in USD -
-
- - -
- -
- - in USD -
-
-
- -
- - -
-
-
- -
- - in USD per kWh -
-
-
+
+ +
+
+ +
+
+
+ + + +
+ +
+ + in USD +
+
+ + +
+ +
+ + in USD +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in USD per kWh +
+
+
Please set a Flat Rate
-
- -
+ + +
+ +
@@ -441,23 +461,25 @@ - - -
- -
-
- -
-
-
- + + + -
- -
+
+ +
+
+ +
+
+
+ + +
+ +
@@ -515,37 +537,38 @@ - - -
- -
-
- -
-
-
- + + -
- -
- -
-
+
+ +
+
+ +
+
+
-
- -
- - in kWh -
-
+ +
+ +
+ +
+
+ +
+ +
+ + in kWh +
+
diff --git a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_negative_flat_rate.page b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_negative_flat_rate.page index 9a51721..850b78d 100644 --- a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_negative_flat_rate.page +++ b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_negative_flat_rate.page @@ -197,14 +197,6 @@
-
-
@@ -213,44 +205,62 @@
- + +
+
+ + -
- -
- -
-
- + -
- -
- -
-
-
- -
- - -
-
-
- -
- - in watts -
-
-
- -
+
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in watts +
+
+ + +
+ +
@@ -308,73 +318,83 @@ - - -
- -
- - in USD -
-
-
- -
-
- -
-
-
- + + -
- -
- - in USD -
-
- - -
- -
- - in USD -
-
-
- -
- - -
-
-
- -
- - in USD per kWh -
-
-
+
+ +
+ + in USD +
+
+ + +
+ +
+
+ +
+
+
+ + + +
+ +
+ + in USD +
+
+ + +
+ +
+ + in USD +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in USD per kWh +
+
+
Flat Rate cannot be negative
-
- -
+ + +
+ +
@@ -435,23 +455,25 @@ - - -
- -
-
- -
-
-
- + + + -
- -
+
+ +
+
+ +
+
+
+ + +
+ +
@@ -509,37 +531,38 @@ - - -
- -
-
- -
-
-
- + + -
- -
- -
-
+
+ +
+
+ +
+
+
-
- -
- - in kWh -
-
+ +
+ +
+ +
+
+ +
+ +
+ + in kWh +
+
diff --git a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_negative_load_limit.page b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_negative_load_limit.page index 2fcadba..ebfae4c 100644 --- a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_negative_load_limit.page +++ b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_negative_load_limit.page @@ -197,14 +197,6 @@
-
-
@@ -213,50 +205,68 @@
- + +
+
+ + -
- -
- -
-
- + -
- -
- -
-
-
- -
- - -
-
-
- -
- - in watts -
-
-
+
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in watts +
+
+
Load Limits cannot be negative
-
- -
+ + +
+ +
@@ -314,67 +324,77 @@ - - -
- -
- - in USD -
-
-
- -
-
- -
-
-
- + + -
- -
- - in USD -
-
- - -
- -
- - in USD -
-
-
- -
- - -
-
-
- -
- - in USD per kWh -
-
-
- -
+
+ +
+ + in USD +
+
+ + +
+ +
+
+ +
+
+
+ + + +
+ +
+ + in USD +
+
+ + +
+ +
+ + in USD +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in USD per kWh +
+
+ + +
+ +
@@ -435,23 +455,25 @@ - - -
- -
-
- -
-
-
- + + + -
- -
+
+ +
+
+ +
+
+
+ + +
+ +
@@ -509,37 +531,38 @@ - - -
- -
-
- -
-
-
- + + -
- -
- -
-
+
+ +
+
+ +
+
+
-
- -
- - in kWh -
-
+ +
+ +
+ +
+
+ +
+ +
+ + in kWh +
+
diff --git a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_no_scheduled_load_limits.page b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_no_scheduled_load_limits.page index bbc03f4..8739e41 100644 --- a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_no_scheduled_load_limits.page +++ b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_no_scheduled_load_limits.page @@ -197,14 +197,6 @@
-
-
@@ -213,44 +205,62 @@
- + +
+
+ + -
- -
- -
-
- + -
- -
- -
-
-
- -
- - -
-
-
- -
- - in watts -
-
-
- -
+
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in watts +
+
+ + +
+ +
@@ -308,79 +318,89 @@ - - -
+
+ +
Please add some Load limit periods.
-
- -
- - in USD -
-
-
- -
-
- -
-
-
- -
- -
- - in USD -
-
- - -
- -
- - in USD -
-
-
- -
- - -
-
-
- -
- - in USD per kWh -
-
-
+
+ +
+ + in USD +
+
+ + +
+ +
+
+ +
+
+
+ + + +
+ +
+ + in USD +
+
+ + +
+ +
+ + in USD +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in USD per kWh +
+
+
Please set a Flat Rate
-
- -
+ + +
+ +
@@ -441,23 +461,25 @@ - - -
- -
-
- -
-
-
- + + + -
- -
+
+ +
+
+ +
+
+
+ + +
+ +
@@ -515,37 +537,38 @@ - - -
- -
-
- -
-
-
- + + -
- -
- -
-
+
+ +
+
+ +
+
+
-
- -
- - in kWh -
-
+ +
+ +
+ +
+
+ +
+ +
+ + in kWh +
+
diff --git a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_tous.page b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_tous.page index 0a36f96..9b271f2 100644 --- a/test-data/tariff/test_tariffviews-TariffViewTest.test_error_tous.page +++ b/test-data/tariff/test_tariffviews-TariffViewTest.test_error_tous.page @@ -197,14 +197,6 @@
-
-
@@ -213,44 +205,62 @@
- + +
+
+ + -
- -
- -
-
- + -
- -
- -
-
-
- -
- - -
-
-
- -
- - in watts -
-
-
- -
+
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in watts +
+
+ + +
+ +
@@ -308,73 +318,83 @@ - - -
- -
- - in USD -
-
-
- -
-
- -
-
-
- + + -
- -
- - in USD -
-
- - -
- -
- - in USD -
-
-
- -
- - -
-
-
- -
- - in USD per kWh -
-
-
+
+ +
+ + in USD +
+
+ + +
+ +
+
+ +
+
+
+ + + +
+ +
+ + in USD +
+
+ + +
+ +
+ + in USD +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + in USD per kWh +
+
+
Please set a Flat Rate
-
- -
+ + +
+ +
@@ -435,23 +455,25 @@ - - -
- -
-
- -
-
-
- + + + -
- -
+
+ +
+
+ +
+
+
+ + +
+ +
@@ -509,43 +531,44 @@ - - -
+
+ +
Please add some TOU periods.
-
- -
-
- -
-
-
- -
- -
- -
-
+
+ +
+
+ +
+
+
-
- -
- - in kWh -
-
+ +
+ +
+ +
+
+ +
+ +
+ + in kWh +
+