Skip to content

Commit

Permalink
[#2102] First setup of new 'My IATI' page
Browse files Browse the repository at this point in the history
  • Loading branch information
KasperBrandt committed Apr 14, 2016
1 parent c3ea6af commit baaf98f
Show file tree
Hide file tree
Showing 6 changed files with 660 additions and 112 deletions.
7 changes: 5 additions & 2 deletions akvo/rest/serializers/iati_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
# See more details in the license.txt file located at the root folder of the Akvo RSR module.
# For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.


from akvo.rest.serializers.rsr_serializer import BaseRSRSerializer
from akvo.rsr.models import IatiActivityExport, IatiExport

from .rsr_serializer import BaseRSRSerializer
from rest_framework import serializers


class IatiExportSerializer(BaseRSRSerializer):

user_name = serializers.Field(source='user.get_full_name')
status_label = serializers.Field(source='show_status')

class Meta:
model = IatiExport

Expand Down
289 changes: 267 additions & 22 deletions akvo/rsr/static/scripts-src/my-iati.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

var csrftoken,
endpoints,
months,
i18n;

/* CSRF TOKEN (this should really be added in base.html, we use it everywhere) */
Expand All @@ -27,7 +28,7 @@ function getCookie(name) {
csrftoken = getCookie('csrftoken');

/* Capitalize the first character of a string */
function capitalizeFirstLetter(string) {
function cap(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}

Expand All @@ -45,17 +46,30 @@ function apiCall(method, url, data, successCallback, retries) {
var success = function(newResponse) {
var oldResults = response.results;
response.results = oldResults.concat(newResponse.results);
return successCallback(response);
if (successCallback !== undefined) {
return successCallback(response);
} else {
return false;
}
};
apiCall(method, response.next, data, success);
} else {
return successCallback(response);
if (successCallback !== undefined) {
return successCallback(response);
} else {
return false;
}
}
} else {
return successCallback(response);
if (successCallback !== undefined) {
return successCallback(response);
} else {
return false;
}
}
} else {
var message = i18nResults.general_error + ': ';
// var message = i18nResults.general_error + ': ';
var message = 'general error: ';
for (var key in response) {
if (response.hasOwnProperty(key)) {
message += response[key] + '. ';
Expand Down Expand Up @@ -115,6 +129,47 @@ function loadAsync(url, callback, retryCount, retryLimit) {
xmlHttp.send();
}

function getDateDescription(month) {
switch (month) {
case 0:
return months.january;
case 1:
return months.february;
case 2:
return months.march;
case 3:
return months.april;
case 4:
return months.may;
case 5:
return months.june;
case 6:
return months.july;
case 7:
return months.august;
case 8:
return months.september;
case 9:
return months.october;
case 10:
return months.november;
case 11:
return months.december;
}
}

function displayDate(dateString) {
// Display a dateString like "25 Jan 2016"
if (dateString !== undefined && dateString !== null) {
var date = new Date(dateString.split(".")[0].replace("/", /-/g));
var day = date.getUTCDate();
var month = getDateDescription(date.getUTCMonth());
var year = date.getUTCFullYear();
return day + " " + month + " " + year;
}
return i18n.unknown_date;
}

function processResponse(label, response) {
var label_content, checks, all_checks_passed, span, checks_response;

Expand Down Expand Up @@ -217,20 +272,102 @@ function loadComponents() {
}
});

var ExportRow = React.createClass({displayName: 'ExportRow',
openPublicFile: function() {
window.open(i18n.last_exports_url, '_blank');
},

openFile: function() {
window.open(endpoints.base_url + '/media/' + this.props.exp.iati_file, '_blank');
},

setPublic: function() {
this.props.setPublic(this.props.exp.id);
},

renderActions: function() {
if (this.props.publicFile) {
return (
React.DOM.button( {className:"btn btn-success btn-sm", onClick:this.openPublicFile},
React.DOM.i( {className:"fa fa-globe"} ), " ", cap(i18n.view_public_file)
)
);
} else if (this.props.exp.iati_file) {
return (
React.DOM.div(null,
React.DOM.button( {className:"btn btn-default btn-sm", onClick:this.openFile},
React.DOM.i( {className:"fa fa-code"} ), " ", cap(i18n.view_file)
),
React.DOM.button( {className:"btn btn-default btn-sm", onClick:this.setPublic},
React.DOM.i( {className:"fa fa-globe"} ), " ", cap(i18n.set_public)
)
)
);
} else {
return (
React.DOM.button( {className:"btn btn-default btn-sm disabled"},
React.DOM.i( {className:"fa fa-globe"} ), " ", cap(i18n.no_iati_file)
)
);
}
},

renderRowClass: function() {
if (this.props.publicFile) {
return 'publicFile';
} else if (this.props.exp.status === 2) {
return 'inProgress';
} else if (this.props.exp.status === 4 || this.props.exp.iati_file === '') {
return 'cancelled';
} else {
return '';
}
},

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.props.exp.user_name),
React.DOM.td(null, displayDate(this.props.exp.created_at)),
React.DOM.td(null, 'v' + this.props.exp.version),
React.DOM.td( {className:"text-right"}, this.renderActions())
)
);
}
});

