diff --git a/akvo/rsr/models/user.py b/akvo/rsr/models/user.py
index a9f079fc92..e8116f2274 100644
--- a/akvo/rsr/models/user.py
+++ b/akvo/rsr/models/user.py
@@ -388,12 +388,31 @@ def employments_dict(self, org_list):
employments=employments_array,
)
+ def has_role_in_org(self, org, group):
+ """
+ Helper function to determine if a user is in a certain group at an organisation.
+
+ :param org; an Organisation instance.
+ :param group; a Group instance.
+ """
+ if self.approved_employments().filter(organisation=org, group=group).exists():
+ return True
+ return False
+
def admin_of(self, org):
"""
Checks if the user is an Admin of this organisation.
+
+ :param org; an Organisation instance.
"""
admin_group = Group.objects.get(name='Admins')
- for employment in Employment.objects.filter(user=self, group=admin_group):
- if employment.organisation == org:
- return True
- return False
+ return self.has_role_in_org(org, admin_group)
+
+ def project_editor_of(self, org):
+ """
+ Checks if the user is a Project editor of this organisation.
+
+ :param org; an Organisation instance
+ """
+ editor_group = Group.objects.get(name='Project editors')
+ return self.has_role_in_org(org, editor_group)
diff --git a/akvo/rsr/static/scripts-src/results-data.js b/akvo/rsr/static/scripts-src/results-data.js
index 9bbc5eee9d..edc74abcc0 100644
--- a/akvo/rsr/static/scripts-src/results-data.js
+++ b/akvo/rsr/static/scripts-src/results-data.js
@@ -5,58 +5,7 @@
// Akvo RSR module. For additional details on the GNU license please see
// < http://www.gnu.org/licenses/agpl.html >.
-var i18n = JSON.parse(document.getElementById("project-main-text").innerHTML);
-
-///////// RESULTS FRAMEWORK /////////
-
- // Default values
- var defaultValues = JSON.parse(document.getElementById("default-values").innerHTML);
- var currentDate = defaultValues.current_datetime;
-
- /* Set the click listeners that expand or hide the full
- ** "Results" entries in the sidebar.
- */
- function setResultExpandOnClicks() {
- var els = document.querySelectorAll('.sidebar .result-expand');
- var el = null;
- var resultID = null;
-
- for (var i = 0; i < els.length; i++) {
- el = els[i];
-
- el.addEventListener('click', function() {
- if (!this.parentNode.classList.contains('expanded')) {
- // Expand this result!
-
- // Collapse any expanded results in the sidebar
- removeClassFromAll('.result-nav', 'expanded');
-
- // Expand this result in the sidebar
- this.parentNode.classList.add('expanded');
-
- // Hide all indicators in the main panel
- hideAll('.indicator');
-
- // Expand the appropriate result in the main panel
- resultID = this.getAttribute('data-result-id');
- showResultsSummary(resultID);
- } else {
-
- // Collapse all result entries in the sidebar
- removeClassFromAll('.result-nav', 'expanded');
-
- // Hide all result summaries in the main panel
- hideAllResultsSummaries();
-
- // Hide all indicators in the main panel
- hideAll('.indicator-group, .indicator');
-
- // Remove the "active" status of any indicators in main panel
- removeClassFromAll('.indicator-nav.active', 'active');
- }
- });
- }
- }
+var currentDate, endpoints, i18n, initialSettings;
/* Show the results summary in the main panel when the
** appropriate side bar link is activated.
@@ -1380,115 +1329,358 @@ var i18n = JSON.parse(document.getElementById("project-main-text").innerHTML);
return updateContainer;
}
- function readMoreOnClicks() {
- function setReadMore(show, hide) {
- return function(e) {
- e.preventDefault();
- hide.classList.add('hidden');
- show.classList.remove('hidden');
- };
+var IndicatorPeriodEntry = React.createClass({displayName: 'IndicatorPeriodEntry',
+ selected: function() {
+ if (this.props.selectedPeriod !== null) {
+ return this.props.selectedPeriod.id === this.props.period.id;
+ } else {
+ return false;
+ }
+ },
+
+ switchPeriod: function() {
+ var selectPeriod = this.props.selectPeriod;
+ this.selected() ? selectPeriod(null) : selectPeriod(this.props.period);
+ },
+
+ render: function() {
+ return (
+ React.DOM.tr(null,
+ React.DOM.td( {className:"period-td"},
+ React.DOM.a( {className:"clickable", onClick:this.switchPeriod},
+ this.props.period.period_start, " - ", this.props.period.period_end
+ )
+ ),
+ React.DOM.td( {className:"target-td"}, this.props.period.target_value),
+ React.DOM.td( {className:"actual-td"},
+ this.props.period.actual_value,
+ React.DOM.span( {className:"percentage-complete"}, " (100%)")
+ ),
+ React.DOM.td( {className:"actions-td"},
+ React.DOM.a( {className:"clickable"}, i18n.update)
+ )
+ )
+ );
}
+});
+
+var IndicatorPeriodList = React.createClass({displayName: 'IndicatorPeriodList',
+ render: function() {
+ var thisList = this;
+
+ var periods = this.props.indicator.periods.map(function (period) {
+ return (
+ React.DOM.tbody( {className:"indicator-period bg-transition", key:period.id},
+ React.createElement(IndicatorPeriodEntry, {
+ period: period,
+ selectedPeriod: thisList.props.selectedPeriod,
+ selectPeriod: thisList.props.selectPeriod
+ })
+ )
+ );
+ });
+
+ return (
+ React.DOM.div( {className:"indicator-period-list"},
+ React.DOM.h4( {className:"indicator-periods-title"}, i18n.indicator_periods),
+ React.DOM.table( {className:"table table-responsive"},
+ React.DOM.thead(null,
+ React.DOM.tr(null,
+ React.DOM.td( {className:"th-period"}, i18n.period),
+ React.DOM.td( {className:"th-target"}, i18n.target_value),
+ React.DOM.td( {className:"th-actual"}, i18n.actual_value),
+ React.DOM.td( {className:"th-actions"} )
+ )
+ ),
+ periods
+ )
+ )
+ );
+ }
+});
- var summaryReadMore = document.getElementById('summary-truncated').querySelector('.read-more');
- var summaryReadLess = document.getElementById('summary-full').querySelector('.read-less');
- summaryReadMore.onclick = setReadMore(summaryReadLess.parentNode, summaryReadMore.parentNode);
- summaryReadLess.onclick = setReadMore(summaryReadMore.parentNode, summaryReadLess.parentNode);
- }
+var MainContent = React.createClass({displayName: 'MainContent',
+ getInitialState: function() {
+ return {
+ selectedPeriod: null
+ };
+ },
+
+ selectPeriod: function(period) {
+ this.setState({selectedPeriod: period})
+ },
+
+ deselectPeriod: function() {
+ this.setState({selectedPeriod: null})
+ },
+
+ showMeasure: function() {
+ switch(this.props.indicator.measure) {
+ case "1":
+ return i18n.unit;
+ case "2":
+ return i18n.percentage;
+ default:
+ return "";
+ }
+ },
+
+ render: function() {
+ if (this.state.selectedPeriod !== null) {
+ return (
+ React.DOM.span(null,
+ "Selected a period!",
+ React.DOM.a( {className:"clickable", onClick:this.deselectPeriod}, "Go back.")
+ )
+ );
+ } else if (this.props.indicator !== null) {
+ return (
+ React.DOM.div( {className:"indicator opacity-transition"},
+ React.DOM.h4( {className:"indicator-title"},
+ React.DOM.i( {className:"fa fa-tachometer"} ),
+ this.props.indicator.title,
+ "(",this.showMeasure(),")"
+ ),
+ React.DOM.div( {className:"indicator-description"},
+ this.props.indicator.description
+ ),
+ React.DOM.dl( {className:"baseline"},
+ React.DOM.div( {className:"baseline-year"},
+ React.DOM.dt(null, i18n.baseline_year),
+ React.DOM.dd(null, this.props.indicator.baseline_year)
+ ),
+ React.DOM.div( {className:"baseline-value"},
+ React.DOM.dt(null, i18n.baseline_value),
+ React.DOM.dd(null, this.props.indicator.baseline_value)
+ )
+ ),
+ React.createElement(IndicatorPeriodList, {
+ indicator: this.props.indicator,
+ selectedPeriod: this.state.selectedPeriod,
+ selectPeriod: this.selectPeriod
+ })
+ )
+ )
+ } else {
+ return (
+ React.DOM.span(null )
+ );
+ }
+ }
+});
- function setCurrentDate() {
- var interval = setInterval(function(){
- var localCurrentDate = new Date(currentDate);
- localCurrentDate.setSeconds(localCurrentDate.getSeconds() + 1);
- currentDate = localCurrentDate.toString();
- }, 1000);
- }
+var IndicatorEntry = React.createClass({displayName: 'IndicatorEntry',
+ selected: function() {
+ if (this.props.selectedIndicator !== null) {
+ return this.props.selectedIndicator.id === this.props.indicator.id;
+ } else {
+ return false;
+ }
+ },
- function showTab(tabClass) {
- var allTabs = document.querySelectorAll('.project-tab');
- var allTabLinks = document.querySelectorAll('.tab-link.selected');
- var activeTab = document.querySelector('.' + tabClass);
- var activeTabLink = document.querySelector('.tab-link[href="#' + tabClass + '"]');
+ switchIndicator: function() {
+ var selectIndicator = this.props.selectIndicator;
+ this.selected() ? selectIndicator(null) : selectIndicator(this.props.indicator);
+ },
- for (var i = 0; i < allTabs.length; i++) {
- var tab = allTabs[i];
+ render: function() {
+ var indicatorClass = "indicator-nav clickable bg-border-transition";
+ if (this.selected()) {
+ indicatorClass += " active"
+ }
- tab.style.display = 'none';
+ return (
+ React.DOM.div( {className:indicatorClass, onClick:this.switchIndicator, key:this.props.indicator.id},
+ React.DOM.a(null,
+ React.DOM.h4(null, this.props.indicator.title)
+ )
+ )
+ );
}
- for (var j = 0; j < allTabLinks.length; j++) {
- var tabLink = allTabLinks[j];
+});
- tabLink.classList.remove('selected');
- }
+var ResultEntry = React.createClass({displayName: 'ResultEntry',
+ expanded: function() {
+ if (this.props.selectedResult !== null) {
+ return this.props.selectedResult.id === this.props.result.id;
+ } else {
+ return false;
+ }
+ },
- activeTab.style.display = 'block';
- activeTabLink.classList.add('selected');
- }
+ switchResult: function() {
+ var selectResult = this.props.selectResult;
+ this.expanded() ? selectResult(null) : selectResult(this.props.result);
+ },
- function setTabOnClicks() {
- var allTabs = document.querySelectorAll('.tab-link');
+ render: function() {
+ var indicatorCount, indicatorEntries;
- for (var i = 0; i < allTabs.length; i++) {
- var tab = allTabs[i];
+ if (this.expanded()) {
+ indicatorCount = React.DOM.span(null );
+ } else {
+ indicatorCount = React.DOM.span( {className:"result-indicator-count"},
+ React.DOM.i( {className:"fa fa-tachometer"} ),
+ React.DOM.span( {className:"indicator-count"}, this.props.result.indicators.length)
+ );
+ }
- tab.addEventListener('click', function() {
- var tabClass = this.getAttribute('href');
+ if (this.expanded()) {
+ var thisResult = this;
+ indicatorEntries = this.props.result.indicators.map(function (indicator) {
+ return (
+ React.DOM.div( {key:indicator.id},
+ React.createElement(IndicatorEntry, {
+ indicator: indicator,
+ selectedIndicator: thisResult.props.selectedIndicator,
+ selectIndicator: thisResult.props.selectIndicator
+ })
+ )
+ );
+ });
+ indicatorEntries = React.DOM.div( {className:"result-nav-full clickable"}, indicatorEntries);
+ } else {
+ indicatorEntries = React.DOM.span(null );
+ }
- // Remove the '#' from the href
- tabClass = tabClass.substring(1);
- showTab(tabClass);
- });
+ var resultNavClass = "result-nav bg-transition";
+ resultNavClass += this.expanded() ? " expanded" : "";
+
+ return (
+ React.DOM.div( {className:resultNavClass, key:this.props.result.id},
+ React.DOM.div( {className:"result-nav-summary clickable", onClick:this.switchResult},
+ React.DOM.h3( {className:"result-title"},
+ React.DOM.i( {className:"fa fa-chevron-down"}),
+ React.DOM.i( {className:"fa fa-chevron-up"}),
+ React.DOM.span(null, this.props.result.title)
+ ),
+ indicatorCount
+ ),
+ indicatorEntries
+ )
+ );
}
- }
-
- function readTabFromFragment() {
- var fragment = window.location.hash;
- var parameters = window.location.search;
-
- if (fragment || parameters.indexOf('?page') > -1) {
- if (parameters.indexOf('?page') > -1) {
- // KB: Hack, only the updates tab has a 'page' parameter
- fragment = 'updates';
- } else {
- // Remove the '#' from the fragment
- fragment = fragment.substring(1);
- }
-
- if (fragment === 'summary' || fragment === 'report' || fragment === 'finance') {
- showTab(fragment);
- } else if (fragment === 'partners' && defaultValues.show_partners_tab) {
- showTab(fragment);
- } else if (fragment === 'results' && defaultValues.show_results_tab) {
- showTab(fragment);
- } else if (fragment === 'updates' && defaultValues.show_updates_tab) {
- showTab(fragment);
- } else {
- showTab('summary');
- }
- } else {
- showTab('summary');
+});
+
+var SideBar = React.createClass({displayName: 'SideBar',
+ render: function() {
+ var thisList = this;
+ var resultEntries = this.props.results.map(function (result) {
+ return (
+ React.DOM.div( {key:result.id},
+ React.createElement(ResultEntry, {
+ result: result,
+ selectedIndicator: thisList.props.selectedIndicator,
+ selectedResult: thisList.props.selectedResult,
+ selectIndicator: thisList.props.selectIndicator,
+ selectResult: thisList.props.selectResult
+ })
+ )
+ );
+ });
+
+ return (
+ React.DOM.div( {className:"results-list"},
+ resultEntries
+ )
+ );
}
- }
+});
+
+var ResultsApp = React.createClass({displayName: 'ResultsApp',
+ getInitialState: function() {
+ return {
+ selectedResult: null,
+ selectedIndicator: null,
+ results: []
+ };
+ },
+
+ componentDidMount: function() {
+ // Load results data
+ var xmlHttp = new XMLHttpRequest();
+ var thisApp = this;
+ xmlHttp.onreadystatechange = function() {
+ if (xmlHttp.readyState == XMLHttpRequest.DONE && xmlHttp.status == 200) {
+ thisApp.setState({'results': JSON.parse(xmlHttp.responseText).results});
+ }
+ };
+ xmlHttp.open("GET", endpoints.base_url + endpoints.results, true);
+ xmlHttp.send();
+ },
+
+ selectResult: function(resultId) {
+ this.setState({selectedResult: resultId});
+ },
+
+ selectIndicator: function(indicatorId) {
+ this.setState({selectedIndicator: indicatorId});
+ },
+
+ render: function() {
+ return (
+ React.DOM.div( {className:"results"},
+ React.DOM.article(null,
+ React.DOM.div( {className:"results-container container"},
+ React.DOM.div( {className:"sidebar"},
+ React.DOM.div( {className:"result-nav-header"},
+ React.DOM.h3(null, i18n.results)
+ ),
+ React.createElement(
+ SideBar, {
+ results: this.state.results,
+ selectedIndicator: this.state.selectedIndicator,
+ selectedResult: this.state.selectedResult,
+ selectIndicator: this.selectIndicator,
+ selectResult: this.selectResult
+ }
+ )
+ ),
+ React.DOM.div( {className:"indicator-container"},
+ React.createElement(
+ MainContent, {
+ indicator: this.state.selectedIndicator
+ }
+ )
+ )
+ )
+ )
+ )
+ );
+ }
+});
- /* POLYFILLS */
+function setCurrentDate() {
+ currentDate = initialSettings.current_datetime;
+ setInterval(function () {
+ var localCurrentDate = new Date(currentDate);
+ localCurrentDate.setSeconds(localCurrentDate.getSeconds() + 1);
+ currentDate = localCurrentDate.toString();
+ }, 1000);
+}
+
+// Polyfill for element.closest() for IE and Safari
+this.Element && function(ElementPrototype) {
+ ElementPrototype.closest = ElementPrototype.closest ||
+ function (selector) {
+ var el = this;
+ while (el.matches && !el.matches(selector)) el = el.parentNode;
+ return el.matches ? el : null;
+ };
+}(Element.prototype);
- // Polyfill for element.closest() for IE and Safari
- this.Element && function(ElementPrototype) {
- ElementPrototype.closest = ElementPrototype.closest ||
- function(selector) {
- var el = this;
- while (el.matches && !el.matches(selector)) el = el.parentNode;
- return el.matches ? el : null;
- };
- }(Element.prototype);
+/* Initialise page */
+document.addEventListener('DOMContentLoaded', function() {
+ // Retrieve data endpoints, translations and page settings
+ endpoints = JSON.parse(document.getElementById('data-endpoints').innerHTML);
+ i18n = JSON.parse(document.getElementById('translation-texts').innerHTML);
+ initialSettings = JSON.parse(document.getElementById('initial-settings').innerHTML);
- /* Initialise page */
- document.addEventListener('DOMContentLoaded', function() {
setCurrentDate();
- // Setup results framework
- setResultExpandOnClicks();
- setIndicatorLinkOnClicks();
- setExpandIndicatorPeriodOnClicks();
- addAddOnClicks();
- buildUpdateJSON();
- });
\ No newline at end of file
+ // Initialize the 'My reports' app
+ ReactDOM.render(
+ React.createElement(ResultsApp),
+ document.getElementById('results-framework')
+ );
+});
\ No newline at end of file
diff --git a/akvo/rsr/static/scripts-src/results-data.jsx b/akvo/rsr/static/scripts-src/results-data.jsx
index 9bbc5eee9d..6cec321ccd 100644
--- a/akvo/rsr/static/scripts-src/results-data.jsx
+++ b/akvo/rsr/static/scripts-src/results-data.jsx
@@ -5,58 +5,7 @@
// Akvo RSR module. For additional details on the GNU license please see
// < http://www.gnu.org/licenses/agpl.html >.
-var i18n = JSON.parse(document.getElementById("project-main-text").innerHTML);
-
-///////// RESULTS FRAMEWORK /////////
-
- // Default values
- var defaultValues = JSON.parse(document.getElementById("default-values").innerHTML);
- var currentDate = defaultValues.current_datetime;
-
- /* Set the click listeners that expand or hide the full
- ** "Results" entries in the sidebar.
- */
- function setResultExpandOnClicks() {
- var els = document.querySelectorAll('.sidebar .result-expand');
- var el = null;
- var resultID = null;
-
- for (var i = 0; i < els.length; i++) {
- el = els[i];
-
- el.addEventListener('click', function() {
- if (!this.parentNode.classList.contains('expanded')) {
- // Expand this result!
-
- // Collapse any expanded results in the sidebar
- removeClassFromAll('.result-nav', 'expanded');
-
- // Expand this result in the sidebar
- this.parentNode.classList.add('expanded');
-
- // Hide all indicators in the main panel
- hideAll('.indicator');
-
- // Expand the appropriate result in the main panel
- resultID = this.getAttribute('data-result-id');
- showResultsSummary(resultID);
- } else {
-
- // Collapse all result entries in the sidebar
- removeClassFromAll('.result-nav', 'expanded');
-
- // Hide all result summaries in the main panel
- hideAllResultsSummaries();
-
- // Hide all indicators in the main panel
- hideAll('.indicator-group, .indicator');
-
- // Remove the "active" status of any indicators in main panel
- removeClassFromAll('.indicator-nav.active', 'active');
- }
- });
- }
- }
+var currentDate, endpoints, i18n, initialSettings;
/* Show the results summary in the main panel when the
** appropriate side bar link is activated.
@@ -1380,115 +1329,358 @@ var i18n = JSON.parse(document.getElementById("project-main-text").innerHTML);
return updateContainer;
}
- function readMoreOnClicks() {
- function setReadMore(show, hide) {
- return function(e) {
- e.preventDefault();
- hide.classList.add('hidden');
- show.classList.remove('hidden');
- };
+var IndicatorPeriodEntry = React.createClass({
+ selected: function() {
+ if (this.props.selectedPeriod !== null) {
+ return this.props.selectedPeriod.id === this.props.period.id;
+ } else {
+ return false;
+ }
+ },
+
+ switchPeriod: function() {
+ var selectPeriod = this.props.selectPeriod;
+ this.selected() ? selectPeriod(null) : selectPeriod(this.props.period);
+ },
+
+ render: function() {
+ return (
+
+
+
+ {this.props.period.period_start} - {this.props.period.period_end}
+
+ |
+ {this.props.period.target_value} |
+
+ {this.props.period.actual_value}
+ (100%)
+ |
+
+ {i18n.update}
+ |
+
+ );
}
+});
+
+var IndicatorPeriodList = React.createClass({
+ render: function() {
+ var thisList = this;
+
+ var periods = this.props.indicator.periods.map(function (period) {
+ return (
+
+ {React.createElement(IndicatorPeriodEntry, {
+ period: period,
+ selectedPeriod: thisList.props.selectedPeriod,
+ selectPeriod: thisList.props.selectPeriod
+ })}
+
+ );
+ });
+
+ return (
+
+
{i18n.indicator_periods}
+
+
+
+ {i18n.period} |
+ {i18n.target_value} |
+ {i18n.actual_value} |
+ |
+
+
+ {periods}
+
+
+ );
+ }
+});
- var summaryReadMore = document.getElementById('summary-truncated').querySelector('.read-more');
- var summaryReadLess = document.getElementById('summary-full').querySelector('.read-less');
- summaryReadMore.onclick = setReadMore(summaryReadLess.parentNode, summaryReadMore.parentNode);
- summaryReadLess.onclick = setReadMore(summaryReadMore.parentNode, summaryReadLess.parentNode);
- }
+var MainContent = React.createClass({
+ getInitialState: function() {
+ return {
+ selectedPeriod: null
+ };
+ },
+
+ selectPeriod: function(period) {
+ this.setState({selectedPeriod: period})
+ },
+
+ deselectPeriod: function() {
+ this.setState({selectedPeriod: null})
+ },
+
+ showMeasure: function() {
+ switch(this.props.indicator.measure) {
+ case "1":
+ return i18n.unit;
+ case "2":
+ return i18n.percentage;
+ default:
+ return "";
+ }
+ },
+
+ render: function() {
+ if (this.state.selectedPeriod !== null) {
+ return (
+
+ Selected a period!
+ Go back.
+
+ );
+ } else if (this.props.indicator !== null) {
+ return (
+
+
+
+ {this.props.indicator.title}
+ ({this.showMeasure()})
+
+
+ {this.props.indicator.description}
+
+
+
+
- {i18n.baseline_year}
+ - {this.props.indicator.baseline_year}
+
+
+
- {i18n.baseline_value}
+ - {this.props.indicator.baseline_value}
+
+
+ {React.createElement(IndicatorPeriodList, {
+ indicator: this.props.indicator,
+ selectedPeriod: this.state.selectedPeriod,
+ selectPeriod: this.selectPeriod
+ })}
+
+ )
+ } else {
+ return (
+
+ );
+ }
+ }
+});
- function setCurrentDate() {
- var interval = setInterval(function(){
- var localCurrentDate = new Date(currentDate);
- localCurrentDate.setSeconds(localCurrentDate.getSeconds() + 1);
- currentDate = localCurrentDate.toString();
- }, 1000);
- }
+var IndicatorEntry = React.createClass({
+ selected: function() {
+ if (this.props.selectedIndicator !== null) {
+ return this.props.selectedIndicator.id === this.props.indicator.id;
+ } else {
+ return false;
+ }
+ },
- function showTab(tabClass) {
- var allTabs = document.querySelectorAll('.project-tab');
- var allTabLinks = document.querySelectorAll('.tab-link.selected');
- var activeTab = document.querySelector('.' + tabClass);
- var activeTabLink = document.querySelector('.tab-link[href="#' + tabClass + '"]');
+ switchIndicator: function() {
+ var selectIndicator = this.props.selectIndicator;
+ this.selected() ? selectIndicator(null) : selectIndicator(this.props.indicator);
+ },
- for (var i = 0; i < allTabs.length; i++) {
- var tab = allTabs[i];
+ render: function() {
+ var indicatorClass = "indicator-nav clickable bg-border-transition";
+ if (this.selected()) {
+ indicatorClass += " active"
+ }
- tab.style.display = 'none';
+ return (
+
+ );
}
- for (var j = 0; j < allTabLinks.length; j++) {
- var tabLink = allTabLinks[j];
+});
- tabLink.classList.remove('selected');
- }
+var ResultEntry = React.createClass({
+ expanded: function() {
+ if (this.props.selectedResult !== null) {
+ return this.props.selectedResult.id === this.props.result.id;
+ } else {
+ return false;
+ }
+ },
- activeTab.style.display = 'block';
- activeTabLink.classList.add('selected');
- }
+ switchResult: function() {
+ var selectResult = this.props.selectResult;
+ this.expanded() ? selectResult(null) : selectResult(this.props.result);
+ },
- function setTabOnClicks() {
- var allTabs = document.querySelectorAll('.tab-link');
+ render: function() {
+ var indicatorCount, indicatorEntries;
- for (var i = 0; i < allTabs.length; i++) {
- var tab = allTabs[i];
+ if (this.expanded()) {
+ indicatorCount = ;
+ } else {
+ indicatorCount =
+
+ {this.props.result.indicators.length}
+ ;
+ }
- tab.addEventListener('click', function() {
- var tabClass = this.getAttribute('href');
+ if (this.expanded()) {
+ var thisResult = this;
+ indicatorEntries = this.props.result.indicators.map(function (indicator) {
+ return (
+
+ {React.createElement(IndicatorEntry, {
+ indicator: indicator,
+ selectedIndicator: thisResult.props.selectedIndicator,
+ selectIndicator: thisResult.props.selectIndicator
+ })}
+
+ );
+ });
+ indicatorEntries = {indicatorEntries}
;
+ } else {
+ indicatorEntries = ;
+ }
- // Remove the '#' from the href
- tabClass = tabClass.substring(1);
- showTab(tabClass);
- });
+ var resultNavClass = "result-nav bg-transition";
+ resultNavClass += this.expanded() ? " expanded" : "";
+
+ return (
+
+
+
+
+
+ {this.props.result.title}
+
+ {indicatorCount}
+
+ {indicatorEntries}
+
+ );
}
- }
-
- function readTabFromFragment() {
- var fragment = window.location.hash;
- var parameters = window.location.search;
-
- if (fragment || parameters.indexOf('?page') > -1) {
- if (parameters.indexOf('?page') > -1) {
- // KB: Hack, only the updates tab has a 'page' parameter
- fragment = 'updates';
- } else {
- // Remove the '#' from the fragment
- fragment = fragment.substring(1);
- }
-
- if (fragment === 'summary' || fragment === 'report' || fragment === 'finance') {
- showTab(fragment);
- } else if (fragment === 'partners' && defaultValues.show_partners_tab) {
- showTab(fragment);
- } else if (fragment === 'results' && defaultValues.show_results_tab) {
- showTab(fragment);
- } else if (fragment === 'updates' && defaultValues.show_updates_tab) {
- showTab(fragment);
- } else {
- showTab('summary');
- }
- } else {
- showTab('summary');
+});
+
+var SideBar = React.createClass({
+ render: function() {
+ var thisList = this;
+ var resultEntries = this.props.results.map(function (result) {
+ return (
+
+ {React.createElement(ResultEntry, {
+ result: result,
+ selectedIndicator: thisList.props.selectedIndicator,
+ selectedResult: thisList.props.selectedResult,
+ selectIndicator: thisList.props.selectIndicator,
+ selectResult: thisList.props.selectResult
+ })}
+
+ );
+ });
+
+ return (
+
+ {resultEntries}
+
+ );
}
- }
+});
+
+var ResultsApp = React.createClass({
+ getInitialState: function() {
+ return {
+ selectedResult: null,
+ selectedIndicator: null,
+ results: []
+ };
+ },
+
+ componentDidMount: function() {
+ // Load results data
+ var xmlHttp = new XMLHttpRequest();
+ var thisApp = this;
+ xmlHttp.onreadystatechange = function() {
+ if (xmlHttp.readyState == XMLHttpRequest.DONE && xmlHttp.status == 200) {
+ thisApp.setState({'results': JSON.parse(xmlHttp.responseText).results});
+ }
+ };
+ xmlHttp.open("GET", endpoints.base_url + endpoints.results, true);
+ xmlHttp.send();
+ },
+
+ selectResult: function(resultId) {
+ this.setState({selectedResult: resultId});
+ },
+
+ selectIndicator: function(indicatorId) {
+ this.setState({selectedIndicator: indicatorId});
+ },
+
+ render: function() {
+ return (
+
+
+
+
+
+
{i18n.results}
+
+ {React.createElement(
+ SideBar, {
+ results: this.state.results,
+ selectedIndicator: this.state.selectedIndicator,
+ selectedResult: this.state.selectedResult,
+ selectIndicator: this.selectIndicator,
+ selectResult: this.selectResult
+ }
+ )}
+
+
+ {React.createElement(
+ MainContent, {
+ indicator: this.state.selectedIndicator
+ }
+ )}
+
+
+
+
+ );
+ }
+});
- /* POLYFILLS */
+function setCurrentDate() {
+ currentDate = initialSettings.current_datetime;
+ setInterval(function () {
+ var localCurrentDate = new Date(currentDate);
+ localCurrentDate.setSeconds(localCurrentDate.getSeconds() + 1);
+ currentDate = localCurrentDate.toString();
+ }, 1000);
+}
+
+// Polyfill for element.closest() for IE and Safari
+this.Element && function(ElementPrototype) {
+ ElementPrototype.closest = ElementPrototype.closest ||
+ function (selector) {
+ var el = this;
+ while (el.matches && !el.matches(selector)) el = el.parentNode;
+ return el.matches ? el : null;
+ };
+}(Element.prototype);
- // Polyfill for element.closest() for IE and Safari
- this.Element && function(ElementPrototype) {
- ElementPrototype.closest = ElementPrototype.closest ||
- function(selector) {
- var el = this;
- while (el.matches && !el.matches(selector)) el = el.parentNode;
- return el.matches ? el : null;
- };
- }(Element.prototype);
+/* Initialise page */
+document.addEventListener('DOMContentLoaded', function() {
+ // Retrieve data endpoints, translations and page settings
+ endpoints = JSON.parse(document.getElementById('data-endpoints').innerHTML);
+ i18n = JSON.parse(document.getElementById('translation-texts').innerHTML);
+ initialSettings = JSON.parse(document.getElementById('initial-settings').innerHTML);
- /* Initialise page */
- document.addEventListener('DOMContentLoaded', function() {
setCurrentDate();
- // Setup results framework
- setResultExpandOnClicks();
- setIndicatorLinkOnClicks();
- setExpandIndicatorPeriodOnClicks();
- addAddOnClicks();
- buildUpdateJSON();
- });
\ No newline at end of file
+ // Initialize the 'My reports' app
+ ReactDOM.render(
+ React.createElement(ResultsApp),
+ document.getElementById('results-framework')
+ );
+});
\ No newline at end of file
diff --git a/akvo/rsr/static/styles-src/main.css b/akvo/rsr/static/styles-src/main.css
index a5b070a70a..af285509cd 100755
--- a/akvo/rsr/static/styles-src/main.css
+++ b/akvo/rsr/static/styles-src/main.css
@@ -2215,7 +2215,7 @@ body.translationBarActive div.skiptranslate ~ article, body.translationBarActive
margin-right: 1.4em; }
.results .indicator-container .indicator-group .table td {
vertical-align: middle; }
- .results .indicator-container .indicator-group .table td.fromTime, .results .indicator-container .indicator-group .table td.toTime, .results .indicator-container .indicator-group .table td.target-td, .results .indicator-container .indicator-group .table td.expand-td {
+ .results .indicator-container .indicator-group .table td.period-td, .results .indicator-container .indicator-group .table td.actual-td, .results .indicator-container .indicator-group .table td.target-td, .results .indicator-container .indicator-group .table td.actions-td {
white-space: nowrap; }
.results .indicator-container .indicator-group .table td.indicator-bar-td {
width: 55%; }
@@ -2229,7 +2229,7 @@ body.translationBarActive div.skiptranslate ~ article, body.translationBarActive
display: none; }
.results .indicator-container .indicator-group .indicator {
display: none; }
- .results .indicator-bar-td, .results .target-td {
+ .results .indicator-bar-td {
height: 80px;
padding: 0; }
.results .th-progress:before {
diff --git a/akvo/rsr/static/styles-src/main.scss b/akvo/rsr/static/styles-src/main.scss
index 177183368a..ea1ab81711 100755
--- a/akvo/rsr/static/styles-src/main.scss
+++ b/akvo/rsr/static/styles-src/main.scss
@@ -2851,7 +2851,7 @@ body {
}
td {
vertical-align: middle;
- &.fromTime, &.toTime, &.target-td, &.expand-td {
+ &.period-td, &.actual-td, &.target-td, &.actions-td {
white-space: nowrap;
}
&.indicator-bar-td {
@@ -2881,8 +2881,7 @@ body {
}
}
/* Styling for the indicator period progress bar */
- .indicator-bar-td,
- .target-td {
+ .indicator-bar-td {
height: 80px;
padding: 0;
}
diff --git a/akvo/rsr/views/my_rsr.py b/akvo/rsr/views/my_rsr.py
index 820311b798..08e25649ec 100644
--- a/akvo/rsr/views/my_rsr.py
+++ b/akvo/rsr/views/my_rsr.py
@@ -16,7 +16,7 @@
from django.core.urlresolvers import reverse
from django.forms.models import model_to_dict
from django.http import HttpResponseRedirect, Http404
-from django.shortcuts import render, render_to_response
+from django.shortcuts import get_object_or_404, render, render_to_response
from django.template import RequestContext
from ..forms import (PasswordForm, ProfileForm, UserOrganisationForm, UserAvatarForm,
@@ -409,67 +409,28 @@ def user_management(request):
@login_required
def results_data(request, project_id):
- """My results section."""
- def _get_indicator_updates_data(updates, child_projects, child=True):
- updates_list = []
- for update in updates:
- if child:
- indicator_period = update.indicator_period
- else:
- indicator_period = update.indicator_period.parent_period()
-
- updates_list.append({
- "id": update.pk,
- "indicator_period": {
- "id": indicator_period.pk if indicator_period else '',
- "target_value": str(indicator_period.target_value) if indicator_period else ''
- },
- "period_update": str(update.period_update),
- "created_at": str(update.created_at),
- "user": {
- "id": update.user.id,
- "first_name": update.user.first_name,
- "last_name": update.user.last_name,
- },
- "text": update.text,
- "photo": update.photo.url if update.photo else '',
- })
-
- for child_project in child_projects:
- updates = child_project.project_updates.select_related('user').order_by('-created_at').\
- filter(indicator_period__gt=0)
- child_updates_list = _get_indicator_updates_data(updates, child_project.children(), False)
- updates_list += child_updates_list
-
- return updates_list
-
- try:
- project = Project.objects.prefetch_related('results').get(pk=project_id)
- except Project.DoesNotExist:
- raise Http404
+ """
+ My results section. Only accessible to Admins and Project editors.
- if not request.user.is_anonymous() and (
- request.user.is_superuser or request.user.is_admin or
- True in [request.user.admin_of(partner) for partner in project.partners.all()]):
- project_admin = True
- else:
- project_admin = False
+ :param request; A Django HTTP request and context
+ :param project_id; The ID of the project
+ """
+ project = get_object_or_404(Project, pk=project_id)
+ user = request.user
+ is_admin = False
- # Updates
- updates = project.project_updates.prefetch_related('user').order_by('-created_at')
- narrative_updates = updates.exclude(indicator_period__isnull=False)
- indicator_updates = updates.filter(indicator_period__isnull=False)
+ if not user.has_perm('rsr.change_project', project):
+ raise PermissionDenied
- # JSON data
- indicator_updates_data = json.dumps(_get_indicator_updates_data(indicator_updates,
- project.children()))
+ # Check if user is an admin
+ if user.is_superuser or user.is_admin or \
+ True in [user.admin_of(partner) for partner in project.partners.all()]:
+ is_admin = True
context = {
- 'current_datetime': datetime.now(),
- 'indicator_updates': indicator_updates_data,
'project': project,
- 'project_admin': project_admin,
- 'updates': narrative_updates[:5] if narrative_updates else None,
+ 'current_datetime': datetime.now(),
+ 'is_admin': is_admin,
'update_timeout': settings.PROJECT_UPDATE_TIMEOUT,
}
diff --git a/akvo/templates/myrsr/results_data.html b/akvo/templates/myrsr/results_data.html
index 2178515ebb..9b451801b7 100644
--- a/akvo/templates/myrsr/results_data.html
+++ b/akvo/templates/myrsr/results_data.html
@@ -5,214 +5,183 @@
{% block title %}{% trans 'MyRSR - My Results' %}{% endblock %}
{% block myrsr_main %}
-
-
-
-
-
- {% for result in project.results.all %}
-
-
{% trans 'Result' %}
-
-
{{result.title}}
- {% if result.parent_result %}
-
- {% endif %}
- {% for child_result in result.child_results.all %}
-
- {% endfor %}
-
- {% if result.description %}
-
-
{% trans "Description" %}
-
- {{result.description}}
-
-
- {% endif %}
-
-
{% trans "Indicators" %} ({{result.indicators.all.count}})
-
- {% for indicator in result.indicators.all %}
- -
- {{indicator.title}}
-
- {% endfor %}
-
-
-
-
- {% for indicator in result.indicators.all %}
-
-
{{indicator.title}} ({% if indicator.measure == "2" %}{% trans "Percentage" %}{% else %}{% trans "Unit" %}{% endif %})
-
- {% if indicator.baseline_year or indicator.baseline_value %}
-
- {% if indicator.baseline_year %}
-
-
- {% trans "Baseline Year" %}
- - {{indicator.baseline_year}}
-
- {% endif %}
-
- {% if indicator.baseline_value %}
-
-
- {% trans "Baseline Value" %}
- - {{indicator.baseline_value}}
-
- {% endif %}
-
- {% endif %}
-
-
{% trans "Indicator periods" %}
-
-
-
-
- {% trans "Start" %} |
- {% trans "End" %} |
- {% trans "Progress" %} |
- {% trans "Target" %} |
- |
- |
-
-
- {% for period in indicator.periods.all %}
-
-
- {{period.period_start}} |
- {{period.period_end}} |
-
-
-
- {{indicator.baseline_value|floatformat:"0"}}
-
-
-
-
-
-
-
- |
-
-
- {{period.actual|floatformat:"0"}}
- / {{period.target_value}}
-
- |
- |
-
-
-
-
-
- |
-
-
-
- + {% trans 'Add a new update' %}
- |
-
-
- {% endfor %}
-
-
- {% endfor %}
-
- {% endfor %}
-
-
-
-
+ {{ project.title }}
+
{% endblock %}
+{#{% block myrsr_main %}#}
+{##}
+{#
#}
+{# #}
+{#
#}
+{# {% for result in project.results.all %}#}
+{#
#}
+{#
{% trans 'Result' %}
#}
+{#
#}
+{#
{{result.title}}
#}
+{# {% if result.parent_result %}#}
+{#
#}
+{# {% endif %}#}
+{# {% for child_result in result.child_results.all %}#}
+{#
#}
+{# {% endfor %}#}
+{#
#}
+{# {% if result.description %}#}
+{#
#}
+{#
{% trans "Description" %}
#}
+{#
#}
+{# {{result.description}}#}
+{#
#}
+{#
#}
+{# {% endif %}#}
+{#
#}
+{#
{% trans "Indicators" %} ({{result.indicators.all.count}})
#}
+{#
#}
+{# {% for indicator in result.indicators.all %}#}
+{# - #}
+{# {{indicator.title}}#}
+{#
#}
+{# {% endfor %}#}
+{#
#}
+{#
#}
+{#
#}
+{#
#}
+{# {% for indicator in result.indicators.all %}#}
+{#
#}
+{#
{{indicator.title}} ({% if indicator.measure == "2" %}{% trans "Percentage" %}{% else %}{% trans "Unit" %}{% endif %})
#}
+{##}
+{# {% if indicator.baseline_year or indicator.baseline_value %}#}
+{#
#}
+{# {% if indicator.baseline_year %}#}
+{# #}
+{#
- {% trans "Baseline Year" %}
#}
+{# - {{indicator.baseline_year}}
#}
+{# #}
+{# {% endif %}#}
+{##}
+{# {% if indicator.baseline_value %}#}
+{# #}
+{#
- {% trans "Baseline Value" %}
#}
+{# - {{indicator.baseline_value}}
#}
+{# #}
+{# {% endif %}#}
+{#
#}
+{# {% endif %}#}
+{##}
+{#
{% trans "Indicator periods" %}
#}
+{##}
+{#
#}
+{# #}
+{# #}
+{# {% trans "Start" %} | #}
+{# {% trans "End" %} | #}
+{# {% trans "Progress" %} | #}
+{# {% trans "Target" %} | #}
+{# | #}
+{# | #}
+{#
#}
+{# #}
+{# {% for period in indicator.periods.all %}#}
+{# #}
+{# #}
+{# {{period.period_start}} | #}
+{# {{period.period_end}} | #}
+{# #}
+{# #}
+{# #}
+{# {{indicator.baseline_value|floatformat:"0"}}#}
+{# #}
+{# #}
+{# #}
+{# #}
+{# #}
+{# #}
+{# #}
+{# #}
+{# #}
+{# #}
+{# #}
+{# | #}
+{# #}
+{# #}
+{# {{period.actual|floatformat:"0"}}#}
+{# / {{period.target_value}}#}
+{# #}
+{# | #}
+{# | #}
+{# #}
+{# #}
+{# #}
+{# #}
+{# #}
+{# | #}
+{#
#}
+{# #}
+{# #}
+{# + {% trans 'Add a new update' %}#}
+{# | #}
+{#
#}
+{# #}
+{# {% endfor %}#}
+{#
#}
+{#
#}
+{# {% endfor %}#}
+{#
#}
+{# {% endfor %}#}
+{#
#}
+{#
#}
+{# #}
+{#
#}
+{#{% endblock %}#}
+
{% block js %}
{{ block.super }}
- {# Updates information #}
-
+
+
+ {# Initial data endpoints #}
+
- {# Default values #}
-
{# Translation strings #}
-
- {# Slider library #}
-
-
-
{% compressed_js 'results_data' %}
{% endblock %}
\ No newline at end of file