From eef346f3a494517d7148bccc6c1b74b9e7cd9e11 Mon Sep 17 00:00:00 2001 From: Kasper Brandt Date: Fri, 15 Apr 2016 16:01:04 +0200 Subject: [PATCH] [#2102] See number of processed projects in overview --- akvo/rest/serializers/iati_export.py | 1 + akvo/rsr/models/iati_export.py | 7 +++ akvo/rsr/static/scripts-src/my-iati.js | 84 ++++++++++++++++++++++--- akvo/rsr/static/scripts-src/my-iati.jsx | 84 ++++++++++++++++++++++--- akvo/templates/myrsr/my_iati.html | 7 ++- 5 files changed, 168 insertions(+), 15 deletions(-) diff --git a/akvo/rest/serializers/iati_export.py b/akvo/rest/serializers/iati_export.py index 20ee5bdcbb..3e04dbd72f 100644 --- a/akvo/rest/serializers/iati_export.py +++ b/akvo/rest/serializers/iati_export.py @@ -14,6 +14,7 @@ class IatiExportSerializer(BaseRSRSerializer): user_name = serializers.Field(source='user.get_full_name') status_label = serializers.Field(source='show_status') + processed_projects = serializers.Field(source='processed_projects') class Meta: model = IatiExport diff --git a/akvo/rsr/models/iati_export.py b/akvo/rsr/models/iati_export.py index 41112a2ba9..ab99ad8587 100644 --- a/akvo/rsr/models/iati_export.py +++ b/akvo/rsr/models/iati_export.py @@ -101,3 +101,10 @@ def create_iati_file(self): else: # No projects, so update the status to 'Cancelled' self.update_status(4) + + def processed_projects(self): + """ + Find the number of processed projects of this IATI export. Generally, for completed + exports, this number will be the same as the number of total projects. + """ + return self.iati_activity_exports.filter(status=2).count() diff --git a/akvo/rsr/static/scripts-src/my-iati.js b/akvo/rsr/static/scripts-src/my-iati.js index 0c51681179..83b741185d 100644 --- a/akvo/rsr/static/scripts-src/my-iati.js +++ b/akvo/rsr/static/scripts-src/my-iati.js @@ -283,7 +283,7 @@ function loadComponents() { return ( React.DOM.tr(null, - React.DOM.td(null, React.DOM.input( {type:"checkbox", onClick:this.switchAction} )), + React.DOM.td(null, React.DOM.input( {type:"checkbox", onClick:this.switchAction, checked:this.props.selected} )), React.DOM.td(null, this.props.project.id), React.DOM.td(null, this.props.project.title || '\<' + cap(i18n.untitled) + ' ' + i18n.project + '\>',React.DOM.br(null), @@ -304,9 +304,11 @@ function loadComponents() { if (this.props.projects.length > 0) { // In case there are projets, show a table overview of the projects. projects = this.props.projects.map(function(project) { + var selected = thisTable.props.selectedProjects.indexOf(project.id) > -1; return React.createElement(ProjectRow, { key: project.id, project: project, + selected: selected, switchProject: thisTable.props.switchProject }); }); @@ -422,6 +424,7 @@ function loadComponents() { // Show a table of projects when the data has been loaded initOrTable = React.createElement(ProjectsTable, { projects: this.state.allProjects.results, + selectedProjects: this.state.selectedProjects, switchProject: this.switchProject }); } @@ -498,11 +501,19 @@ function loadComponents() { } }, + renderNumberOfProjects: function() { + if (this.props.exp.status !== 2) { + return this.props.exp.projects.length; + } else { + return this.props.exp.projects.length + ' (' + this.props.exp.processed_projects + ' ' + i18n.processed + ')'; + } + }, + render: function() { return ( React.DOM.tr( {className:this.renderRowClass()}, React.DOM.td(null, this.props.exp.status_label), - React.DOM.td(null, this.props.exp.projects.length), + React.DOM.td(null, this.renderNumberOfProjects()), React.DOM.td(null, this.props.exp.user_name), React.DOM.td(null, displayDate(this.props.exp.created_at)), React.DOM.td(null, 'v' + this.props.exp.version), @@ -566,6 +577,7 @@ function loadComponents() { exports: null, initializing: true, refreshing: false, + refreshingIn: 0, actionInProgress: false }; }, @@ -584,6 +596,9 @@ function loadComponents() { initializing: false, exports: response }); + if (thisApp.pendingOrInProgress()) { + thisApp.startCountDown(); + } } function refreshingSuccess(response) { @@ -591,6 +606,9 @@ function loadComponents() { refreshing: false, exports: response }); + if (thisApp.pendingOrInProgress()) { + thisApp.startCountDown(); + } } if (!first_time) { @@ -599,7 +617,26 @@ function loadComponents() { } else { apiCall('GET', url, {}, firstTimeSuccess); } - }, + }, + + startCountDown: function() { + // Set the countdown for refreshing the table to start at 10 seconds + this.setState({refreshingIn: 10}); + + // Start ticking down + var thisApp = this, + intervalId = setInterval(tick, 1000); + + function tick() { + var newRefreshing = thisApp.state.refreshingIn - 1; + thisApp.setState({refreshingIn: newRefreshing}); + if (newRefreshing === 0) { + // Once done, reload the exports + clearInterval(intervalId); + thisApp.loadExports(false); + } + } + }, publicFile: function() { if (this.state.exports === null) { @@ -624,6 +661,21 @@ function loadComponents() { } return null; }, + + pendingOrInProgress: function() { + // Check to see whether there is at least one export pending or in progress. + if (!this.state.initializing) { + for (var i = 0; i < this.state.exports.results.length; i++) { + var exp = this.state.exports.results[i]; + if (exp.status < 3) { + return true; + } + } + return false; + } else { + return false; + } + }, setPublic: function(exportId) { // Basically what we do is to set this export to public first, and then set all @@ -672,15 +724,33 @@ function loadComponents() { this.setState({actionInProgress: true}); apiCall('PATCH', exportUrl.replace('{iati_export}', exportId), publicData, exportUpdated); }, + + renderRefreshing: function() { + if (this.state.refreshing) { + return ( + React.DOM.span( {className:"small"}, + React.DOM.i( {className:"fa fa-spin fa-spinner"} ), " ", cap(i18n.refreshing) + ' ' + i18n.iati_exports + '...' + ) + ); + } else if (this.pendingOrInProgress()) { + return ( + React.DOM.span( {className:"small"}, + React.DOM.i( {className:"fa fa-spin fa-spinner"} ), " ", cap(i18n.pending_or_progress) + '. ' + cap(i18n.refreshing) + ' ' + i18n.in + ' ' + this.state.refreshingIn + ' ' + i18n.seconds + '...' + ) + ); + } else { + return ( + React.DOM.span(null ) + ); + } + }, render: function() { var initOrTable, - refreshing, exportCount, exportCountString, lastExportDescription; - refreshing = this.state.refreshing ? React.DOM.span( {className:"small"}, React.DOM.i( {className:"fa fa-spin fa-spinner"} ),' ' + cap(i18n.refreshing) + ' ' + i18n.iati_exports + '...') : React.DOM.span(null ); exportCount = !this.state.initializing ? this.state.exports.count : null; exportCountString = (exportCount !== null && exportCount > 0) ? ' ' + this.state.exports.count + ' ' : ' '; @@ -688,7 +758,7 @@ function loadComponents() { // Only show a message that data is being loading when initializing initOrTable = React.DOM.span( {className:"small"}, React.DOM.i( {className:"fa fa-spin fa-spinner"}),' ' + cap(i18n.loading) + ' ' + i18n.last + ' ' + i18n.iati_exports + '...'); } else { - // Show a table of existing imports (max 10) when the data has been loaded + // Show a table of existing imports when the data has been loaded initOrTable = React.createElement(ExportsTable, { exports: this.state.exports, refreshing: this.state.refreshing, @@ -709,7 +779,7 @@ function loadComponents() { React.DOM.div(null, React.DOM.h4( {className:"topMargin"}, cap(i18n.last) + exportCountString + i18n.iati_exports), lastExportDescription, - refreshing, + this.renderRefreshing(), initOrTable ) ); diff --git a/akvo/rsr/static/scripts-src/my-iati.jsx b/akvo/rsr/static/scripts-src/my-iati.jsx index d763e646c9..17ce997923 100644 --- a/akvo/rsr/static/scripts-src/my-iati.jsx +++ b/akvo/rsr/static/scripts-src/my-iati.jsx @@ -283,7 +283,7 @@ function loadComponents() { return ( - + {this.props.project.id} {this.props.project.title || '\<' + cap(i18n.untitled) + ' ' + i18n.project + '\>'}
@@ -304,9 +304,11 @@ function loadComponents() { if (this.props.projects.length > 0) { // In case there are projets, show a table overview of the projects. projects = this.props.projects.map(function(project) { + var selected = thisTable.props.selectedProjects.indexOf(project.id) > -1; return React.createElement(ProjectRow, { key: project.id, project: project, + selected: selected, switchProject: thisTable.props.switchProject }); }); @@ -422,6 +424,7 @@ function loadComponents() { // Show a table of projects when the data has been loaded initOrTable = React.createElement(ProjectsTable, { projects: this.state.allProjects.results, + selectedProjects: this.state.selectedProjects, switchProject: this.switchProject }); } @@ -498,11 +501,19 @@ function loadComponents() { } }, + renderNumberOfProjects: function() { + if (this.props.exp.status !== 2) { + return this.props.exp.projects.length; + } else { + return this.props.exp.projects.length + ' (' + this.props.exp.processed_projects + ' ' + i18n.processed + ')'; + } + }, + render: function() { return ( {this.props.exp.status_label} - {this.props.exp.projects.length} + {this.renderNumberOfProjects()} {this.props.exp.user_name} {displayDate(this.props.exp.created_at)} {'v' + this.props.exp.version} @@ -566,6 +577,7 @@ function loadComponents() { exports: null, initializing: true, refreshing: false, + refreshingIn: 0, actionInProgress: false }; }, @@ -584,6 +596,9 @@ function loadComponents() { initializing: false, exports: response }); + if (thisApp.pendingOrInProgress()) { + thisApp.startCountDown(); + } } function refreshingSuccess(response) { @@ -591,6 +606,9 @@ function loadComponents() { refreshing: false, exports: response }); + if (thisApp.pendingOrInProgress()) { + thisApp.startCountDown(); + } } if (!first_time) { @@ -599,7 +617,26 @@ function loadComponents() { } else { apiCall('GET', url, {}, firstTimeSuccess); } - }, + }, + + startCountDown: function() { + // Set the countdown for refreshing the table to start at 10 seconds + this.setState({refreshingIn: 10}); + + // Start ticking down + var thisApp = this, + intervalId = setInterval(tick, 1000); + + function tick() { + var newRefreshing = thisApp.state.refreshingIn - 1; + thisApp.setState({refreshingIn: newRefreshing}); + if (newRefreshing === 0) { + // Once done, reload the exports + clearInterval(intervalId); + thisApp.loadExports(false); + } + } + }, publicFile: function() { if (this.state.exports === null) { @@ -624,6 +661,21 @@ function loadComponents() { } return null; }, + + pendingOrInProgress: function() { + // Check to see whether there is at least one export pending or in progress. + if (!this.state.initializing) { + for (var i = 0; i < this.state.exports.results.length; i++) { + var exp = this.state.exports.results[i]; + if (exp.status < 3) { + return true; + } + } + return false; + } else { + return false; + } + }, setPublic: function(exportId) { // Basically what we do is to set this export to public first, and then set all @@ -672,15 +724,33 @@ function loadComponents() { this.setState({actionInProgress: true}); apiCall('PATCH', exportUrl.replace('{iati_export}', exportId), publicData, exportUpdated); }, + + renderRefreshing: function() { + if (this.state.refreshing) { + return ( + + {cap(i18n.refreshing) + ' ' + i18n.iati_exports + '...'} + + ); + } else if (this.pendingOrInProgress()) { + return ( + + {cap(i18n.pending_or_progress) + '. ' + cap(i18n.refreshing) + ' ' + i18n.in + ' ' + this.state.refreshingIn + ' ' + i18n.seconds + '...'} + + ); + } else { + return ( + + ); + } + }, render: function() { var initOrTable, - refreshing, exportCount, exportCountString, lastExportDescription; - refreshing = this.state.refreshing ? {' ' + cap(i18n.refreshing) + ' ' + i18n.iati_exports + '...'} : ; exportCount = !this.state.initializing ? this.state.exports.count : null; exportCountString = (exportCount !== null && exportCount > 0) ? ' ' + this.state.exports.count + ' ' : ' '; @@ -688,7 +758,7 @@ function loadComponents() { // Only show a message that data is being loading when initializing initOrTable = {' ' + cap(i18n.loading) + ' ' + i18n.last + ' ' + i18n.iati_exports + '...'}; } else { - // Show a table of existing imports (max 10) when the data has been loaded + // Show a table of existing imports when the data has been loaded initOrTable = React.createElement(ExportsTable, { exports: this.state.exports, refreshing: this.state.refreshing, @@ -709,7 +779,7 @@ function loadComponents() {

{cap(i18n.last) + exportCountString + i18n.iati_exports}

{lastExportDescription} - {refreshing} + {this.renderRefreshing()} {initOrTable}
); diff --git a/akvo/templates/myrsr/my_iati.html b/akvo/templates/myrsr/my_iati.html index 0ec0467c51..db4d64623e 100644 --- a/akvo/templates/myrsr/my_iati.html +++ b/akvo/templates/myrsr/my_iati.html @@ -71,6 +71,9 @@

{% trans 'Select an organisation' %}

"perform_checks_description_2": "{% trans 'Projects with all mandatory IATI information filled in will be marked green and projects with missing information will be marked red.' %}", "loading": "{% trans 'loading' %}", "refreshing": "{% trans 'refreshing' %}", + "pending_or_progress": "{% trans 'there is at least one export pending or in progress' %}", + "in": "{% trans 'in' %}", + "seconds": "{% trans 'seconds' %}", "last": "{% trans 'last' %}", "view_public_file": "{% trans 'view public file' %}", "view_file": "{% trans 'view file' %}", @@ -81,6 +84,7 @@

{% trans 'Select an organisation' %}

"status": "{% trans 'status' %}", "number_of_projects": "{% trans '# of projects' %}", "actions": "{% trans 'actions' %}", + "processed": "{% trans 'processed' %}", "no_iati_file": "{% trans 'no IATI file' %}", "no_projects_1": "{% trans 'Your organisation does not report any projects yet.' %}", "no_projects_2": "{% trans 'Set your organisation to reporting organisation of a project to create an IATI file for the project.' %}", @@ -109,9 +113,10 @@

{% trans 'Select an organisation' %}