var ExportsTable = React.createClass({displayName: 'ExportsTable',
render: function() {
var thisTable = this;

var exports = this.props.exports.results.map(function(exp) {
var publicFile = thisTable.props.publicFile === exp.id;

return React.createElement(ExportRow, {
key: exp.id,
exp: exp,
publicFile: publicFile,
setPublic: thisTable.props.setPublic
});
});


return (
React.DOM.table( {className:"table table-striped table-responsive myProjectList"},
React.DOM.table( {className:"table table-striped table-responsive myProjectList topMargin"},
React.DOM.thead(null,
React.DOM.tr(null,
React.DOM.th(null, "Last export"),
React.DOM.th(null, "User"),
React.DOM.th(null, "Created at"),
React.DOM.th(null, "IATI version"),
React.DOM.th(null, "status"),
React.DOM.th(null, "IATI file"),
React.DOM.th(null, "Number of projects")
React.DOM.th(null, cap(i18n.status)),
React.DOM.th(null, i18n.number_of_projects),
React.DOM.th(null, cap(i18n.created_by)),
React.DOM.th(null, cap(i18n.created_at)),
React.DOM.th(null, i18n.iati_version),
React.DOM.th( {className:"text-right"}, cap(i18n.actions))
)
),
React.DOM.tbody(null,
exports
)
)
);
Expand All @@ -242,7 +379,8 @@ function loadComponents() {
return {
exports: null,
initializing: true,
refreshing: false
refreshing: false,
actionInProgress: false
};
},

Expand Down Expand Up @@ -277,16 +415,122 @@ function loadComponents() {
}
},

publicFile: function() {
if (this.state.exports === null) {
return null;
} else {
for (var i = 0; i < this.state.exports.results.length; i++) {
var exp = this.state.exports.results[i];
if (exp.iati_file !== '' && exp.is_public) {
return exp.id;
}
}
return null;
}
},

findExport: function(exportId) {
for (var i = 0; i < this.state.exports.results.length; i++) {
var exp = this.state.exports.results[i];
if (exp.id === exportId) {
return exp;
}
}
return null;
},

setPublic: function(exportId) {
// Basically what we do is to set this export to public first, and then set all
// newer exports to private. This automatically makes this export the public export.
var thisApp = this,
exportUrl = endpoints.base_url + endpoints.iati_export,
thisExport = this.findExport(exportId),
newerExports = [],
newerExportsUpdated = 0,
publicData = JSON.stringify({'is_public': true}),
privateData = JSON.stringify({'is_public': false});

function allExportsUpdated() {
thisApp.loadExports(false);
thisApp.setState({actionInProgress: false});
}

function exportUpdated(response) {
newerExportsUpdated++;
if (newerExportsUpdated === newerExports.length) {
allExportsUpdated();
}
}

// Set current IATI export to public
this.setState({actionInProgress: true});
apiCall('PATCH', exportUrl.replace('{iati_export}', exportId), publicData);

// Find the newer IATI exports
for (var i = 0; i < this.state.exports.results.length; i++) {
var newerExp = this.state.exports.results[i];
if (newerExp.id !== exportId && newerExp.created_at > thisExport.created_at) {
newerExports.push(newerExp);
}
}

// Update the newer IATI exports
if (newerExports.length > 0) {
for (var j = 0; j < newerExports.length; j++) {
var exp = newerExports[j];
apiCall('PATCH', exportUrl.replace('{iati_export}', exp.id), privateData, exportUpdated);
}
} else {
allExportsUpdated();
}
},

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 + ' ' : ' ';

if (this.state.initializing) {
// 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 if (exportCount > 0) {
// Show a table of exiting imports (max 10) when the data has been loaded and exports exist
initOrTable = React.createElement(ExportsTable, {
exports: this.state.exports,
refreshing: this.state.refreshing,
publicFile: this.publicFile(),
setPublic: this.setPublic
});
} else {
// Do not show the 'Last exports' part when no exports exist yet
return (
React.DOM.div(null,
React.DOM.h4( {className:"topMargin"}, cap(i18n.new) + ' ' + i18n.iati_export)
)
);
}

lastExportDescription = React.DOM.div( {className:"lastExportDescription"},
React.DOM.span(null, cap(i18n.last_exports_1)),
React.DOM.a( {href:i18n.last_exports_url, target:"_blank"}, i18n.last_exports_url),
React.DOM.span(null, '. ' + cap(i18n.last_exports_2) + ' ' + cap(i18n.last_exports_3)),
React.DOM.a( {href:"http://iatiregistry.org", target:"_blank"}, i18n.iati_registry),
React.DOM.span(null, i18n.last_exports_4)
);

return (
React.DOM.div(null,
React.DOM.h4( {className:"topMargin"}, capitalizeFirstLetter(i18n.last_ten) + ' ' + i18n.iati_exports),
React.createElement(ExportsTable, {
exports: this.state.exports,
initializing: this.state.initializing,
refreshing: this.state.refreshing
}),
React.DOM.h4( {className:"topMargin"}, capitalizeFirstLetter(i18n.new) + ' ' + i18n.iati_export)
React.DOM.h4( {className:"topMargin"}, cap(i18n.last) + exportCountString + i18n.iati_exports),
lastExportDescription,
refreshing,
initOrTable,
React.DOM.h4( {className:"topMargin"}, cap(i18n.new) + ' ' + i18n.iati_export)
)
);
}
Expand Down Expand Up @@ -329,8 +573,9 @@ function loadAndRenderReact() {
}

document.addEventListener('DOMContentLoaded', function() {
i18n = JSON.parse(document.getElementById("translations").innerHTML);
endpoints = JSON.parse(document.getElementById("endpoints").innerHTML);
months = JSON.parse(document.getElementById("months").innerHTML);
i18n = JSON.parse(document.getElementById("translations").innerHTML);

if (document.getElementById('myIATIContainer')) {
if (typeof React !== 'undefined' && typeof ReactDOM !== 'undefined') {
Expand Down
Loading

0 comments on commit baaf98f

Please sign in to comment.