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}

+ + + + + + + + + {periods} +
{i18n.period}{i18n.target_value}{i18n.actual_value} +
+
+ ); + } +}); - 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 ( +
+ +

{this.props.indicator.title}

+
+
+ ); } - 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 %} -
- {% trans "Parent project:" %} {{ result.parent_result.project.title }} -
- {% endif %} - {% for child_result in result.child_results.all %} -
- {% trans "Linked child project:" %} {{ child_result.project.title }} -
- {% endfor %} -
- {% if result.description %} -
-

{% trans "Description" %}

-
- {{result.description}} -
-
- {% endif %} -
-

{% trans "Indicators" %} ({{result.indicators.all.count}})

-
    - {% for indicator in result.indicators.all %} - - {% 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" %}

- - - - - - - - - - - - - {% for period in indicator.periods.all %} - - - - - - - - - - - - - - {% endfor %} -
{% trans "Start" %}{% trans "End" %}{% trans "Progress" %}{% trans "Target" %}
{{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 %} -
-
-
-
+

{{ project.title }}

+
{% endblock %} +{#{% block myrsr_main %}#} +{#
#} +{#
#} +{#
#} +{#
#} +{# {% for result in project.results.all %}#} +{#
#} +{#

{% trans 'Result' %}

#} +{#
#} +{#

{{result.title}}

#} +{# {% if result.parent_result %}#} +{#
#} +{# {% trans "Parent project:" %} {{ result.parent_result.project.title }}#} +{#
#} +{# {% endif %}#} +{# {% for child_result in result.child_results.all %}#} +{#
#} +{# {% trans "Linked child project:" %} {{ child_result.project.title }}#} +{#
#} +{# {% endfor %}#} +{#
#} +{# {% if result.description %}#} +{#
#} +{#

{% trans "Description" %}

#} +{#
#} +{# {{result.description}}#} +{#
#} +{#
#} +{# {% endif %}#} +{#
#} +{#

{% trans "Indicators" %} ({{result.indicators.all.count}})

#} +{#
    #} +{# {% for indicator in result.indicators.all %}#} +{# #} +{# {% 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" %}

#} +{##} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# {% for period in indicator.periods.all %}#} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# #} +{# {% endfor %}#} +{#
{% trans "Start" %}{% trans "End" %}{% trans "Progress" %}{% trans "Target" %}
{{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 %}#} +{#
#} +{#
#} +{#
#} +{#
#} +{#{% 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