diff options
author | Matthew Somerville <matthew-github@dracos.co.uk> | 2016-12-01 18:25:24 +0000 |
---|---|---|
committer | Matthew Somerville <matthew-github@dracos.co.uk> | 2016-12-16 10:21:55 +0000 |
commit | 2abd85a6d9151f95c82656df9e6b8220e381ca03 (patch) | |
tree | 5fa4145bdad659cdc70e86f3604f6c37544f8249 | |
parent | 34f942d7881451e164431a3231774568421a00f5 (diff) |
Add offline storing of inspect forms.
This allows the inspect form to be submitted when offline, with the data
saved in localStorage, the number of saved forms shown in the banner,
and the forms to be uploaded when back online.
It copes if you go back to a report after having submitted the form, and
if the back-online submission fails due to CSRF failure, retrying once
with a new token.
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Report.pm | 8 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Root.pm | 1 | ||||
-rwxr-xr-x | templates/web/base/errors/generic.html | 4 | ||||
-rw-r--r-- | web/cobrands/fixmystreet/offline.js | 125 |
4 files changed, 133 insertions, 5 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Report.pm b/perllib/FixMyStreet/App/Controller/Report.pm index f7ccddc70..481bc4ab8 100644 --- a/perllib/FixMyStreet/App/Controller/Report.pm +++ b/perllib/FixMyStreet/App/Controller/Report.pm @@ -385,10 +385,14 @@ sub inspect : Private { $problem->lastupdate( \'current_timestamp' ); $problem->update; if ( defined($update_text) ) { + my $timestamp = \'current_timestamp'; + if (my $saved_at = $c->get_param('saved_at')) { + $timestamp = DateTime->from_epoch( epoch => $saved_at ); + } $problem->add_to_comments( { text => $update_text, - created => \'current_timestamp', - confirmed => \'current_timestamp', + created => $timestamp, + confirmed => $timestamp, user_id => $c->user->id, name => $c->user->from_body->name, state => 'confirmed', diff --git a/perllib/FixMyStreet/App/Controller/Root.pm b/perllib/FixMyStreet/App/Controller/Root.pm index 1df249999..20a871b17 100644 --- a/perllib/FixMyStreet/App/Controller/Root.pm +++ b/perllib/FixMyStreet/App/Controller/Root.pm @@ -108,6 +108,7 @@ sub page_error_403_access_denied : Private { sub page_error_400_bad_request : Private { my ( $self, $c, $error_msg ) = @_; + $c->forward('/auth/get_csrf_token'); $c->detach('page_error', [ $error_msg, 400 ]); } diff --git a/templates/web/base/errors/generic.html b/templates/web/base/errors/generic.html index d0d1e2e00..241b310de 100755 --- a/templates/web/base/errors/generic.html +++ b/templates/web/base/errors/generic.html @@ -1,5 +1,9 @@ [% INCLUDE 'header.html', bodyclass = 'fullwidthpage', title = loc('Error') %] +[% IF csrf_token ~%] +<input type="hidden" name="token" value="[% csrf_token %]"> +[% END ~%] + <div class="confirmation-header confirmation-header--failure"> <h1>[% loc('Error') %]</h1> <p>[% message %]</p> diff --git a/web/cobrands/fixmystreet/offline.js b/web/cobrands/fixmystreet/offline.js index 23db5d472..c875c9b84 100644 --- a/web/cobrands/fixmystreet/offline.js +++ b/web/cobrands/fixmystreet/offline.js @@ -2,13 +2,80 @@ fixmystreet.offlineBanner = (function() { var toCache = 0; var cachedSoFar = 0; + function formText() { + var num = fixmystreet.offlineData.getForms().length; + return num + ' form' + (num===1 ? '' : 's'); + } + + function onlineText() { + return 'You have <a id="oFN" href=""><span>' + formText() + '</span> saved to submit</a>.'; + } + + function offlineText() { + return 'You are offline \u2013 <span>' + formText() + '</span> saved.'; + } + return { make: function(offline) { - var banner = ['<div class="top_banner top_banner--offline"><p><span id="offline_saving">']; + var num = fixmystreet.offlineData.getForms().length; + var banner = ['<div class="top_banner top_banner--offline"><p><span id="offline_saving"></span> <span id="offline_forms">']; + if (offline || num > 0) { + banner.push(offline ? offlineText() : onlineText()); + } banner.push('</span></p></div>'); banner = $(banner.join('')); banner.prependTo('.content'); - banner.hide(); + if (!offline && num === 0) { + banner.hide(); + } + + window.addEventListener("offline", function(e) { + $('.top_banner--offline').slideDown(); + $('#offline_forms').html(offlineText()); + }); + + window.addEventListener("online", function(e) { + $('#offline_forms').html(onlineText()); + }); + + function nextForm(DataOrJqXHR, textStatus, jqXHROrErrorThrown) { + fixmystreet.offlineData.shiftForm(); + $(document).dequeue('postForm'); + } + + function postForm(url, data) { + return $.ajax({ url: url, data: data, type: 'POST' }).done(nextForm); + } + + $(document).on('click', '#oFN', function(e) { + e.preventDefault(); + fixmystreet.offlineData.getForms().forEach(function(form) { + $(document).queue('postForm', function() { + postForm(form[0], form[1]).fail(function(jqXHR) { + if (jqXHR.status !== 400) { + return nextForm(); + } + // In case the request failed due to out-of-date CSRF token, + // try once more with a new token given in the error response. + var m = jqXHR.responseText.match(/name="token" value="([^"]*)"/); + if (!m) { + return nextForm(); + } + var token = m[1]; + if (!token) { + return nextForm(); + } + var param = form[1].replace(/&token=[^&]*/, '&token=' + token); + return postForm(form[0], param).fail(nextForm); + }); + }); + }); + $(document).dequeue('postForm'); + }); + }, + update: function() { + $('.top_banner--offline').slideDown(); + $('#offline_forms span').text(formText()); }, startProgress: function(l) { $('.top_banner--offline').slideDown(); @@ -33,7 +100,7 @@ fixmystreet.offlineData = (function() { if (data === undefined) { data = JSON.parse(localStorage.getItem('offlineData')); if (!data) { - data = { cachedReports: {} }; + data = { cachedReports: {}, forms: [] }; } } return data; @@ -44,6 +111,22 @@ fixmystreet.offlineData = (function() { } return { + getForms: function() { + return getData().forms; + }, + addForm: function(action, formData) { + var forms = getData().forms; + if (!forms.length || formData != forms[forms.length - 1][1]) { + forms.push([action, formData]); + saveData(); + } + fixmystreet.offlineBanner.update(); + }, + shiftForm: function(idx) { + getData().forms.shift(); + saveData(); + fixmystreet.offlineBanner.update(); + }, getCachedUrls: function() { return Object.keys(getData().cachedReports); }, @@ -216,6 +299,29 @@ fixmystreet.offline = (function() { $('.js-back-to-report-list').attr('href', '/my/planned'); + // Refill form with saved data if there is any + var savedForm; + fixmystreet.offlineData.getForms().forEach(function(form) { + if (form[0].endsWith(url)) { + savedForm = form[1]; + } + }); + if (savedForm) { + savedForm.replace(/\+/g, '%20').split('&').forEach(function(kv) { + kv = kv.split('=', 2); + if (kv[0] != 'save_inspected' && kv[0] != 'public_update' && kv[0] != 'save') { + $('[name=' + kv[0] + ']').val(decodeURIComponent(kv[1])); + } + }); + } + + $('#report_inspect_form').submit(function() { + var data = $(this).serialize() + '&save=1&saved_at=' + Math.floor(+new Date() / 1000); + fixmystreet.offlineData.addForm(this.action, data); + location.href = '/my/planned?saved=1'; + return false; + }); + return true; } @@ -250,7 +356,20 @@ if ($('#offline_list').length) { if (html) { $('#offline_list').before('<h2>Your offline reports</h2>'); $('#offline_list').html(html); + if (location.search.indexOf('saved=1') > 0) { + $('#offline_list').before('<p class="form-success">Your form has been saved offline for submission when back online.</p>'); + } fixmystreet.offline.replaceImages('#offline_list img'); + var offlineForms = fixmystreet.offlineData.getForms(); + var savedForms = {}; + offlineForms.forEach(function(form) { + savedForms[form[0]] = 1; + }); + $('#offline_list a').each(function(i, a) { + if (savedForms[a.href]) { + $(this).find('h3').prepend('<em>Form data saved</em> '); + } + }); } } fixmystreet.offlineBanner.make(true); |