From bae811e98d27a22b387115728a944382b4c41fa7 Mon Sep 17 00:00:00 2001 From: Kasper Brandt Date: Fri, 29 Jan 2016 14:52:36 +0100 Subject: [PATCH] [#1897] Add results framework to MyRSR --- akvo/rsr/static/scripts-src/results-data.js | 1494 +++++++++++++++++ akvo/rsr/static/scripts-src/results-data.jsx | 1494 +++++++++++++++++ akvo/rsr/static/styles-src/library.css | 2 +- akvo/rsr/static/styles-src/main.css | 207 +-- akvo/rsr/views/my_rsr.py | 72 + akvo/settings/40-pipeline.conf | 6 + akvo/templates/myrsr/my_projects.html | 4 +- akvo/templates/myrsr/myrsr_base.html | 3 + akvo/templates/myrsr/results_data.html | 218 +++ akvo/templates/project_main_tabs/results.html | 2 +- akvo/urls.py | 3 + 11 files changed, 3342 insertions(+), 163 deletions(-) create mode 100644 akvo/rsr/static/scripts-src/results-data.js create mode 100644 akvo/rsr/static/scripts-src/results-data.jsx create mode 100644 akvo/templates/myrsr/results_data.html diff --git a/akvo/rsr/static/scripts-src/results-data.js b/akvo/rsr/static/scripts-src/results-data.js new file mode 100644 index 0000000000..9bbc5eee9d --- /dev/null +++ b/akvo/rsr/static/scripts-src/results-data.js @@ -0,0 +1,1494 @@ +/** @jsx React.DOM */ + +// Akvo RSR is covered by the GNU Affero General Public License. +// 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 >. + +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'); + } + }); + } + } + + /* Show the results summary in the main panel when the + ** appropriate side bar link is activated. + */ + function showResultsSummary(id) { + var selector = '.result-' + id + '.result-summary'; + + hideAllResultsSummaries(); + fadeIn(selector); + } + + /* Hide all results summaries. */ + function hideAllResultsSummaries() { + hideAll('.result-summary'); + } + + /* Set the listeners to show the appropriate indicator in + ** the main panel when the sidebar link is clicked. + */ + function setIndicatorLinkOnClicks() { + var els = document.querySelectorAll('.indicator-nav, .indicator-link'); + + for (var i = 0; i < els.length; i++) { + el = els[i]; + + el.addEventListener('click', function() { + + /* Hide all the indicator and result summary elements */ + hideAll('.indicator-group, .indicator'); + hideAllResultsSummaries(); + + /* Now show the indicator group and indicator that + ** matches the element that's been clicked. + */ + var indicatorID = this.getAttribute('data-indicator-id'); + var indicatorSelector = '.indicator-' + indicatorID; + var indicatorGroupSelector = '.indicator-group.result-' + this.getAttribute('data-result-id'); + + // document.querySelector(indicatorGroupSelector).style.display = 'block'; + // document.querySelector(indicatorSelector).style.display = 'block'; + + fadeIn(indicatorGroupSelector); + fadeIn(indicatorSelector); + + /* Add an "active" class to this indicator in the sidebar for styling purposes */ + removeClassFromAll('.indicator-nav.active', 'active'); + document.querySelector('.indicator-nav[data-indicator-id="' + indicatorID + '"]').classList.add('active'); + + }); + } + } + /* Set the onClicks to expand a given indicator period and show the "Add update" dialog */ + function setExpandIndicatorPeriodOnClicks() { + var els = document.querySelectorAll('.expand-indicator-period'); + var el = null; + + for (var i = 0; i < els.length; i++) { + el = els[i]; + + el.addEventListener('click', function() { + var parentElement, periodId, periodNode; + + periodNode = this.closest('tbody'); + periodId = periodNode.getAttribute('period-id'); + parentElement = document.querySelector('.indicator-period-' + periodId + '-tr'); + + if (this.classList.contains('expanded')) { + /* This is period already expanded. Collapse it and remove all update dialogs. */ + removeFromPeriod(periodId, 'tr.update-dialog-container'); + displayAddButton(periodId, false); + removeClassFromAll('tr.expanded, .expand-indicator-period.expanded', 'expanded', periodId); + document.querySelector('.indicator-period-' + periodId + '-tr').parentNode.classList.remove('expanded'); + /* Remove the 'add' update in the store, in case the 'add Update' button was clicked */ + removeUpdatefromStore(periodId, 'add'); + } else { + var container; + var updateDialog = getUpdateDialog(periodId); + + displayAddButton(periodId, true); + updateDialog.style.display = 'none'; + parentElement.parentNode.appendChild(updateDialog); + fadeIn(updateDialog, true, 'table-row'); + + this.parentNode.parentNode.classList.add('expanded'); + this.parentNode.parentNode.parentNode.classList.add('expanded'); + this.classList.add('expanded'); + + addEditOnClicks(); + addDeleteOnClicks(); + } + }); + } + } + + function addAddOnClicks() { + var els = document.querySelectorAll('.add-button'); + + for (var i = 0; i < els.length; i++) { + var el = els[i]; + el.addEventListener('click', function() { + var addUpdateContainer, container, containerCell, newUpdate, periodId, periodNode, periodTarget; + + periodNode = this.closest('tbody'); + periodId = periodNode.getAttribute('period-id'); + periodTarget = periodNode.getAttribute('period-target'); + + newUpdate = { + id: 'add', + indicator_period: { + id: periodId, + target_value: periodTarget + }, + period_update: "0", + created_at: currentDate, + user: { + id: defaultValues.user_id, + first_name: defaultValues.user_first_name, + last_name: defaultValues.user_last_name + }, + text: '', + photo: '' + }; + addUpdateToStore(newUpdate); + + container = document.createElement('tr'); + container.classList.add('update-dialog-container'); + container.classList.add('pending-new-update'); + + containerCell = document.createElement('td'); + containerCell.setAttribute('colspan', '6'); + + addUpdateContainer = getUpdateEntry(newUpdate); + containerCell.appendChild(addUpdateContainer); + container.appendChild(containerCell); + container.style.opacity = 0; + + if (this.parentNode.parentNode.nextSibling) { + this.parentNode.parentNode.parentNode.insertBefore(container, this.parentNode.parentNode.nextSibling); + } else { + this.parentNode.parentNode.parentNode.appendChild(container); + } + fadeIn(container, true, 'table-row'); + + addEditOnClicks(); + addDeleteOnClicks(); + container.querySelector('.edit-button').click(); + displayAddButton(periodId, false); + }); + } + } + + function savingPeriod(periodNode, saving) { + var saveMessageContainer = periodNode.querySelector('.add-new-update-container'); + var saveMessageNode; + if (saving) { + saveMessageNode = document.createElement('div'); + saveMessageNode.classList.add('save-message'); + saveMessageNode.innerHTML = 'Saving...'; + saveMessageContainer.appendChild(saveMessageNode); + } else { + saveMessageNode = saveMessageContainer.querySelector('.save-message'); + if (saveMessageNode !== null) { + saveMessageNode.parentNode.removeChild(saveMessageNode); + } + } + } + + function savingPeriodError(periodNode, message) { + var saveMessageContainer = periodNode.querySelector('.add-new-update-container'); + var saveMessageNode = saveMessageContainer.querySelector('.save-message'); + if (saveMessageNode !== null) { + saveMessageNode = document.createElement('div'); + saveMessageNode.classList.add('save-message'); + saveMessageContainer.appendChild(saveMessageNode); + } + saveMessageNode.innerHTML = message; + + setInterval(function () { + saveMessageNode.parentNode.removeChild(saveMessageNode); + }, 10000); + } + + function addSaveOnClicks() { + var els = document.querySelectorAll('.save-button'); + + for (var i = 0; i < els.length; i++) { + var el = els[i]; + + el.addEventListener('click', function() { + var actualValue, description, periodId, periodNode, photo, photoNode, newValue, updateContainer, updateId, value; + var exceedTargetNode, exceedCheckbox, exceedValueNode; + var periodValue; + + updateContainer = this.parentNode; + updateId = updateContainer.getAttribute('update-id'); + periodNode = this.closest('tbody'); + + exceedTargetNode = updateContainer.querySelector('.update-exceed-target'); + exceedCheckbox = exceedTargetNode.querySelector('.update-exceed-target-checkbox'); + exceedValueNode = exceedTargetNode.querySelector('.exceed-value'); + + // When checkbox is checked, use the exceeding value + if (exceedCheckbox.checked) { + newValue = parseInt(exceedValueNode.value); + } else { + newValue = parseInt(updateContainer.querySelector('.update-dialog-timeline-marker:nth-last-child(2)').getAttribute('data-value')); + } + + if (updateId !== 'add') { + actualValue = parseInt(updateContainer.getAttribute('current-actual')); + value = newValue - actualValue + parseInt(updateContainer.getAttribute('current-change')); + } else { + actualValue = parseInt(periodNode.getAttribute('period-actual')); + value = newValue - actualValue; + } + + description = updateContainer.querySelector('.update-description').innerText; + + photoNode = updateContainer.querySelector('.photo-upload'); + if (photoNode !== null) { + photo = photoNode.files.length > 0 ? photoNode.files[0] : undefined; + } + periodId = parseInt(this.closest('tbody').getAttribute('period-id')); + updateId === 'add' ? addNewUpdate(description, periodId, value, photo) : editUpdate(updateId, periodId, description, value, updateContainer.getAttribute('current-change'), photo); + }); + } + } + + function addDeleteOnClicks() { + var els = document.querySelectorAll('.delete-button'); + + for (var i = 0; i < els.length; i++) { + var el = els[i]; + + el.addEventListener('click', function () { + var api_url, deleteConfirmContainer, deleteConfirmYes, deleteConfirmNo, periodId, periodNode, request, requestData, updateNode, updateChange, updateId; + + updateNode = this.closest('.update-entry-container'); + updateChange = parseInt(updateNode.getAttribute('current-change')); + updateId = updateNode.getAttribute('update-id'); + periodNode = updateNode.closest('tbody'); + periodId = periodNode.getAttribute('period-id'); + + // Show confirmation dialog + deleteConfirmContainer = updateNode.querySelector('.delete-confirm'); + + // Add a class to update-name so we can set a margin on it to make room + // for the delete confirmation dialog. + updateNode.querySelector('.update-name').classList.add('delete-pending'); + deleteConfirmContainer.style.display="block"; + + deleteConfirmNo = updateNode.querySelector('.delete-confirm-no'); + deleteConfirmNo.removeEventListener('click'); + deleteConfirmNo.addEventListener('click', function() { + + // Cancel the delete operation + deleteConfirmContainer.style.display='none'; + updateNode.querySelector('.update-name').classList.remove('delete-pending'); + }); + + deleteConfirmYes = updateNode.querySelector('.delete-confirm-yes'); + deleteConfirmYes.removeEventListener('click'); + deleteConfirmYes.addEventListener('click', function() { + + // Create request + api_url = '/rest/v1/project_update/' + updateId + '/?format=json'; + request = new XMLHttpRequest(); + request.open('DELETE', api_url, true); + request.setRequestHeader("X-CSRFToken", csrftoken); + request.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + + // TODO: Create recalculation function so that closing isn't necessary + savingPeriod(periodNode, true); + + request.onload = function() { + if (request.status === 204) { + // Object successfully deleted + removeUpdatefromStore(periodId, parseInt(updateId)); + updateActualValue(periodId, updateChange * -1); + + savingPeriod(periodNode, false); + + // Close the indicator period panel + periodNode.querySelector('.expand-indicator-period').click(); + deleteConfirmContainer.style.display='none'; + updateNode.querySelector('.update-name').classList.remove('delete-pending'); + updatePeriodValues(periodId, updateChange * -1); + + // Reopen the indicator period panel + periodNode.querySelector('.expand-indicator-period').click(); + return true; + } else { + // We reached our target server, but it returned an error + savingPeriodError(periodNode, 'Could not delete update.'); + return false; + } + }; + + request.onerror = function() { + // There was a connection error of some sort + savingPeriodError(periodNode, 'Connection error.'); + return false; + }; + + request.send(JSON.stringify(requestData)); + }); + }); + } + } + + function addEditOnClicks() { + var els = document.querySelectorAll('.edit-button'); + + for (var i = 0; i < els.length; i++) { + var el = els[i]; + + el.addEventListener('click', function() { + var updateContainer = this.parentNode; + var updateId = updateContainer.getAttribute('update-id'); + var periodNode = this.closest('tbody'); + var periodId = periodNode.getAttribute('period-id'); + var exceedTargetNode = updateContainer.querySelector('.update-exceed-target'); + var exceedCheckbox = exceedTargetNode.querySelector('.update-exceed-target-checkbox'); + var exceedValueNode = exceedTargetNode.querySelector('.exceed-value'); + var deleteButton = updateContainer.querySelector('.delete-button'); + + if (this.classList.contains('activated')) { + var editables = updateContainer.querySelectorAll('.editable'); + + updateContainer.classList.remove('edit-in-progress'); + + for (var j = 0; j < editables.length; j++) { + editables[j].classList.remove('editable'); + editables[j].removeAttribute('contenteditable'); + } + this.classList.remove('activated'); + updateContainer.querySelector('.save-button').classList.remove('active'); + updateContainer.querySelector('.cancel-button').classList.remove('active'); + updateContainer.querySelector('.edit-slider').noUiSlider.destroy(); + + if (deleteButton !== null) { + deleteButton.classList.remove('activated'); + } + + exceedCheckbox.setAttribute('disabled', ''); + exceedValueNode.setAttribute('disabled', ''); + + var photoUpload = updateContainer.querySelector('.photo-upload'); + photoUpload.parentNode.removeChild(photoUpload); + + exceedTargetNode.style.display = 'none'; + } else { + this.classList.add('activated'); + if (deleteButton !== null) { + deleteButton.classList.add('activated'); + } + + updateContainer.classList.add('edit-in-progress'); + + updateContainer.querySelector('.update-description').setAttribute('contentEditable', 'true'); + updateContainer.querySelector('.update-description').classList.add('editable'); + + updateContainer.querySelector('.save-button').classList.add('active'); + updateContainer.querySelector('.cancel-button').classList.add('active'); + + updateContainer.querySelector('.cancel-button').addEventListener('click', function() { + if (updateId === 'add') { + updateContainer.parentNode.removeChild(updateContainer); + displayAddButton(periodId, true); + } else { + periodNode.querySelector('.expand-indicator-period').click(); + periodNode.querySelector('.expand-indicator-period').click(); + } + }); + + var uploadPhoto = document.createElement('input'); + uploadPhoto.setAttribute('type', 'file'); + uploadPhoto.setAttribute('accept', 'image/*'); + uploadPhoto.classList.add('photo-upload'); + updateContainer.appendChild(uploadPhoto); + + var sliderEl = updateContainer.querySelector('.edit-slider'); + var startVal = updateContainer.getAttribute('current-actual'); + var minVal = parseInt(this.closest('.indicator-period').getAttribute('indicator-baseline')); + var maxVal = parseInt(sliderEl.getAttribute('data-max')); + + noUiSlider.create(sliderEl, { + start: startVal, + step: 1, + range: { + 'min': minVal, + 'max': maxVal + } + }); + + var updateMarker = updateContainer.querySelector('.update-dialog-timeline-marker:nth-last-child(2)'); + var updateProgress = updateContainer.querySelector('.indicator-bar-progress-amount'); + var updateActual = updateContainer.querySelector('.update-target-actual'); + var originalPercentageProgress = parseInt(updateMarker.style.left.substring(0, updateMarker.style.left.length - 1)); + var originalValue = updateActual.textContent; + var originalPositionMarkerEl = document.createElement('div'); + var changeIndicatorEl = document.createElement('div'); + var handleLabelEl = document.createElement('div'); + var handleChangeLabelEl = document.createElement('div'); + + originalPositionMarkerEl.classList.add('original-position-marker'); + originalPositionMarkerEl.style.left = originalPercentageProgress + '%'; + + changeIndicatorEl.classList.add('change-indicator'); + changeIndicatorEl.style.left = originalPercentageProgress + '%'; + + handleLabelEl.classList.add('handle-label'); + + // Placeholder to ensure label is correct size - under rare + // conditions label will have no text content until slider handle + // is moved. + handleLabelEl.textContent = '--'; + + handleChangeLabelEl.classList.add('handle-change-label'); + + + document.querySelector('.edit-slider').appendChild(originalPositionMarkerEl); + document.querySelector('.edit-slider').appendChild(changeIndicatorEl); + document.querySelector('.edit-slider .noUi-handle').appendChild(handleLabelEl); + document.querySelector('.edit-slider .noUi-handle').appendChild(handleChangeLabelEl); + + exceedCheckbox.removeAttribute('disabled'); + if (exceedCheckbox.checked) { + exceedValueNode.removeAttribute('disabled'); + displayEditSlider(updateContainer, false); + } + + sliderEl.noUiSlider.on('update', function(value) { + if (exceedCheckbox.checked) { + updateActual.textContent = exceedValueNode.value; + } else { + var percentage; + var changeValueIsNegative; + + value = parseInt(value); + + percentage = (value - minVal) / (maxVal - minVal) * 100; + percentage = percentage > 100 ? 100 : percentage; + + percentage < originalPercentageProgress ? changeValueIsNegative = true : changeValueIsNegative = false; + + updateMarker.style.left = percentage + '%'; + updateMarker.setAttribute('data-value', value); + updateProgress.style.width = percentage + '%'; + + handleLabelEl.textContent = value; + handleChangeLabelEl.textContent = changeValueIsNegative ? '-' : '+'; + handleChangeLabelEl.textContent += Math.abs(value - originalValue); + + if (changeValueIsNegative) { + // Change is negative + changeIndicatorEl.style.right = (100 - originalPercentageProgress) + '%'; + changeIndicatorEl.style.left = percentage + '%'; + changeIndicatorEl.classList.add('negative'); + } else { + // Change is positive + changeIndicatorEl.style.left = originalPercentageProgress + '%'; + changeIndicatorEl.style.right = (100 - percentage) + '%'; + changeIndicatorEl.classList.remove('negative'); + } + + updateActual.textContent = value; + } + }); + + fadeIn(exceedTargetNode, true); + + addSaveOnClicks(); + } + }); + } + } + + /* GENERAL HELPER FUNCTIONS */ + + /* Fade in + ** ======= + ** Takes a selector and fades in each matching element. + ** Takes an optional second argument, isElement, that indicates + ** the first argument in an element rather than a selector. + ** Takes an optional third argument, displayVal, that indicates + ** what display value to give the element after fading in. + ** If this argument is not present, defaults to 'block' + */ + function fadeIn(selector, isElement, displayVal) { + + if (isElement) { + fadeElIn(selector); + } else { + var els = document.querySelectorAll(selector); + + for (var i = 0; i < els.length; i++) { + var el = els[i]; + fadeElIn(el); + } + } + + function fadeElIn(el) { + var opacityCallback = getOpacityCallback(el); + var classCallback = getClassCallback(el); + + el.classList.add('opacity-transition'); + el.style.opacity = 0; + el.style.display = displayVal || 'block'; + el.classList.add('fading-in'); + + setTimeout(opacityCallback, 1); + setTimeout(classCallback, 250); + } + + function getOpacityCallback(el) { + var cb = function() { + el.style.opacity = 1; + }; + + return cb; + } + + function getClassCallback(el) { + var cb = function() { + el.classList.remove('fading-in'); + }; + + return cb; + } + } + + /* Fade out */ + + function fadeOut(selector) { + var els = document.querySelectorAll(selector); + + for (var i = 0; i < els.length; i++) { + var el = els[i]; + var opacityCallback = getOpacityCallback(el); + var displayCallback = getDisplayCallback(el); + + el.style.opacity = 1; + setTimeout(opacityCallback, 1); + setTimeout(displayCallback, 250); + } + + function getOpacityCallback(el) { + var cb = function() { + el.style.opacity = 0; + }; + + return cb; + } + + function getDisplayCallback(el) { + var cb = function() { + if (!el.classList.contains('fading-in')) { + el.style.display = 'none'; + } + }; + + return cb; + } + } + + /* CSRF TOKEN (this should really be added in base.html, we use it everywhere) */ + function getCookie(name) { + var cookieValue = null; + if (document.cookie && document.cookie !== '') { + var cookies = document.cookie.split(';'); + for (var i = 0; i < cookies.length; i++) { + var cookie = cookies[i].trim(); + if (cookie.substring(0, name.length + 1) == (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } + var csrftoken = getCookie('csrftoken'); + + /* Time remaining to edit (needs to be in GMT) */ + function getTimeDifference(endDate) { + var remainingDate = endDate - new Date(currentDate); + + return { + 'days': Math.floor( remainingDate/(1000*60*60*24) ), + 'hours': Math.floor( (remainingDate/(1000*60*60)) % 24 ), + 'minutes': Math.floor( (remainingDate/1000/60) % 60 ) + }; + } + + function setMinutesRemaining(node, createdAtDate) { + var endDate = createdAtDate; + endDate.setMinutes(endDate.getMinutes() + parseInt(defaultValues.update_timeout)); + + var timeinterval = setInterval(function(){ + var remainingTime = getTimeDifference(endDate); + node.setAttribute('title', remainingTime.minutes + ' minutes remaining'); + + if (remainingTime.minutes < 0) { + clearInterval(timeinterval); + node.parentNode.removeChild(node); + } + },100); + } + + /* Upload photo */ + function uploadPhoto(photo, updateId, periodNode) { + var apiUrl, fileRequest, formData; + + apiUrl = '/rest/v1/project_update/' + updateId + '/upload_photo/?format=json'; + + formData = new FormData(); + formData.append("photo", photo); + + fileRequest = new XMLHttpRequest(); + fileRequest.open("POST", apiUrl); + fileRequest.setRequestHeader("X-CSRFToken", csrftoken); + + fileRequest.onload = function() { + if (fileRequest.status >= 200 && fileRequest.status < 400) { + addAdditionalUpdateData(updateId); + savingPeriod(periodNode, false); + return false; + } else { + // We reached our target server, but it returned an error + savingPeriodError(periodNode, 'Uploading photo failed.'); + return false; + } + }; + + fileRequest.onerror = function() { + // There was a connection error of some sort + savingPeriodError(periodNode, 'Uploading photo failed: connection error.'); + return false; + }; + + fileRequest.send(formData); + } + + /* Add new update */ + function addNewUpdate(text, periodId, value, photo) { + var api_url, periodNode, request, requestData; + + removeUpdatefromStore(periodId, 'add'); + periodNode = findPeriod(periodId); + + // TODO: Create recalculation function per period so that closing it isn't necessary + savingPeriod(periodNode, true); + + // Create request + api_url = '/rest/v1/project_update/?format=json'; + request = new XMLHttpRequest(); + request.open('POST', api_url, true); + request.setRequestHeader("X-CSRFToken", csrftoken); + request.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + + request.onload = function() { + if (request.status === 201) { + // Object successfully created + + // This callback expands the indicator period panel when the new data is loaded + var callback = function(){ + periodNode.querySelector('.expand-indicator-period').click(); + }; + + // TODO: only close the panel and remove the "saving" message once the "addAdditionalUpdateData" + // call has finished + + // Close the indicator period panel + periodNode.querySelector('.expand-indicator-period').click(); + + var response, updateId, updateNode; + response = JSON.parse(request.response); + updateId = response.id; + + updateActualValue(periodId, value); + addAdditionalUpdateData(updateId, callback); + updatePeriodValues(periodId, value); + // updateUpdateValues(periodId, 'add', updateId, value); + + // Upload photo + if (photo !== undefined) { + uploadPhoto(photo, updateId, periodNode); + } else { + savingPeriod(periodNode, false); + } + + return true; + } else { + // We reached our target server, but it returned an error + savingPeriodError(periodNode, 'Adding update failed.'); + return false; + } + }; + + request.onerror = function() { + // There was a connection error of some sort + savingPeriodError(periodNode, 'Connection error.'); + return false; + }; + + requestData = { + project: defaultValues.project_id, + user: defaultValues.user_id, + title: 'Indicator update', + text: text, + indicator_period: periodId, + period_update: value + }; + + request.send(JSON.stringify(requestData)); + } + + /* Edit existing update */ + function editUpdate(updateId, periodId, text, value, oldValue, photo) { + var api_url, periodNode, request; + + periodNode = findPeriod(periodId); + savingPeriod(periodNode, true); + + // Create request + api_url = '/rest/v1/project_update/' + updateId + '/?format=json'; + request = new XMLHttpRequest(); + request.open('PATCH', api_url, true); + request.setRequestHeader("X-CSRFToken", csrftoken); + request.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + + request.onload = function() { + if (request.status >= 200 && request.status < 400) { + // Object successfully saved + + // This callback expands the indicator period panel when the new data is loaded + var callback = function(){ + periodNode.querySelector('.expand-indicator-period').click(); + }; + + // TODO: only close the panel and remove the "saving" message once the "addAdditionalUpdateData" + // call has finished + + // Close the indicator period panel + periodNode.querySelector('.expand-indicator-period').click(); + addAdditionalUpdateData(updateId, callback); + updateActualValue(periodId, value - oldValue); + // updateUpdateValues(periodId, updateId, updateId, value); + updatePeriodValues(periodId, (value - oldValue)); + + // Upload photo + if (photo !== undefined) { + uploadPhoto(photo, updateId, periodNode); + } else { + savingPeriod(periodNode, false); + } + + return true; + } else { + // We reached our target server, but it returned an error + savingPeriodError(periodNode, 'Editing update failed.'); + return false; + } + }; + + request.onerror = function() { + // There was a connection error of some sort + savingPeriodError(periodNode, 'Connection error.'); + return false; + }; + + request.send(JSON.stringify({text: text, period_update: value})); + } + + /* Display the photo in the update. Either by replacing the existing photo or by adding a new photo */ + function displayPhoto(update, updateNode) { + if (update.photo) { + var photoNode = document.createElement('div'); + photoNode.classList.add('update-photo'); + photoNode.style['background-image'] = 'url("' + update.photo + '")'; + photoNode.style['background-size'] = 'cover'; + photoNode.style.height = '120px'; + photoNode.style.width = '200px'; + updateNode.appendChild(photoNode); + } + } + + /* Display the add button in the top row */ + function displayAddButton(periodId, display) { + var addUpdateButton, canUpdate, isAdmin, periodNode; + + canUpdate = defaultValues.can_update; + isAdmin = defaultValues.user_is_admin; + periodNode = findPeriod(periodId); + addUpdateButton = periodNode.querySelector('.add-button'); + + addUpdateButton.style.display = display && (isAdmin || canUpdate) ? '' : 'none'; + } + + /* Display the edit slider in the update */ + function displayEditSlider(updateNode, display) { + var editSlider = updateNode.querySelector('.edit-slider'); + editSlider.style.display = display ? '' : 'none'; + } + + /* Get additional data of update */ + function addAdditionalUpdateData(updateId, callback) { + var api_url, request; + + // Create request + api_url = '/rest/v1/project_update_extra/' + updateId + '/?format=json'; + request = new XMLHttpRequest(); + request.open('GET', api_url, true); + request.setRequestHeader("X-CSRFToken", csrftoken); + request.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + + request.onload = function() { + if (request.status >= 200 && request.status < 400) { + // Object successfully retrieved + var response = JSON.parse(request.response); + addUpdateToStore(response); + callback(); + } else { + return false; + } + }; + + request.onerror = function() { + // There was a connection error of some sort + return false; + }; + + request.send(); + } + + /* Update the values of a period */ + function updateActualValue(periodId, value) { + var baseline, newPercentage, newValue, oldValue, periodNode, target; + + periodNode = findPeriod(periodId); + target = parseInt(periodNode.getAttribute('period-target')); + oldValue = parseInt(periodNode.getAttribute('period-actual')); + newValue = oldValue + parseInt(value); + baseline = parseInt(periodNode.getAttribute('indicator-baseline')); + periodNode.setAttribute('period-actual', newValue); + + newPercentage = (newValue - baseline) / (target - baseline) * 100; + newPercentage = newPercentage > 100 ? 100 : newPercentage; + periodNode.querySelector('.indicator-bar-progress-amount').setAttribute('style', 'width: ' + newPercentage + '%;'); + periodNode.querySelector('.indicator-bar-progress').setAttribute('style', 'left: ' + newPercentage + '%; z-index: ' + newPercentage + ';'); + periodNode.querySelector('.indicator-bar-progress').setAttribute('data-progress', newPercentage); + periodNode.querySelector('.indicator-bar-progress-text').textContent = newValue.toString();// > target ? target.toString() : newValue.toString(); + } + + /* Update the values of an update */ + function updateUpdateValues(periodId, oldUpdateId, newUpdateId, change) { + var updateNode = findUpdate(periodId, oldUpdateId); + updateNode.setAttribute('update-id', newUpdateId); + updateNode.setAttribute('current-change', change); + // Only works for the latest update + updateNode.setAttribute('current-actual', findPeriod(periodId).getAttribute('period-actual')); + } + + /* Update the values of other periods when an earlier period is edited */ + function updatePeriodValues(periodId, change) { + var baseDate = new Date(document.querySelector('.indicator-period[period-id="' + periodId + '"] .toTime').textContent); + var resultEl = document.querySelector('.indicator-period[period-id="' + periodId + '"]').closest('.indicator-group'); + var allPeriods = resultEl.querySelectorAll('.indicator-period'); + + for (var i = 0; i < allPeriods.length; i++) { + var period = allPeriods[i]; + var periodDate = new Date(period.querySelector('.fromTime').textContent); + + if (periodDate.getTime() >= baseDate.getTime()) { + // This indicator period is more recent that in the original + // We need to update it's value with the new changes from the original + + var oldStart = parseInt(period.getAttribute('period-start')); + var oldActual = parseInt(period.getAttribute('period-actual')); + var oldProgress = parseInt(period.querySelector('.indicator-bar-progress-text')); + + + var baseline = parseInt(period.getAttribute('indicator-baseline')); + var target = parseInt(period.getAttribute('period-target')); + var newValue = oldActual + change; + var completionPercentage = ((newValue - baseline) / (target - baseline)) * 100; + + if (completionPercentage > 100) completionPercentage = 100; + + period.setAttribute('period-start', oldStart + change); + period.setAttribute('period-actual', newValue); + period.querySelector('.indicator-bar-progress-text').textContent = newValue; + period.querySelector('.indicator-bar-progress').style.left = completionPercentage + '%'; + period.querySelector('.indicator-bar-progress-amount').style.width = completionPercentage + '%'; + } + } + } + + /* Find the period node based on its' ID */ + function findPeriod(periodId) { + var indicatorsPeriods = document.querySelectorAll('tbody.indicator-period'); + for (var i=0; i < indicatorsPeriods.length; i++) { + if (indicatorsPeriods[i].getAttribute('period-id') == periodId) { + return indicatorsPeriods[i]; + } + } + } + + /* Find the update node based on the period ID and update ID */ + function findUpdate(periodId, updateId) { + var periodNode = findPeriod(periodId); + var updateNodes = periodNode.querySelectorAll('.update-entry-container'); + for (var i=0; i < updateNodes.length; i++) { + if (updateNodes[i].getAttribute('update-id') == updateId) { + return updateNodes[i]; + } + } + } + + /* User allowed to click edit button */ + function allowEdit(update) { + if (defaultValues.user_is_admin) { + return true; + } else { + var endDate = new Date(update.created_at); + endDate.setMinutes(endDate.getMinutes() + parseInt(defaultValues.update_timeout)); + var remaining = getTimeDifference(endDate); + var enoughTime = !!(remaining.days >= 0 && remaining.hours >= 0 && remaining.minutes >= 0); + var userPk = defaultValues.user_id; + return !!(enoughTime && userPk == update.user.id); + } + } + + /* Hide all elements for a given selector */ + function hideAll(selector) { + var els = document.querySelectorAll(selector); + + for (var i = 0; i < els.length; i++) { + var el = els[i]; + + el.style.display = 'none'; + } + } + + /* Remove all elements for a given selector from the DOM */ + function removeFromPeriod(periodId, selector) { + var periodNode, els; + + periodNode = findPeriod(periodId); + els = periodNode.querySelectorAll(selector); + + for (var i = 0; i < els.length; i++) { + var el = els[i]; + + el.parentNode.removeChild(el); + } + } + + /* Remove a class from all elements for a given selector */ + function removeClassFromAll(selector, className, periodId) { + var els, periodNode; + + if (periodId === undefined) { + els = document.querySelectorAll(selector); + } else { + periodNode = findPeriod(periodId); + els = periodNode.querySelectorAll(selector); + } + + for (var i = 0; i < els.length; i++) { + els[i].classList.remove(className); + } + } + function addClassActive(elem) { + // get all 'a' elements + var a = document.getElementsByTagName('a'); + // loop through all 'a' elements + for (i = 0; i < a.length; i++) { + // Remove the class 'active' if it exists + a[i].classList.remove('active'); + } + // add 'active' classs to the element that was clicked + elem.classList.add('active'); + } + var updatesByIndicatorPeriod = {}; + + /* Get the grab the update data from the script we use to print + ** the template tags, then format that data into an object sorted + ** by indicator period, to make life easier for us later. + */ + + function buildUpdateJSON() { + var updateJSON = document.querySelector('#update-json').innerHTML; + var updateData = JSON.parse(updateJSON); + + for (var i=0; i= 0; i--) { + containerCell.appendChild(childElements[i]); + } + } + container.appendChild(containerCell); + return container; + } + + /* Returns a node representing a single "update" entry for the "Add update" dialog. + ** E.G if an indicator period has 7 updates, this function should be called once + ** for each and return the appropriate markup each time. + */ + + function getUpdateEntry(update) { + var updateContainer = document.createElement('div'); + var deleteEl, deleteConfirmEl, deleteConfirmContentsEl, deleteConfirmYesEl, deleteConfirmNoEl, editEl, dateEl, userNameEl, timeLineEl, targetEl, targetText, photoEl, descriptionEl; + + updateContainer.setAttribute("update-id", update.id); + updateContainer.setAttribute("current-change", update.period_update); + updateContainer.classList.add('update-entry-container'); + updateContainer.classList.add('bg-border-transition'); + + if (allowEdit(update)) { + editEl = document.createElement('i'); + editEl.classList.add('fa'); + editEl.classList.add('fa-pencil-square-o'); + editEl.classList.add('edit-button'); + editEl.classList.add('clickable'); + updateContainer.appendChild(editEl); + + if (!defaultValues.user_is_admin) { + setMinutesRemaining(editEl, new Date(update.created_at)); + } else { + deleteEl = document.createElement('i'); + deleteEl.classList.add('fa'); + deleteEl.classList.add('fa-trash-o'); + deleteEl.classList.add('delete-button'); + deleteEl.classList.add('clickable'); + updateContainer.appendChild(deleteEl); + + deleteConfirmEl = document.createElement('div'); + // TODO: use "delete_confirm_text" from translation strings in main template + deleteConfirmEl.textContent = "Are you sure you want to delete this update?"; + deleteConfirmEl.style.display = 'none'; + deleteConfirmEl.classList.add('delete-confirm'); + + deleteConfirmYesEl = document.createElement('a'); + deleteConfirmYesEl.classList.add('btn'); + deleteConfirmYesEl.classList.add('btn-primary'); + deleteConfirmYesEl.classList.add('delete-confirm-yes'); + // TODO: use "delete_confirm_yes_text" from translation strings in main template + deleteConfirmYesEl.textContent = "Delete update"; + + deleteConfirmNoEl = document.createElement('a'); + deleteConfirmNoEl.classList.add('btn'); + deleteConfirmNoEl.classList.add('btn-primary'); + deleteConfirmNoEl.classList.add('delete-confirm-no'); + // TODO: use "delete_confirm_cancel_text" from translation strings in main template + deleteConfirmNoEl.textContent = "Cancel"; + + deleteConfirmContentsEl = document.createElement('div'); + + deleteConfirmContentsEl.appendChild(deleteConfirmNoEl); + deleteConfirmContentsEl.appendChild(deleteConfirmYesEl); + deleteConfirmEl.appendChild(deleteConfirmContentsEl); + updateContainer.appendChild(deleteConfirmEl); + } + } + + if (update.created_at) { + var dateObj = new Date(update.created_at); + dateEl = document.createElement('div'); + dateEl.classList.add('update-date'); + dateEl.textContent = dateObj.toLocaleDateString() + ' ' + dateObj.toLocaleTimeString(); + updateContainer.appendChild(dateEl); + } + if (update.user) { + userNameEl = document.createElement('div'); + userNameEl.classList.add('update-name'); + userNameEl.textContent = update.user.first_name + ' ' + update.user.last_name; + updateContainer.appendChild(userNameEl); + } + + timeLineEl = document.createElement('div'); + timeLineEl.classList.add('update-timeline'); + + var progressContainer = document.createElement('div'); + progressContainer.classList.add('indicator-bar-display-container'); + + var markerContainer = document.createElement('div'); + markerContainer.classList.add('indicator-bar-progress-container'); + + var previousUpdates = []; + /* Now we need to loop through every update with an ID lower than the id of *this* update, + ** and store its change amount so we can build the timeline. + */ + for (var updateID in updatesByIndicatorPeriod[update.indicator_period.id]) { + if (updatesByIndicatorPeriod[update.indicator_period.id].hasOwnProperty(updateID)) { + var previousUpdate = updatesByIndicatorPeriod[update.indicator_period.id][updateID]; + + /* If the change amount is "None", changes this to zero */ + if (previousUpdate.period_update === 'None') { + previousUpdate.period_update = "0"; + } + + previousUpdates.push(previousUpdate); + } + } + + previousUpdates.sort(function(a,b) { + return a.id > b.id; + }); + + var actualText, periodNode, periodTarget, periodBaseline, progress; + + periodNode = findPeriod(update.indicator_period.id); + periodTarget = parseInt(periodNode.getAttribute('period-target')); + periodBaseline = parseInt(periodNode.getAttribute('period-start')); + indicatorBaseline = parseInt(periodNode.getAttribute('indicator-baseline')); + + progress = periodBaseline; + + for (var i=0; i < previousUpdates.length; i++) { + var entry = previousUpdates[i]; + + if (entry.id <= update.id || update.id === 'add') { + /* This update is older (or the same) as the update we're building the dialog for. + We need to make a marker for it. */ + progress += parseInt(entry.period_update, 10); + + var updateMarker = document.createElement('div'); + var textSpan = document.createElement('div'); + + var percentage = (progress - indicatorBaseline) / (periodTarget - indicatorBaseline) * 100; + percentage = percentage > 100 ? 100 : percentage; + + updateMarker.classList.add('update-dialog-timeline-marker'); + updateMarker.classList.add('indicator-bar-progress'); + updateMarker.style.left = percentage + '%'; + updateMarker.style['z-index'] = progress; + + textSpan.classList.add('indicator-bar-progress-text'); + textSpan.classList.add('bg-transition'); + textSpan.textContent = progress; + textSpan.style.left = percentage + '%'; + + var textHoverEl = document.createElement('span'); + var createdDate = new Date(entry.created_at); + + textHoverEl.classList.add('progress-hover-text'); + textHoverEl.classList.add('opacity-transition'); + textHoverEl.textContent = createdDate.toLocaleDateString() + ' ' + createdDate.toLocaleTimeString(); + textSpan.appendChild(textHoverEl); + + markerContainer.appendChild(updateMarker); + markerContainer.appendChild(textSpan); + } + } + + if (progress > periodTarget) { + updateContainer.classList.add('target-exceeded'); + } + + var baselineEl = document.createElement('div'); + baselineEl.classList.add('indicator-baseline'); + baselineEl.textContent = periodNode.querySelector('.indicator-bar-td > div > .indicator-baseline').textContent; + progressContainer.appendChild(baselineEl); + + var indicatorBar = document.createElement('div'); + indicatorBar.classList.add('indicator-bar'); + progressContainer.appendChild(indicatorBar); + + var highPercentage = (progress - indicatorBaseline) / (periodTarget - indicatorBaseline) * 100; + highPercentage = highPercentage > 100 ? 100 : highPercentage; + + var indicatorBarProgressAmount = document.createElement('div'); + indicatorBarProgressAmount.classList.add('indicator-bar-progress-amount'); + indicatorBarProgressAmount.style.width = highPercentage + '%'; + + progressContainer.appendChild(indicatorBarProgressAmount); + progressContainer.appendChild(markerContainer); + + updateContainer.setAttribute("current-actual", progress); + var sliderEl = document.createElement('div'); + sliderEl.classList.add('edit-slider'); + sliderEl.setAttribute('data-start', progress); + sliderEl.setAttribute('data-max', periodTarget); + + progressContainer.appendChild(sliderEl); + timeLineEl.appendChild(progressContainer); + updateContainer.appendChild(timeLineEl); + + targetEl = document.createElement('div'); + targetEl.classList.add('update-target'); + + actualText = document.createElement('span'); + actualText.classList.add('update-target-actual'); + actualText.textContent = progress; + targetEl.appendChild(actualText); + + targetText = document.createElement('span'); + actualText.classList.add('update-target-actual'); + targetText.textContent = ' / ' + periodTarget; + targetEl.appendChild(targetText); + updateContainer.appendChild(targetEl); + + var exceedTargetEl = document.createElement('div'); + exceedTargetEl.classList.add('update-exceed-target'); + + var exceedTargetLabel = document.createElement('label'); + exceedTargetLabel.textContent = 'Exceed target'; + exceedTargetLabel.setAttribute('for', 'exceed-' + update.id); + + var exceedTargetCheckbox = document.createElement('input'); + exceedTargetCheckbox.setAttribute('type', 'checkbox'); + exceedTargetCheckbox.setAttribute('id', 'exceed-' + update.id); + exceedTargetCheckbox.setAttribute('disabled', ''); + exceedTargetCheckbox.classList.add('update-exceed-target-checkbox'); + + var exceedTargetNewValue = document.createElement('input'); + exceedTargetNewValue.setAttribute('type', 'number'); + exceedTargetNewValue.setAttribute('disabled', ''); + exceedTargetNewValue.classList.add('exceed-value'); + exceedTargetNewValue.classList.add('opacity-transition'); + + exceedTargetNewValue.addEventListener('input', function () { + if (exceedTargetCheckbox.checked) { + updateContainer.querySelector('.update-target-actual').textContent = exceedTargetNewValue.value; + } + }); + + exceedTargetCheckbox.addEventListener('change', function () { + if (exceedTargetCheckbox.checked) { + exceedTargetNewValue.removeAttribute('disabled'); + exceedTargetNewValue.value = parseInt(findPeriod(update.indicator_period.id).getAttribute('period-target')); + displayEditSlider(updateContainer, false); + } else { + exceedTargetNewValue.value = ''; + exceedTargetNewValue.setAttribute('disabled', ''); + displayEditSlider(updateContainer, true); + } + }); + + exceedTargetEl.appendChild(exceedTargetCheckbox); + exceedTargetEl.appendChild(exceedTargetLabel); + exceedTargetEl.appendChild(exceedTargetNewValue); + exceedTargetEl.style.display = 'none'; + updateContainer.appendChild(exceedTargetEl); + + if (progress > periodTarget) { + exceedTargetNewValue.value = progress; + exceedTargetCheckbox.checked = true; + } + + descriptionEl = document.createElement('div'); + descriptionEl.classList.add('update-description'); + descriptionEl.innerHTML = update.text.replace(/\n/g,"
"); + + updateContainer.appendChild(descriptionEl); + + displayPhoto(update, updateContainer); + + var saveEl = document.createElement('div'); + saveEl.classList.add('save-button'); + saveEl.classList.add('clickable'); + saveEl.textContent = 'Save'; + + updateContainer.appendChild(saveEl); + + var cancelEl = document.createElement('div'); + cancelEl.classList.add('cancel-button'); + cancelEl.classList.add('clickable'); + cancelEl.textContent = 'Cancel'; + updateContainer.appendChild(cancelEl); + + return updateContainer; + } + + function readMoreOnClicks() { + function setReadMore(show, hide) { + return function(e) { + e.preventDefault(); + hide.classList.add('hidden'); + show.classList.remove('hidden'); + }; + } + + 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); + } + + function setCurrentDate() { + var interval = setInterval(function(){ + var localCurrentDate = new Date(currentDate); + localCurrentDate.setSeconds(localCurrentDate.getSeconds() + 1); + currentDate = localCurrentDate.toString(); + }, 1000); + } + + 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 + '"]'); + + for (var i = 0; i < allTabs.length; i++) { + var tab = allTabs[i]; + + tab.style.display = 'none'; + } + for (var j = 0; j < allTabLinks.length; j++) { + var tabLink = allTabLinks[j]; + + tabLink.classList.remove('selected'); + } + + activeTab.style.display = 'block'; + activeTabLink.classList.add('selected'); + } + + function setTabOnClicks() { + var allTabs = document.querySelectorAll('.tab-link'); + + for (var i = 0; i < allTabs.length; i++) { + var tab = allTabs[i]; + + tab.addEventListener('click', function() { + var tabClass = this.getAttribute('href'); + + // Remove the '#' from the href + tabClass = tabClass.substring(1); + showTab(tabClass); + }); + } + } + + 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'); + } + } + + /* POLYFILLS */ + + // 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() { + setCurrentDate(); + + // Setup results framework + setResultExpandOnClicks(); + setIndicatorLinkOnClicks(); + setExpandIndicatorPeriodOnClicks(); + addAddOnClicks(); + buildUpdateJSON(); + }); \ 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 new file mode 100644 index 0000000000..9bbc5eee9d --- /dev/null +++ b/akvo/rsr/static/scripts-src/results-data.jsx @@ -0,0 +1,1494 @@ +/** @jsx React.DOM */ + +// Akvo RSR is covered by the GNU Affero General Public License. +// 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 >. + +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'); + } + }); + } + } + + /* Show the results summary in the main panel when the + ** appropriate side bar link is activated. + */ + function showResultsSummary(id) { + var selector = '.result-' + id + '.result-summary'; + + hideAllResultsSummaries(); + fadeIn(selector); + } + + /* Hide all results summaries. */ + function hideAllResultsSummaries() { + hideAll('.result-summary'); + } + + /* Set the listeners to show the appropriate indicator in + ** the main panel when the sidebar link is clicked. + */ + function setIndicatorLinkOnClicks() { + var els = document.querySelectorAll('.indicator-nav, .indicator-link'); + + for (var i = 0; i < els.length; i++) { + el = els[i]; + + el.addEventListener('click', function() { + + /* Hide all the indicator and result summary elements */ + hideAll('.indicator-group, .indicator'); + hideAllResultsSummaries(); + + /* Now show the indicator group and indicator that + ** matches the element that's been clicked. + */ + var indicatorID = this.getAttribute('data-indicator-id'); + var indicatorSelector = '.indicator-' + indicatorID; + var indicatorGroupSelector = '.indicator-group.result-' + this.getAttribute('data-result-id'); + + // document.querySelector(indicatorGroupSelector).style.display = 'block'; + // document.querySelector(indicatorSelector).style.display = 'block'; + + fadeIn(indicatorGroupSelector); + fadeIn(indicatorSelector); + + /* Add an "active" class to this indicator in the sidebar for styling purposes */ + removeClassFromAll('.indicator-nav.active', 'active'); + document.querySelector('.indicator-nav[data-indicator-id="' + indicatorID + '"]').classList.add('active'); + + }); + } + } + /* Set the onClicks to expand a given indicator period and show the "Add update" dialog */ + function setExpandIndicatorPeriodOnClicks() { + var els = document.querySelectorAll('.expand-indicator-period'); + var el = null; + + for (var i = 0; i < els.length; i++) { + el = els[i]; + + el.addEventListener('click', function() { + var parentElement, periodId, periodNode; + + periodNode = this.closest('tbody'); + periodId = periodNode.getAttribute('period-id'); + parentElement = document.querySelector('.indicator-period-' + periodId + '-tr'); + + if (this.classList.contains('expanded')) { + /* This is period already expanded. Collapse it and remove all update dialogs. */ + removeFromPeriod(periodId, 'tr.update-dialog-container'); + displayAddButton(periodId, false); + removeClassFromAll('tr.expanded, .expand-indicator-period.expanded', 'expanded', periodId); + document.querySelector('.indicator-period-' + periodId + '-tr').parentNode.classList.remove('expanded'); + /* Remove the 'add' update in the store, in case the 'add Update' button was clicked */ + removeUpdatefromStore(periodId, 'add'); + } else { + var container; + var updateDialog = getUpdateDialog(periodId); + + displayAddButton(periodId, true); + updateDialog.style.display = 'none'; + parentElement.parentNode.appendChild(updateDialog); + fadeIn(updateDialog, true, 'table-row'); + + this.parentNode.parentNode.classList.add('expanded'); + this.parentNode.parentNode.parentNode.classList.add('expanded'); + this.classList.add('expanded'); + + addEditOnClicks(); + addDeleteOnClicks(); + } + }); + } + } + + function addAddOnClicks() { + var els = document.querySelectorAll('.add-button'); + + for (var i = 0; i < els.length; i++) { + var el = els[i]; + el.addEventListener('click', function() { + var addUpdateContainer, container, containerCell, newUpdate, periodId, periodNode, periodTarget; + + periodNode = this.closest('tbody'); + periodId = periodNode.getAttribute('period-id'); + periodTarget = periodNode.getAttribute('period-target'); + + newUpdate = { + id: 'add', + indicator_period: { + id: periodId, + target_value: periodTarget + }, + period_update: "0", + created_at: currentDate, + user: { + id: defaultValues.user_id, + first_name: defaultValues.user_first_name, + last_name: defaultValues.user_last_name + }, + text: '', + photo: '' + }; + addUpdateToStore(newUpdate); + + container = document.createElement('tr'); + container.classList.add('update-dialog-container'); + container.classList.add('pending-new-update'); + + containerCell = document.createElement('td'); + containerCell.setAttribute('colspan', '6'); + + addUpdateContainer = getUpdateEntry(newUpdate); + containerCell.appendChild(addUpdateContainer); + container.appendChild(containerCell); + container.style.opacity = 0; + + if (this.parentNode.parentNode.nextSibling) { + this.parentNode.parentNode.parentNode.insertBefore(container, this.parentNode.parentNode.nextSibling); + } else { + this.parentNode.parentNode.parentNode.appendChild(container); + } + fadeIn(container, true, 'table-row'); + + addEditOnClicks(); + addDeleteOnClicks(); + container.querySelector('.edit-button').click(); + displayAddButton(periodId, false); + }); + } + } + + function savingPeriod(periodNode, saving) { + var saveMessageContainer = periodNode.querySelector('.add-new-update-container'); + var saveMessageNode; + if (saving) { + saveMessageNode = document.createElement('div'); + saveMessageNode.classList.add('save-message'); + saveMessageNode.innerHTML = 'Saving...'; + saveMessageContainer.appendChild(saveMessageNode); + } else { + saveMessageNode = saveMessageContainer.querySelector('.save-message'); + if (saveMessageNode !== null) { + saveMessageNode.parentNode.removeChild(saveMessageNode); + } + } + } + + function savingPeriodError(periodNode, message) { + var saveMessageContainer = periodNode.querySelector('.add-new-update-container'); + var saveMessageNode = saveMessageContainer.querySelector('.save-message'); + if (saveMessageNode !== null) { + saveMessageNode = document.createElement('div'); + saveMessageNode.classList.add('save-message'); + saveMessageContainer.appendChild(saveMessageNode); + } + saveMessageNode.innerHTML = message; + + setInterval(function () { + saveMessageNode.parentNode.removeChild(saveMessageNode); + }, 10000); + } + + function addSaveOnClicks() { + var els = document.querySelectorAll('.save-button'); + + for (var i = 0; i < els.length; i++) { + var el = els[i]; + + el.addEventListener('click', function() { + var actualValue, description, periodId, periodNode, photo, photoNode, newValue, updateContainer, updateId, value; + var exceedTargetNode, exceedCheckbox, exceedValueNode; + var periodValue; + + updateContainer = this.parentNode; + updateId = updateContainer.getAttribute('update-id'); + periodNode = this.closest('tbody'); + + exceedTargetNode = updateContainer.querySelector('.update-exceed-target'); + exceedCheckbox = exceedTargetNode.querySelector('.update-exceed-target-checkbox'); + exceedValueNode = exceedTargetNode.querySelector('.exceed-value'); + + // When checkbox is checked, use the exceeding value + if (exceedCheckbox.checked) { + newValue = parseInt(exceedValueNode.value); + } else { + newValue = parseInt(updateContainer.querySelector('.update-dialog-timeline-marker:nth-last-child(2)').getAttribute('data-value')); + } + + if (updateId !== 'add') { + actualValue = parseInt(updateContainer.getAttribute('current-actual')); + value = newValue - actualValue + parseInt(updateContainer.getAttribute('current-change')); + } else { + actualValue = parseInt(periodNode.getAttribute('period-actual')); + value = newValue - actualValue; + } + + description = updateContainer.querySelector('.update-description').innerText; + + photoNode = updateContainer.querySelector('.photo-upload'); + if (photoNode !== null) { + photo = photoNode.files.length > 0 ? photoNode.files[0] : undefined; + } + periodId = parseInt(this.closest('tbody').getAttribute('period-id')); + updateId === 'add' ? addNewUpdate(description, periodId, value, photo) : editUpdate(updateId, periodId, description, value, updateContainer.getAttribute('current-change'), photo); + }); + } + } + + function addDeleteOnClicks() { + var els = document.querySelectorAll('.delete-button'); + + for (var i = 0; i < els.length; i++) { + var el = els[i]; + + el.addEventListener('click', function () { + var api_url, deleteConfirmContainer, deleteConfirmYes, deleteConfirmNo, periodId, periodNode, request, requestData, updateNode, updateChange, updateId; + + updateNode = this.closest('.update-entry-container'); + updateChange = parseInt(updateNode.getAttribute('current-change')); + updateId = updateNode.getAttribute('update-id'); + periodNode = updateNode.closest('tbody'); + periodId = periodNode.getAttribute('period-id'); + + // Show confirmation dialog + deleteConfirmContainer = updateNode.querySelector('.delete-confirm'); + + // Add a class to update-name so we can set a margin on it to make room + // for the delete confirmation dialog. + updateNode.querySelector('.update-name').classList.add('delete-pending'); + deleteConfirmContainer.style.display="block"; + + deleteConfirmNo = updateNode.querySelector('.delete-confirm-no'); + deleteConfirmNo.removeEventListener('click'); + deleteConfirmNo.addEventListener('click', function() { + + // Cancel the delete operation + deleteConfirmContainer.style.display='none'; + updateNode.querySelector('.update-name').classList.remove('delete-pending'); + }); + + deleteConfirmYes = updateNode.querySelector('.delete-confirm-yes'); + deleteConfirmYes.removeEventListener('click'); + deleteConfirmYes.addEventListener('click', function() { + + // Create request + api_url = '/rest/v1/project_update/' + updateId + '/?format=json'; + request = new XMLHttpRequest(); + request.open('DELETE', api_url, true); + request.setRequestHeader("X-CSRFToken", csrftoken); + request.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + + // TODO: Create recalculation function so that closing isn't necessary + savingPeriod(periodNode, true); + + request.onload = function() { + if (request.status === 204) { + // Object successfully deleted + removeUpdatefromStore(periodId, parseInt(updateId)); + updateActualValue(periodId, updateChange * -1); + + savingPeriod(periodNode, false); + + // Close the indicator period panel + periodNode.querySelector('.expand-indicator-period').click(); + deleteConfirmContainer.style.display='none'; + updateNode.querySelector('.update-name').classList.remove('delete-pending'); + updatePeriodValues(periodId, updateChange * -1); + + // Reopen the indicator period panel + periodNode.querySelector('.expand-indicator-period').click(); + return true; + } else { + // We reached our target server, but it returned an error + savingPeriodError(periodNode, 'Could not delete update.'); + return false; + } + }; + + request.onerror = function() { + // There was a connection error of some sort + savingPeriodError(periodNode, 'Connection error.'); + return false; + }; + + request.send(JSON.stringify(requestData)); + }); + }); + } + } + + function addEditOnClicks() { + var els = document.querySelectorAll('.edit-button'); + + for (var i = 0; i < els.length; i++) { + var el = els[i]; + + el.addEventListener('click', function() { + var updateContainer = this.parentNode; + var updateId = updateContainer.getAttribute('update-id'); + var periodNode = this.closest('tbody'); + var periodId = periodNode.getAttribute('period-id'); + var exceedTargetNode = updateContainer.querySelector('.update-exceed-target'); + var exceedCheckbox = exceedTargetNode.querySelector('.update-exceed-target-checkbox'); + var exceedValueNode = exceedTargetNode.querySelector('.exceed-value'); + var deleteButton = updateContainer.querySelector('.delete-button'); + + if (this.classList.contains('activated')) { + var editables = updateContainer.querySelectorAll('.editable'); + + updateContainer.classList.remove('edit-in-progress'); + + for (var j = 0; j < editables.length; j++) { + editables[j].classList.remove('editable'); + editables[j].removeAttribute('contenteditable'); + } + this.classList.remove('activated'); + updateContainer.querySelector('.save-button').classList.remove('active'); + updateContainer.querySelector('.cancel-button').classList.remove('active'); + updateContainer.querySelector('.edit-slider').noUiSlider.destroy(); + + if (deleteButton !== null) { + deleteButton.classList.remove('activated'); + } + + exceedCheckbox.setAttribute('disabled', ''); + exceedValueNode.setAttribute('disabled', ''); + + var photoUpload = updateContainer.querySelector('.photo-upload'); + photoUpload.parentNode.removeChild(photoUpload); + + exceedTargetNode.style.display = 'none'; + } else { + this.classList.add('activated'); + if (deleteButton !== null) { + deleteButton.classList.add('activated'); + } + + updateContainer.classList.add('edit-in-progress'); + + updateContainer.querySelector('.update-description').setAttribute('contentEditable', 'true'); + updateContainer.querySelector('.update-description').classList.add('editable'); + + updateContainer.querySelector('.save-button').classList.add('active'); + updateContainer.querySelector('.cancel-button').classList.add('active'); + + updateContainer.querySelector('.cancel-button').addEventListener('click', function() { + if (updateId === 'add') { + updateContainer.parentNode.removeChild(updateContainer); + displayAddButton(periodId, true); + } else { + periodNode.querySelector('.expand-indicator-period').click(); + periodNode.querySelector('.expand-indicator-period').click(); + } + }); + + var uploadPhoto = document.createElement('input'); + uploadPhoto.setAttribute('type', 'file'); + uploadPhoto.setAttribute('accept', 'image/*'); + uploadPhoto.classList.add('photo-upload'); + updateContainer.appendChild(uploadPhoto); + + var sliderEl = updateContainer.querySelector('.edit-slider'); + var startVal = updateContainer.getAttribute('current-actual'); + var minVal = parseInt(this.closest('.indicator-period').getAttribute('indicator-baseline')); + var maxVal = parseInt(sliderEl.getAttribute('data-max')); + + noUiSlider.create(sliderEl, { + start: startVal, + step: 1, + range: { + 'min': minVal, + 'max': maxVal + } + }); + + var updateMarker = updateContainer.querySelector('.update-dialog-timeline-marker:nth-last-child(2)'); + var updateProgress = updateContainer.querySelector('.indicator-bar-progress-amount'); + var updateActual = updateContainer.querySelector('.update-target-actual'); + var originalPercentageProgress = parseInt(updateMarker.style.left.substring(0, updateMarker.style.left.length - 1)); + var originalValue = updateActual.textContent; + var originalPositionMarkerEl = document.createElement('div'); + var changeIndicatorEl = document.createElement('div'); + var handleLabelEl = document.createElement('div'); + var handleChangeLabelEl = document.createElement('div'); + + originalPositionMarkerEl.classList.add('original-position-marker'); + originalPositionMarkerEl.style.left = originalPercentageProgress + '%'; + + changeIndicatorEl.classList.add('change-indicator'); + changeIndicatorEl.style.left = originalPercentageProgress + '%'; + + handleLabelEl.classList.add('handle-label'); + + // Placeholder to ensure label is correct size - under rare + // conditions label will have no text content until slider handle + // is moved. + handleLabelEl.textContent = '--'; + + handleChangeLabelEl.classList.add('handle-change-label'); + + + document.querySelector('.edit-slider').appendChild(originalPositionMarkerEl); + document.querySelector('.edit-slider').appendChild(changeIndicatorEl); + document.querySelector('.edit-slider .noUi-handle').appendChild(handleLabelEl); + document.querySelector('.edit-slider .noUi-handle').appendChild(handleChangeLabelEl); + + exceedCheckbox.removeAttribute('disabled'); + if (exceedCheckbox.checked) { + exceedValueNode.removeAttribute('disabled'); + displayEditSlider(updateContainer, false); + } + + sliderEl.noUiSlider.on('update', function(value) { + if (exceedCheckbox.checked) { + updateActual.textContent = exceedValueNode.value; + } else { + var percentage; + var changeValueIsNegative; + + value = parseInt(value); + + percentage = (value - minVal) / (maxVal - minVal) * 100; + percentage = percentage > 100 ? 100 : percentage; + + percentage < originalPercentageProgress ? changeValueIsNegative = true : changeValueIsNegative = false; + + updateMarker.style.left = percentage + '%'; + updateMarker.setAttribute('data-value', value); + updateProgress.style.width = percentage + '%'; + + handleLabelEl.textContent = value; + handleChangeLabelEl.textContent = changeValueIsNegative ? '-' : '+'; + handleChangeLabelEl.textContent += Math.abs(value - originalValue); + + if (changeValueIsNegative) { + // Change is negative + changeIndicatorEl.style.right = (100 - originalPercentageProgress) + '%'; + changeIndicatorEl.style.left = percentage + '%'; + changeIndicatorEl.classList.add('negative'); + } else { + // Change is positive + changeIndicatorEl.style.left = originalPercentageProgress + '%'; + changeIndicatorEl.style.right = (100 - percentage) + '%'; + changeIndicatorEl.classList.remove('negative'); + } + + updateActual.textContent = value; + } + }); + + fadeIn(exceedTargetNode, true); + + addSaveOnClicks(); + } + }); + } + } + + /* GENERAL HELPER FUNCTIONS */ + + /* Fade in + ** ======= + ** Takes a selector and fades in each matching element. + ** Takes an optional second argument, isElement, that indicates + ** the first argument in an element rather than a selector. + ** Takes an optional third argument, displayVal, that indicates + ** what display value to give the element after fading in. + ** If this argument is not present, defaults to 'block' + */ + function fadeIn(selector, isElement, displayVal) { + + if (isElement) { + fadeElIn(selector); + } else { + var els = document.querySelectorAll(selector); + + for (var i = 0; i < els.length; i++) { + var el = els[i]; + fadeElIn(el); + } + } + + function fadeElIn(el) { + var opacityCallback = getOpacityCallback(el); + var classCallback = getClassCallback(el); + + el.classList.add('opacity-transition'); + el.style.opacity = 0; + el.style.display = displayVal || 'block'; + el.classList.add('fading-in'); + + setTimeout(opacityCallback, 1); + setTimeout(classCallback, 250); + } + + function getOpacityCallback(el) { + var cb = function() { + el.style.opacity = 1; + }; + + return cb; + } + + function getClassCallback(el) { + var cb = function() { + el.classList.remove('fading-in'); + }; + + return cb; + } + } + + /* Fade out */ + + function fadeOut(selector) { + var els = document.querySelectorAll(selector); + + for (var i = 0; i < els.length; i++) { + var el = els[i]; + var opacityCallback = getOpacityCallback(el); + var displayCallback = getDisplayCallback(el); + + el.style.opacity = 1; + setTimeout(opacityCallback, 1); + setTimeout(displayCallback, 250); + } + + function getOpacityCallback(el) { + var cb = function() { + el.style.opacity = 0; + }; + + return cb; + } + + function getDisplayCallback(el) { + var cb = function() { + if (!el.classList.contains('fading-in')) { + el.style.display = 'none'; + } + }; + + return cb; + } + } + + /* CSRF TOKEN (this should really be added in base.html, we use it everywhere) */ + function getCookie(name) { + var cookieValue = null; + if (document.cookie && document.cookie !== '') { + var cookies = document.cookie.split(';'); + for (var i = 0; i < cookies.length; i++) { + var cookie = cookies[i].trim(); + if (cookie.substring(0, name.length + 1) == (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } + var csrftoken = getCookie('csrftoken'); + + /* Time remaining to edit (needs to be in GMT) */ + function getTimeDifference(endDate) { + var remainingDate = endDate - new Date(currentDate); + + return { + 'days': Math.floor( remainingDate/(1000*60*60*24) ), + 'hours': Math.floor( (remainingDate/(1000*60*60)) % 24 ), + 'minutes': Math.floor( (remainingDate/1000/60) % 60 ) + }; + } + + function setMinutesRemaining(node, createdAtDate) { + var endDate = createdAtDate; + endDate.setMinutes(endDate.getMinutes() + parseInt(defaultValues.update_timeout)); + + var timeinterval = setInterval(function(){ + var remainingTime = getTimeDifference(endDate); + node.setAttribute('title', remainingTime.minutes + ' minutes remaining'); + + if (remainingTime.minutes < 0) { + clearInterval(timeinterval); + node.parentNode.removeChild(node); + } + },100); + } + + /* Upload photo */ + function uploadPhoto(photo, updateId, periodNode) { + var apiUrl, fileRequest, formData; + + apiUrl = '/rest/v1/project_update/' + updateId + '/upload_photo/?format=json'; + + formData = new FormData(); + formData.append("photo", photo); + + fileRequest = new XMLHttpRequest(); + fileRequest.open("POST", apiUrl); + fileRequest.setRequestHeader("X-CSRFToken", csrftoken); + + fileRequest.onload = function() { + if (fileRequest.status >= 200 && fileRequest.status < 400) { + addAdditionalUpdateData(updateId); + savingPeriod(periodNode, false); + return false; + } else { + // We reached our target server, but it returned an error + savingPeriodError(periodNode, 'Uploading photo failed.'); + return false; + } + }; + + fileRequest.onerror = function() { + // There was a connection error of some sort + savingPeriodError(periodNode, 'Uploading photo failed: connection error.'); + return false; + }; + + fileRequest.send(formData); + } + + /* Add new update */ + function addNewUpdate(text, periodId, value, photo) { + var api_url, periodNode, request, requestData; + + removeUpdatefromStore(periodId, 'add'); + periodNode = findPeriod(periodId); + + // TODO: Create recalculation function per period so that closing it isn't necessary + savingPeriod(periodNode, true); + + // Create request + api_url = '/rest/v1/project_update/?format=json'; + request = new XMLHttpRequest(); + request.open('POST', api_url, true); + request.setRequestHeader("X-CSRFToken", csrftoken); + request.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + + request.onload = function() { + if (request.status === 201) { + // Object successfully created + + // This callback expands the indicator period panel when the new data is loaded + var callback = function(){ + periodNode.querySelector('.expand-indicator-period').click(); + }; + + // TODO: only close the panel and remove the "saving" message once the "addAdditionalUpdateData" + // call has finished + + // Close the indicator period panel + periodNode.querySelector('.expand-indicator-period').click(); + + var response, updateId, updateNode; + response = JSON.parse(request.response); + updateId = response.id; + + updateActualValue(periodId, value); + addAdditionalUpdateData(updateId, callback); + updatePeriodValues(periodId, value); + // updateUpdateValues(periodId, 'add', updateId, value); + + // Upload photo + if (photo !== undefined) { + uploadPhoto(photo, updateId, periodNode); + } else { + savingPeriod(periodNode, false); + } + + return true; + } else { + // We reached our target server, but it returned an error + savingPeriodError(periodNode, 'Adding update failed.'); + return false; + } + }; + + request.onerror = function() { + // There was a connection error of some sort + savingPeriodError(periodNode, 'Connection error.'); + return false; + }; + + requestData = { + project: defaultValues.project_id, + user: defaultValues.user_id, + title: 'Indicator update', + text: text, + indicator_period: periodId, + period_update: value + }; + + request.send(JSON.stringify(requestData)); + } + + /* Edit existing update */ + function editUpdate(updateId, periodId, text, value, oldValue, photo) { + var api_url, periodNode, request; + + periodNode = findPeriod(periodId); + savingPeriod(periodNode, true); + + // Create request + api_url = '/rest/v1/project_update/' + updateId + '/?format=json'; + request = new XMLHttpRequest(); + request.open('PATCH', api_url, true); + request.setRequestHeader("X-CSRFToken", csrftoken); + request.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + + request.onload = function() { + if (request.status >= 200 && request.status < 400) { + // Object successfully saved + + // This callback expands the indicator period panel when the new data is loaded + var callback = function(){ + periodNode.querySelector('.expand-indicator-period').click(); + }; + + // TODO: only close the panel and remove the "saving" message once the "addAdditionalUpdateData" + // call has finished + + // Close the indicator period panel + periodNode.querySelector('.expand-indicator-period').click(); + addAdditionalUpdateData(updateId, callback); + updateActualValue(periodId, value - oldValue); + // updateUpdateValues(periodId, updateId, updateId, value); + updatePeriodValues(periodId, (value - oldValue)); + + // Upload photo + if (photo !== undefined) { + uploadPhoto(photo, updateId, periodNode); + } else { + savingPeriod(periodNode, false); + } + + return true; + } else { + // We reached our target server, but it returned an error + savingPeriodError(periodNode, 'Editing update failed.'); + return false; + } + }; + + request.onerror = function() { + // There was a connection error of some sort + savingPeriodError(periodNode, 'Connection error.'); + return false; + }; + + request.send(JSON.stringify({text: text, period_update: value})); + } + + /* Display the photo in the update. Either by replacing the existing photo or by adding a new photo */ + function displayPhoto(update, updateNode) { + if (update.photo) { + var photoNode = document.createElement('div'); + photoNode.classList.add('update-photo'); + photoNode.style['background-image'] = 'url("' + update.photo + '")'; + photoNode.style['background-size'] = 'cover'; + photoNode.style.height = '120px'; + photoNode.style.width = '200px'; + updateNode.appendChild(photoNode); + } + } + + /* Display the add button in the top row */ + function displayAddButton(periodId, display) { + var addUpdateButton, canUpdate, isAdmin, periodNode; + + canUpdate = defaultValues.can_update; + isAdmin = defaultValues.user_is_admin; + periodNode = findPeriod(periodId); + addUpdateButton = periodNode.querySelector('.add-button'); + + addUpdateButton.style.display = display && (isAdmin || canUpdate) ? '' : 'none'; + } + + /* Display the edit slider in the update */ + function displayEditSlider(updateNode, display) { + var editSlider = updateNode.querySelector('.edit-slider'); + editSlider.style.display = display ? '' : 'none'; + } + + /* Get additional data of update */ + function addAdditionalUpdateData(updateId, callback) { + var api_url, request; + + // Create request + api_url = '/rest/v1/project_update_extra/' + updateId + '/?format=json'; + request = new XMLHttpRequest(); + request.open('GET', api_url, true); + request.setRequestHeader("X-CSRFToken", csrftoken); + request.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + + request.onload = function() { + if (request.status >= 200 && request.status < 400) { + // Object successfully retrieved + var response = JSON.parse(request.response); + addUpdateToStore(response); + callback(); + } else { + return false; + } + }; + + request.onerror = function() { + // There was a connection error of some sort + return false; + }; + + request.send(); + } + + /* Update the values of a period */ + function updateActualValue(periodId, value) { + var baseline, newPercentage, newValue, oldValue, periodNode, target; + + periodNode = findPeriod(periodId); + target = parseInt(periodNode.getAttribute('period-target')); + oldValue = parseInt(periodNode.getAttribute('period-actual')); + newValue = oldValue + parseInt(value); + baseline = parseInt(periodNode.getAttribute('indicator-baseline')); + periodNode.setAttribute('period-actual', newValue); + + newPercentage = (newValue - baseline) / (target - baseline) * 100; + newPercentage = newPercentage > 100 ? 100 : newPercentage; + periodNode.querySelector('.indicator-bar-progress-amount').setAttribute('style', 'width: ' + newPercentage + '%;'); + periodNode.querySelector('.indicator-bar-progress').setAttribute('style', 'left: ' + newPercentage + '%; z-index: ' + newPercentage + ';'); + periodNode.querySelector('.indicator-bar-progress').setAttribute('data-progress', newPercentage); + periodNode.querySelector('.indicator-bar-progress-text').textContent = newValue.toString();// > target ? target.toString() : newValue.toString(); + } + + /* Update the values of an update */ + function updateUpdateValues(periodId, oldUpdateId, newUpdateId, change) { + var updateNode = findUpdate(periodId, oldUpdateId); + updateNode.setAttribute('update-id', newUpdateId); + updateNode.setAttribute('current-change', change); + // Only works for the latest update + updateNode.setAttribute('current-actual', findPeriod(periodId).getAttribute('period-actual')); + } + + /* Update the values of other periods when an earlier period is edited */ + function updatePeriodValues(periodId, change) { + var baseDate = new Date(document.querySelector('.indicator-period[period-id="' + periodId + '"] .toTime').textContent); + var resultEl = document.querySelector('.indicator-period[period-id="' + periodId + '"]').closest('.indicator-group'); + var allPeriods = resultEl.querySelectorAll('.indicator-period'); + + for (var i = 0; i < allPeriods.length; i++) { + var period = allPeriods[i]; + var periodDate = new Date(period.querySelector('.fromTime').textContent); + + if (periodDate.getTime() >= baseDate.getTime()) { + // This indicator period is more recent that in the original + // We need to update it's value with the new changes from the original + + var oldStart = parseInt(period.getAttribute('period-start')); + var oldActual = parseInt(period.getAttribute('period-actual')); + var oldProgress = parseInt(period.querySelector('.indicator-bar-progress-text')); + + + var baseline = parseInt(period.getAttribute('indicator-baseline')); + var target = parseInt(period.getAttribute('period-target')); + var newValue = oldActual + change; + var completionPercentage = ((newValue - baseline) / (target - baseline)) * 100; + + if (completionPercentage > 100) completionPercentage = 100; + + period.setAttribute('period-start', oldStart + change); + period.setAttribute('period-actual', newValue); + period.querySelector('.indicator-bar-progress-text').textContent = newValue; + period.querySelector('.indicator-bar-progress').style.left = completionPercentage + '%'; + period.querySelector('.indicator-bar-progress-amount').style.width = completionPercentage + '%'; + } + } + } + + /* Find the period node based on its' ID */ + function findPeriod(periodId) { + var indicatorsPeriods = document.querySelectorAll('tbody.indicator-period'); + for (var i=0; i < indicatorsPeriods.length; i++) { + if (indicatorsPeriods[i].getAttribute('period-id') == periodId) { + return indicatorsPeriods[i]; + } + } + } + + /* Find the update node based on the period ID and update ID */ + function findUpdate(periodId, updateId) { + var periodNode = findPeriod(periodId); + var updateNodes = periodNode.querySelectorAll('.update-entry-container'); + for (var i=0; i < updateNodes.length; i++) { + if (updateNodes[i].getAttribute('update-id') == updateId) { + return updateNodes[i]; + } + } + } + + /* User allowed to click edit button */ + function allowEdit(update) { + if (defaultValues.user_is_admin) { + return true; + } else { + var endDate = new Date(update.created_at); + endDate.setMinutes(endDate.getMinutes() + parseInt(defaultValues.update_timeout)); + var remaining = getTimeDifference(endDate); + var enoughTime = !!(remaining.days >= 0 && remaining.hours >= 0 && remaining.minutes >= 0); + var userPk = defaultValues.user_id; + return !!(enoughTime && userPk == update.user.id); + } + } + + /* Hide all elements for a given selector */ + function hideAll(selector) { + var els = document.querySelectorAll(selector); + + for (var i = 0; i < els.length; i++) { + var el = els[i]; + + el.style.display = 'none'; + } + } + + /* Remove all elements for a given selector from the DOM */ + function removeFromPeriod(periodId, selector) { + var periodNode, els; + + periodNode = findPeriod(periodId); + els = periodNode.querySelectorAll(selector); + + for (var i = 0; i < els.length; i++) { + var el = els[i]; + + el.parentNode.removeChild(el); + } + } + + /* Remove a class from all elements for a given selector */ + function removeClassFromAll(selector, className, periodId) { + var els, periodNode; + + if (periodId === undefined) { + els = document.querySelectorAll(selector); + } else { + periodNode = findPeriod(periodId); + els = periodNode.querySelectorAll(selector); + } + + for (var i = 0; i < els.length; i++) { + els[i].classList.remove(className); + } + } + function addClassActive(elem) { + // get all 'a' elements + var a = document.getElementsByTagName('a'); + // loop through all 'a' elements + for (i = 0; i < a.length; i++) { + // Remove the class 'active' if it exists + a[i].classList.remove('active'); + } + // add 'active' classs to the element that was clicked + elem.classList.add('active'); + } + var updatesByIndicatorPeriod = {}; + + /* Get the grab the update data from the script we use to print + ** the template tags, then format that data into an object sorted + ** by indicator period, to make life easier for us later. + */ + + function buildUpdateJSON() { + var updateJSON = document.querySelector('#update-json').innerHTML; + var updateData = JSON.parse(updateJSON); + + for (var i=0; i= 0; i--) { + containerCell.appendChild(childElements[i]); + } + } + container.appendChild(containerCell); + return container; + } + + /* Returns a node representing a single "update" entry for the "Add update" dialog. + ** E.G if an indicator period has 7 updates, this function should be called once + ** for each and return the appropriate markup each time. + */ + + function getUpdateEntry(update) { + var updateContainer = document.createElement('div'); + var deleteEl, deleteConfirmEl, deleteConfirmContentsEl, deleteConfirmYesEl, deleteConfirmNoEl, editEl, dateEl, userNameEl, timeLineEl, targetEl, targetText, photoEl, descriptionEl; + + updateContainer.setAttribute("update-id", update.id); + updateContainer.setAttribute("current-change", update.period_update); + updateContainer.classList.add('update-entry-container'); + updateContainer.classList.add('bg-border-transition'); + + if (allowEdit(update)) { + editEl = document.createElement('i'); + editEl.classList.add('fa'); + editEl.classList.add('fa-pencil-square-o'); + editEl.classList.add('edit-button'); + editEl.classList.add('clickable'); + updateContainer.appendChild(editEl); + + if (!defaultValues.user_is_admin) { + setMinutesRemaining(editEl, new Date(update.created_at)); + } else { + deleteEl = document.createElement('i'); + deleteEl.classList.add('fa'); + deleteEl.classList.add('fa-trash-o'); + deleteEl.classList.add('delete-button'); + deleteEl.classList.add('clickable'); + updateContainer.appendChild(deleteEl); + + deleteConfirmEl = document.createElement('div'); + // TODO: use "delete_confirm_text" from translation strings in main template + deleteConfirmEl.textContent = "Are you sure you want to delete this update?"; + deleteConfirmEl.style.display = 'none'; + deleteConfirmEl.classList.add('delete-confirm'); + + deleteConfirmYesEl = document.createElement('a'); + deleteConfirmYesEl.classList.add('btn'); + deleteConfirmYesEl.classList.add('btn-primary'); + deleteConfirmYesEl.classList.add('delete-confirm-yes'); + // TODO: use "delete_confirm_yes_text" from translation strings in main template + deleteConfirmYesEl.textContent = "Delete update"; + + deleteConfirmNoEl = document.createElement('a'); + deleteConfirmNoEl.classList.add('btn'); + deleteConfirmNoEl.classList.add('btn-primary'); + deleteConfirmNoEl.classList.add('delete-confirm-no'); + // TODO: use "delete_confirm_cancel_text" from translation strings in main template + deleteConfirmNoEl.textContent = "Cancel"; + + deleteConfirmContentsEl = document.createElement('div'); + + deleteConfirmContentsEl.appendChild(deleteConfirmNoEl); + deleteConfirmContentsEl.appendChild(deleteConfirmYesEl); + deleteConfirmEl.appendChild(deleteConfirmContentsEl); + updateContainer.appendChild(deleteConfirmEl); + } + } + + if (update.created_at) { + var dateObj = new Date(update.created_at); + dateEl = document.createElement('div'); + dateEl.classList.add('update-date'); + dateEl.textContent = dateObj.toLocaleDateString() + ' ' + dateObj.toLocaleTimeString(); + updateContainer.appendChild(dateEl); + } + if (update.user) { + userNameEl = document.createElement('div'); + userNameEl.classList.add('update-name'); + userNameEl.textContent = update.user.first_name + ' ' + update.user.last_name; + updateContainer.appendChild(userNameEl); + } + + timeLineEl = document.createElement('div'); + timeLineEl.classList.add('update-timeline'); + + var progressContainer = document.createElement('div'); + progressContainer.classList.add('indicator-bar-display-container'); + + var markerContainer = document.createElement('div'); + markerContainer.classList.add('indicator-bar-progress-container'); + + var previousUpdates = []; + /* Now we need to loop through every update with an ID lower than the id of *this* update, + ** and store its change amount so we can build the timeline. + */ + for (var updateID in updatesByIndicatorPeriod[update.indicator_period.id]) { + if (updatesByIndicatorPeriod[update.indicator_period.id].hasOwnProperty(updateID)) { + var previousUpdate = updatesByIndicatorPeriod[update.indicator_period.id][updateID]; + + /* If the change amount is "None", changes this to zero */ + if (previousUpdate.period_update === 'None') { + previousUpdate.period_update = "0"; + } + + previousUpdates.push(previousUpdate); + } + } + + previousUpdates.sort(function(a,b) { + return a.id > b.id; + }); + + var actualText, periodNode, periodTarget, periodBaseline, progress; + + periodNode = findPeriod(update.indicator_period.id); + periodTarget = parseInt(periodNode.getAttribute('period-target')); + periodBaseline = parseInt(periodNode.getAttribute('period-start')); + indicatorBaseline = parseInt(periodNode.getAttribute('indicator-baseline')); + + progress = periodBaseline; + + for (var i=0; i < previousUpdates.length; i++) { + var entry = previousUpdates[i]; + + if (entry.id <= update.id || update.id === 'add') { + /* This update is older (or the same) as the update we're building the dialog for. + We need to make a marker for it. */ + progress += parseInt(entry.period_update, 10); + + var updateMarker = document.createElement('div'); + var textSpan = document.createElement('div'); + + var percentage = (progress - indicatorBaseline) / (periodTarget - indicatorBaseline) * 100; + percentage = percentage > 100 ? 100 : percentage; + + updateMarker.classList.add('update-dialog-timeline-marker'); + updateMarker.classList.add('indicator-bar-progress'); + updateMarker.style.left = percentage + '%'; + updateMarker.style['z-index'] = progress; + + textSpan.classList.add('indicator-bar-progress-text'); + textSpan.classList.add('bg-transition'); + textSpan.textContent = progress; + textSpan.style.left = percentage + '%'; + + var textHoverEl = document.createElement('span'); + var createdDate = new Date(entry.created_at); + + textHoverEl.classList.add('progress-hover-text'); + textHoverEl.classList.add('opacity-transition'); + textHoverEl.textContent = createdDate.toLocaleDateString() + ' ' + createdDate.toLocaleTimeString(); + textSpan.appendChild(textHoverEl); + + markerContainer.appendChild(updateMarker); + markerContainer.appendChild(textSpan); + } + } + + if (progress > periodTarget) { + updateContainer.classList.add('target-exceeded'); + } + + var baselineEl = document.createElement('div'); + baselineEl.classList.add('indicator-baseline'); + baselineEl.textContent = periodNode.querySelector('.indicator-bar-td > div > .indicator-baseline').textContent; + progressContainer.appendChild(baselineEl); + + var indicatorBar = document.createElement('div'); + indicatorBar.classList.add('indicator-bar'); + progressContainer.appendChild(indicatorBar); + + var highPercentage = (progress - indicatorBaseline) / (periodTarget - indicatorBaseline) * 100; + highPercentage = highPercentage > 100 ? 100 : highPercentage; + + var indicatorBarProgressAmount = document.createElement('div'); + indicatorBarProgressAmount.classList.add('indicator-bar-progress-amount'); + indicatorBarProgressAmount.style.width = highPercentage + '%'; + + progressContainer.appendChild(indicatorBarProgressAmount); + progressContainer.appendChild(markerContainer); + + updateContainer.setAttribute("current-actual", progress); + var sliderEl = document.createElement('div'); + sliderEl.classList.add('edit-slider'); + sliderEl.setAttribute('data-start', progress); + sliderEl.setAttribute('data-max', periodTarget); + + progressContainer.appendChild(sliderEl); + timeLineEl.appendChild(progressContainer); + updateContainer.appendChild(timeLineEl); + + targetEl = document.createElement('div'); + targetEl.classList.add('update-target'); + + actualText = document.createElement('span'); + actualText.classList.add('update-target-actual'); + actualText.textContent = progress; + targetEl.appendChild(actualText); + + targetText = document.createElement('span'); + actualText.classList.add('update-target-actual'); + targetText.textContent = ' / ' + periodTarget; + targetEl.appendChild(targetText); + updateContainer.appendChild(targetEl); + + var exceedTargetEl = document.createElement('div'); + exceedTargetEl.classList.add('update-exceed-target'); + + var exceedTargetLabel = document.createElement('label'); + exceedTargetLabel.textContent = 'Exceed target'; + exceedTargetLabel.setAttribute('for', 'exceed-' + update.id); + + var exceedTargetCheckbox = document.createElement('input'); + exceedTargetCheckbox.setAttribute('type', 'checkbox'); + exceedTargetCheckbox.setAttribute('id', 'exceed-' + update.id); + exceedTargetCheckbox.setAttribute('disabled', ''); + exceedTargetCheckbox.classList.add('update-exceed-target-checkbox'); + + var exceedTargetNewValue = document.createElement('input'); + exceedTargetNewValue.setAttribute('type', 'number'); + exceedTargetNewValue.setAttribute('disabled', ''); + exceedTargetNewValue.classList.add('exceed-value'); + exceedTargetNewValue.classList.add('opacity-transition'); + + exceedTargetNewValue.addEventListener('input', function () { + if (exceedTargetCheckbox.checked) { + updateContainer.querySelector('.update-target-actual').textContent = exceedTargetNewValue.value; + } + }); + + exceedTargetCheckbox.addEventListener('change', function () { + if (exceedTargetCheckbox.checked) { + exceedTargetNewValue.removeAttribute('disabled'); + exceedTargetNewValue.value = parseInt(findPeriod(update.indicator_period.id).getAttribute('period-target')); + displayEditSlider(updateContainer, false); + } else { + exceedTargetNewValue.value = ''; + exceedTargetNewValue.setAttribute('disabled', ''); + displayEditSlider(updateContainer, true); + } + }); + + exceedTargetEl.appendChild(exceedTargetCheckbox); + exceedTargetEl.appendChild(exceedTargetLabel); + exceedTargetEl.appendChild(exceedTargetNewValue); + exceedTargetEl.style.display = 'none'; + updateContainer.appendChild(exceedTargetEl); + + if (progress > periodTarget) { + exceedTargetNewValue.value = progress; + exceedTargetCheckbox.checked = true; + } + + descriptionEl = document.createElement('div'); + descriptionEl.classList.add('update-description'); + descriptionEl.innerHTML = update.text.replace(/\n/g,"
"); + + updateContainer.appendChild(descriptionEl); + + displayPhoto(update, updateContainer); + + var saveEl = document.createElement('div'); + saveEl.classList.add('save-button'); + saveEl.classList.add('clickable'); + saveEl.textContent = 'Save'; + + updateContainer.appendChild(saveEl); + + var cancelEl = document.createElement('div'); + cancelEl.classList.add('cancel-button'); + cancelEl.classList.add('clickable'); + cancelEl.textContent = 'Cancel'; + updateContainer.appendChild(cancelEl); + + return updateContainer; + } + + function readMoreOnClicks() { + function setReadMore(show, hide) { + return function(e) { + e.preventDefault(); + hide.classList.add('hidden'); + show.classList.remove('hidden'); + }; + } + + 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); + } + + function setCurrentDate() { + var interval = setInterval(function(){ + var localCurrentDate = new Date(currentDate); + localCurrentDate.setSeconds(localCurrentDate.getSeconds() + 1); + currentDate = localCurrentDate.toString(); + }, 1000); + } + + 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 + '"]'); + + for (var i = 0; i < allTabs.length; i++) { + var tab = allTabs[i]; + + tab.style.display = 'none'; + } + for (var j = 0; j < allTabLinks.length; j++) { + var tabLink = allTabLinks[j]; + + tabLink.classList.remove('selected'); + } + + activeTab.style.display = 'block'; + activeTabLink.classList.add('selected'); + } + + function setTabOnClicks() { + var allTabs = document.querySelectorAll('.tab-link'); + + for (var i = 0; i < allTabs.length; i++) { + var tab = allTabs[i]; + + tab.addEventListener('click', function() { + var tabClass = this.getAttribute('href'); + + // Remove the '#' from the href + tabClass = tabClass.substring(1); + showTab(tabClass); + }); + } + } + + 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'); + } + } + + /* POLYFILLS */ + + // 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() { + setCurrentDate(); + + // Setup results framework + setResultExpandOnClicks(); + setIndicatorLinkOnClicks(); + setExpandIndicatorPeriodOnClicks(); + addAddOnClicks(); + buildUpdateJSON(); + }); \ No newline at end of file diff --git a/akvo/rsr/static/styles-src/library.css b/akvo/rsr/static/styles-src/library.css index 848c8bd781..e5d11375e6 100755 --- a/akvo/rsr/static/styles-src/library.css +++ b/akvo/rsr/static/styles-src/library.css @@ -28,4 +28,4 @@ /* Media Queries ========================================================================== */ /* Placeholder - ========================================================================== */ + ========================================================================== */ \ 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 80fc993d5f..a5b070a70a 100755 --- a/akvo/rsr/static/styles-src/main.css +++ b/akvo/rsr/static/styles-src/main.css @@ -45,19 +45,14 @@ body { color: #394c50; line-height: 1.42857; text-rendering: optimizelegibility; - font-family: "Open Sans", "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-family: 'Open Sans', 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif; padding-top: 80px; } @media only screen and (max-width: 768px) { body { padding-top: 65px; } } -h1, -h2, -h3, -h4, -h5, -h6 { - font-family: "Montserrat", "Helvetica Neue", Helvetica, Arial, sans-serif; +h1, h2, h3, h4, h5, h6 { + font-family: 'Montserrat', "Helvetica Neue", Helvetica, Arial, sans-serif; font-weight: normal; color: #2c2a74; } @@ -98,7 +93,7 @@ time { color: #de8929; } em { - font-family: "Baskerville", "Goudy Old Style", "Palatino", "Book Antiqua", Georgia, serif; + font-family: 'Baskerville', "Goudy Old Style", "Palatino", "Book Antiqua", Georgia, serif; font-size: 1.2em; margin: 0 5px 0em 0em; font-style: italic; } @@ -213,8 +208,7 @@ span.twitter-typeahead .tt-suggestion > p { color: rgba(0, 0, 0, 0.7); white-space: nowrap; } -span.twitter-typeahead .tt-suggestion > p:hover, -span.twitter-typeahead .tt-suggestion > p:focus { +span.twitter-typeahead .tt-suggestion > p:hover, span.twitter-typeahead .tt-suggestion > p:focus { color: #ffffff; text-decoration: none; outline: 0; @@ -249,16 +243,13 @@ dl.dl-horizontal dt { font-weight: bold; } dl.dl-horizontal dt.funders { margin-top: 5px; } - dl.dl-horizontal dd.totalFinance { margin-top: 30px; font-weight: bold; } - dl.dl-horizontal dd.funders { margin-top: 5px; } dl.dl-horizontal dd.funders span.iati-activity-id { font-style: italic; } - dl.dl-horizontal.org_statistics_table dd { text-align: right; } @@ -324,7 +315,7 @@ nav.navbar-fixed-top { padding-top: 3px; } nav.navbar-fixed-top .navbar-nav li a { color: #00aaff; - font-family: "Montserrat", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-family: 'Montserrat', "Helvetica Neue", Helvetica, Arial, sans-serif; border: thin solid transparent; } nav.navbar-fixed-top .navbar-nav li a:hover { color: #ff5500; } @@ -439,7 +430,7 @@ nav.navbar-fixed-top { -webkit-box-shadow: rgba(0, 0, 0, 0.17255) 0px 6px 12px 0px; } nav.navbar-fixed-top .navbar-nav.navbar-right li .langDropdown .dropdown-menu a { color: #00aaff; - font-family: "Montserrat", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-family: 'Montserrat', "Helvetica Neue", Helvetica, Arial, sans-serif; border: thin solid transparent; } nav.navbar-fixed-top .navbar-nav.navbar-right li .langDropdown .dropdown-menu a:hover { color: #ff5500; } @@ -450,10 +441,10 @@ nav.navbar-fixed-top { -moz-border-radius: 3px; -o-border-radius: 3px; -webkit-border-radius: 3px; - border-radius: 3px; } } - @media only screen and (max-width: 768px) and (max-width: 1024px) { - nav.navbar-fixed-top .navbar-nav.navbar-right li .langDropdown .dropdown-menu a.active { - background: rgba(255, 85, 0, 0.1); } } + border-radius: 3px; } + @media only screen and (max-width: 1024px) { + nav.navbar-fixed-top .navbar-nav.navbar-right li .langDropdown .dropdown-menu a.active { + background: rgba(255, 85, 0, 0.1); } } } .navbar .container { /* nav menu overflows to two lines in spanish and french unless container width @@ -519,7 +510,7 @@ nav.navbar-fixed-top { display: inline; } .myRsrMenu nav[role=navigation] ul li a { color: #00aaff; - font-family: "Montserrat", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-family: 'Montserrat', "Helvetica Neue", Helvetica, Arial, sans-serif; padding: 15px; } @media only screen and (max-width: 768px) { .myRsrMenu nav[role=navigation] ul li a { @@ -540,14 +531,11 @@ nav.navbar-fixed-top { background: rgba(217, 83, 79, 0.1) !important; } .myProjectList tbody tr.isPrivate { background: rgba(240, 173, 78, 0.1) !important; } - @media only screen and (min-width: 1200px) { .myProjectList div.projectListMenu { min-width: 170px; } } - .myProjectList div.projectListMenu > a { min-width: 70px; } - .myProjectList a { margin-right: 25px; } .myProjectList a:last-child { @@ -597,7 +585,6 @@ div.paginationWrap { overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } - .table-responsive .media .media-body p { max-width: 170px; overflow: hidden; @@ -985,12 +972,10 @@ h4.detailedInfo { .main-list li dl.dl-horizontal dd { margin-left: 0px; display: block; } } - .main-list .excerpt { overflow: hidden; text-overflow: ellipsis; white-space: normal; } - .main-list.projects ul li { border: thin solid rgba(44, 42, 116, 0); -moz-transition: all 0.2s ease-in; @@ -1021,7 +1006,6 @@ h4.detailedInfo { margin-top: 1em; } } .main-list.projects ul li .donateButton .totalBudgetLabel { width: initial; } - .main-list.updates ul li { border: thin solid rgba(0, 167, 157, 0); -moz-transition: all 0.2s ease-in; @@ -1054,7 +1038,6 @@ h4.detailedInfo { list-style-type: initial; margin: initial; padding: initial; } - .main-list.organisations ul li { border: thin solid rgba(238, 49, 36, 0); -moz-transition: all 0.2s ease-in; @@ -1087,7 +1070,7 @@ h4.detailedInfo { .main-list.organisations ul li img { filter: gray; -webkit-filter: grayscale(100%); - filter: url("data:image/svg+xml;utf8,#grayscale"); + filter: url("data:image/svg+xml;utf8,#grayscale"); filter: grayscale(100%); transition: all 0.2s ease-in; } .main-list.organisations ul li:hover img { @@ -1251,7 +1234,7 @@ div.projectTopRow { width: 80%; filter: gray; -webkit-filter: grayscale(100%); - filter: url("data:image/svg+xml;utf8,#grayscale"); + filter: url("data:image/svg+xml;utf8,#grayscale"); filter: grayscale(100%); transition: all 0.2s ease-in; margin: inherit; } @@ -1311,7 +1294,6 @@ div.textBlock { #accordion .panel-group .panel-heading a:before { content: '- '; position: relative; } - #accordion .panel-group .panel-heading a.collapsed:before { content: '+ '; top: 1px; } @@ -1377,9 +1359,7 @@ div.textBlock { -webkit-border-radius: 5px; border-radius: 5px; } -.project-hierarchy-window:hover, -.project-hierarchy-window._jsPlumb_source_hover, -.project-hierarchy-window._jsPlumb_target_hover { +.project-hierarchy-window:hover, .project-hierarchy-window._jsPlumb_source_hover, .project-hierarchy-window._jsPlumb_target_hover { border: 1px solid orange; color: orange; } @@ -1443,8 +1423,7 @@ ul.typeahead-selector { padding-left: 6px; padding-top: 4px; padding-bottom: 4px; } - ul.typeahead-selector li:hover, - ul.typeahead-selector li.hover { + ul.typeahead-selector li:hover, ul.typeahead-selector li.hover { font-weight: bold; cursor: pointer; } ul.typeahead-selector li a { @@ -1456,12 +1435,10 @@ ul.typeahead-selector { /* Organisation Page */ .organisationDetail .organisationHeader { background: rgba(32, 32, 36, 0.05); } - .organisationDetail .orgDescr { background-color: rgba(244, 116, 107, 0); } .organisationDetail .orgDescr p { padding-left: 20px; } - .organisationDetail h1 { margin-top: 0; margin-bottom: 20px; @@ -1472,26 +1449,20 @@ ul.typeahead-selector { .organisationDetail h1 i { font-size: 0.75em; top: -5px; } - .organisationDetail .orgLogo { padding: 10px 0; } .organisationDetail .orgLogo img { width: 35%; margin-left: 5px; } - .organisationDetail .orgDetails { background: rgba(255, 255, 255, 0.45); -moz-border-radius: 5px; -o-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; } - -.organisationDetail .orgUrl, -.organisationDetail .orgIati, -.organisationDetail .orgEmail { +.organisationDetail .orgUrl, .organisationDetail .orgIati, .organisationDetail .orgEmail { display: block; padding-bottom: 0.75em; } - @media only screen and (max-width: 768px) { .organisationDetail .orgDetails > h4 { padding-top: 0.75em; } } @@ -1514,7 +1485,6 @@ ul.typeahead-selector { .updateMain h2 { color: #00a79d; margin-top: 0; } - .updateMain .asideUpList .row { padding: 15px 0; } .updateMain .asideUpList .row:nth-of-type(2n+1) { @@ -1532,9 +1502,7 @@ ul.typeahead-selector { width: 95%; max-width: 450px; text-align: justify; } - .projectDonate .paymentOption, - .projectDonate .donateTitle, - .projectDonate h3 { + .projectDonate .paymentOption, .projectDonate .donateTitle, .projectDonate h3 { text-align: center; } } .projectDonate .donateButton { margin-bottom: 2em; } @@ -1555,10 +1523,10 @@ ul.typeahead-selector { clear: both; } .projectDonate .paymentSystemInfo { background-color: #f2f0e6; } - -@media only screen and (min-width: 768px) and (max-width: 992px) { - .projectDonate h3 { - font-size: 1.3em; } } + @media only screen and (min-width: 768px) { + @media only screen and (max-width: 992px) { + .projectDonate h3 { + font-size: 1.3em; } } } @media only screen and (max-width: 768px) { .projectDonate .paymentOption { margin-top: 3em; } @@ -1576,10 +1544,8 @@ ul.typeahead-selector { /* Financial details page */ section.projectFinancial .currentFunders dl > dd { text-align: right; } - section.projectFinancial .donationBreak { display: none; } - @media only screen and (max-width: 768px) { section.projectFinancial { width: 95%; @@ -1588,22 +1554,21 @@ section.projectFinancial .donationBreak { margin-right: auto; } section.projectFinancial .dl-horizontal dt { float: left; } } - -@media only screen and (max-width: 992px) and (min-width: 768px) { - section.projectFinancial .dl-horizontal dt { - width: initial; } - section.projectFinancial .donationBreak { - display: initial; - clear: both; } - section.projectFinancial .currentFunders dd.donation { - float: right; } - section.projectFinancial .currentFunders dt.donation { - clear: both; } } +@media only screen and (max-width: 992px) { + @media only screen and (min-width: 768px) { + section.projectFinancial .dl-horizontal dt { + width: initial; } + section.projectFinancial .donationBreak { + display: initial; + clear: both; } + section.projectFinancial .currentFunders dd.donation { + float: right; } + section.projectFinancial .currentFunders dt.donation { + clear: both; } } } /* Project Partners details page */ section.projectPartners .title { margin-bottom: 1em; } - section.projectPartners .row { margin-left: 0; margin-right: 0; @@ -1638,7 +1603,6 @@ section.projectPartners .row { padding: 0; } section.projectPartners .row li { list-style: none; } - @media only screen and (max-width: 768px) { section.projectPartners .container { padding-left: 0; @@ -1662,18 +1626,17 @@ dd.currencyAmount { max-width: 95%; margin-left: auto; margin-right: auto; } } - @media only screen and (min-width: 992px) { .projectReport table { table-layout: fixed; } .projectReport table th { - word-break: break-word; } } - @media only screen and (min-width: 992px) and (max-width: 1200px) { - .projectReport table th { - width: 90px; } } - @media only screen and (min-width: 992px) and (min-width: 1200px) { - .projectReport table th { - width: 120px; } } + word-break: break-word; } + @media only screen and (max-width: 1200px) { + .projectReport table th { + width: 90px; } } + @media only screen and (min-width: 1200px) { + .projectReport table th { + width: 120px; } } } /* Project update page */ .updateText { @@ -1698,20 +1661,11 @@ a.addUpdateBtn { /* Google translation bar styles */ body.translationBarActive .nav { padding-top: 39px; } - body.translationBarActive .navbar-brand { margin-top: 39px; } - -body.translationBarActive article, -body.translationBarActive .updateMain, -body.translationBarActive .organisationDetail, -body.translationBarActive #map { +body.translationBarActive article, body.translationBarActive .updateMain, body.translationBarActive .organisationDetail, body.translationBarActive #map { padding-top: 39px; } - -body.translationBarActive div.skiptranslate ~ article, -body.translationBarActive div.skiptranslate ~ .updateMain, -body.translationBarActive div.skiptranslate ~ .organisationDetail, -body.translationBarActive div.skiptranslate ~ #map { +body.translationBarActive div.skiptranslate ~ article, body.translationBarActive div.skiptranslate ~ .updateMain, body.translationBarActive div.skiptranslate ~ .organisationDetail, body.translationBarActive div.skiptranslate ~ #map { padding-top: 0px; } /* Cookie */ @@ -1737,13 +1691,10 @@ body.translationBarActive div.skiptranslate ~ #map { /* Project Admin */ .progress .progress-bar[data-completion='empty'] { background: #d9534f; } - .progress .progress-bar[data-completion='incomplete'] { background: #f0ad4e; } - .progress .progress-bar[data-completion='complete'] { background: #5cb85c; } - .progress .progress-bar .progress-percentage { display: none; } @@ -1752,31 +1703,23 @@ body.translationBarActive div.skiptranslate ~ #map { transition: background 0.1s linear; } .formProgress .panel-heading:hover { background: rgba(92, 184, 92, 0.1); } - .formProgress .progress .progress-bar .progress-percentage { display: initial; } - .formProgress .progress-and-publish { top: 20px; } - .formProgress .redLegenda { background: #d9534f; color: #d9534f; } - .formProgress .orangeLegenda { background: #f0ad4e; color: #f0ad4e; } - .formProgress .greenLegenda { background: #5cb85c; color: #5cb85c; } - .formProgress .mandatoryIndicator { color: #d9534f; } - .formProgress .published { color: #5cb85c; } - .formProgress .notPublished { color: #e31b23; } @@ -1785,7 +1728,6 @@ body.translationBarActive div.skiptranslate ~ #map { transition: background 0.1s linear; } .formOverviewInfo .panel-heading:hover { background: rgba(114, 205, 255, 0.1); } - .formOverviewInfo label { color: rgba(44, 42, 116, 0.5); } @@ -1794,7 +1736,6 @@ body.translationBarActive div.skiptranslate ~ #map { text-transform: uppercase; font-size: 1.5em; display: block; } - .projectEdit .myPanel { overflow: hidden; line-height: 25px; @@ -1805,7 +1746,7 @@ body.translationBarActive div.skiptranslate ~ #map { display: block; overflow: hidden; box-sizing: border-box; - transition: all 0.5s linear; + transition: all .5s linear; padding: 5px 15px; margin: 0 auto; -moz-border-radius: 5px; @@ -1817,10 +1758,8 @@ body.translationBarActive div.skiptranslate ~ #map { overflow: auto; background: rgba(253, 242, 232, 0); box-shadow: 0 0 15px rgba(44, 42, 116, 0.2); } - .projectEdit input[type='radio'] { display: none; } - .projectEdit .formStep { position: relative; } .projectEdit .formStep > div > label { @@ -1852,16 +1791,11 @@ body.translationBarActive div.skiptranslate ~ #map { padding-top: 10px; } .projectEdit .formStep span.tab { padding-left: 5em; } - .projectEdit .borderBottom { padding-bottom: 10px; border-bottom: 1px solid rgba(44, 42, 116, 0); } - -.projectEdit *, -.projectEdit *:before, -.projectEdit *:after { +.projectEdit *, .projectEdit *:before, .projectEdit *:after { box-sizing: border-box; } - .projectEdit .control { position: relative; padding-top: 23px; @@ -1873,21 +1807,16 @@ body.translationBarActive div.skiptranslate ~ #map { padding-left: 10px; } .projectEdit .control:last-child { border: 0; } - .projectEdit .control-label { min-width: 90%; } - .projectEdit a.btn { margin-top: 23px; } .projectEdit a.btn.btn-link { margin-top: 0; } - .projectEdit .col-md-4 a.btn.btn-link, .projectEdit .col-md-3 a.btn.btn-link { margin-top: 23px; } - .projectEdit .add-object-link { top: 8px; } - .projectEdit hr { border: none; height: 1px; @@ -1895,49 +1824,28 @@ body.translationBarActive div.skiptranslate ~ #map { /* old IE */ background-color: rgba(44, 42, 116, 0.3); /* Modern Browsers */ } - -.projectEdit input + label, -.projectEdit input + span + label, -.projectEdit input + ul + label, .projectEdit textarea + label, -.projectEdit textarea + span + label, -.projectEdit textarea + ul + label, .projectEdit select + label, -.projectEdit select + span + label, -.projectEdit select + ul + label { +.projectEdit input + label, .projectEdit input + span + label, .projectEdit input + ul + label, .projectEdit textarea + label, .projectEdit textarea + span + label, .projectEdit textarea + ul + label, .projectEdit select + label, .projectEdit select + span + label, .projectEdit select + ul + label { position: absolute; top: 0px; transition: top 0.7s ease, opacity 0.7s ease; opacity: 1; font-size: 13px; font-weight: 600; } - -.projectEdit input:focus + label, -.projectEdit input:focus + span + label, -.projectEdit input:focus + ul + label, .projectEdit textarea:focus + label, -.projectEdit textarea:focus + span + label, -.projectEdit textarea:focus + ul + label, .projectEdit select:focus + label, -.projectEdit select:focus + span + label, -.projectEdit select:focus + ul + label { +.projectEdit input:focus + label, .projectEdit input:focus + span + label, .projectEdit input:focus + ul + label, .projectEdit textarea:focus + label, .projectEdit textarea:focus + span + label, .projectEdit textarea:focus + ul + label, .projectEdit select:focus + label, .projectEdit select:focus + span + label, .projectEdit select:focus + ul + label { color: #2c2a74; } - -.projectEdit div.input-group + label, -.projectEdit div.input-group + span + label, -.projectEdit div.input-group + ul + label { +.projectEdit div.input-group + label, .projectEdit div.input-group + span + label, .projectEdit div.input-group + ul + label { position: absolute; top: 0px; transition: top 0.7s ease, opacity 0.7s ease; opacity: 1; font-size: 13px; font-weight: 600; } - .projectEdit .delete-related-object { color: #d9534f; } - .projectEdit .delete-photo-button { color: #d9534f; } - .projectEdit .delete-document { color: #d9534f; } - .projectEdit .info-icon { padding-left: 0.5em; cursor: pointer; @@ -1945,62 +1853,48 @@ body.translationBarActive div.skiptranslate ~ #map { font-size: 120%; } .projectEdit .info-icon.activated { color: rgba(44, 42, 116, 0.3); } - .projectEdit .mandatory, .projectEdit .mandatory-block { color: #d9534f; font-size: 125%; margin-left: 4px; margin-right: -4px; } - .projectEdit .mandatory-block { font-size: 90%; margin-left: 0px; margin-right: 0px; } - .projectEdit label.progress-bar[aria-valuenow="0"] { background: #d9534f; color: #d9534f; } - .projectEdit label.progress-bar[aria-valuenow="10"] { background: #d9534f; color: white; } - .projectEdit label.progress-bar[aria-valuenow="20"] { background: #d9534f; color: white; } - .projectEdit label.progress-bar[aria-valuenow="30"] { background: #d9534f; color: white; } - .projectEdit label.progress-bar[aria-valuenow="40"] { background: #f0ad4e; color: white; } - .projectEdit label.progress-bar[aria-valuenow="50"] { background: #f0ad4e; color: white; } - .projectEdit label.progress-bar[aria-valuenow="60"] { background: #f0ad4e; color: white; } - .projectEdit label.progress-bar[aria-valuenow="70"] { background: #5cb85c; color: white; } - .projectEdit label.progress-bar[aria-valuenow="80"] { background: #5cb85c; color: white; } - .projectEdit label.progress-bar[aria-valuenow="90"] { background: #5cb85c; color: white; } - .projectEdit label.progress-bar[aria-valuenow="100"] { background: #5cb85c; color: white; } - .projectEdit div.parent { background: rgba(240, 173, 78, 0.15); -webkit-border-radius: 10px; @@ -2012,7 +1906,6 @@ body.translationBarActive div.skiptranslate ~ #map { margin-top: 10px; margin-bottom: 10px; transition: background 0.1s linear; } - .projectEdit div.partial-header { font-family: 'Montserrat', "Helvetica Neue", Helvetica, Arial, sans-serif; font-weight: normal; @@ -2021,13 +1914,10 @@ body.translationBarActive div.skiptranslate ~ #map { margin-top: 10px; margin-bottom: 10px; cursor: pointer; } - .projectEdit .save-message { text-align: center; } - .projectEdit .save-button { text-align: right; } - .projectEdit .toggle-help { font-size: 0.9em; color: rgba(44, 42, 116, 0.5); } @@ -2339,8 +2229,7 @@ body.translationBarActive div.skiptranslate ~ #map { display: none; } .results .indicator-container .indicator-group .indicator { display: none; } - .results .indicator-bar-td, - .results .target-td { + .results .indicator-bar-td, .results .target-td { height: 80px; padding: 0; } .results .th-progress:before { diff --git a/akvo/rsr/views/my_rsr.py b/akvo/rsr/views/my_rsr.py index 960d2abe63..820311b798 100644 --- a/akvo/rsr/views/my_rsr.py +++ b/akvo/rsr/views/my_rsr.py @@ -7,6 +7,9 @@ see < http://www.gnu.org/licenses/agpl.html >. """ +from datetime import datetime + +from django.conf import settings from django.contrib.auth.decorators import login_required from django.contrib.auth.models import Group from django.core.exceptions import PermissionDenied @@ -402,3 +405,72 @@ def user_management(request): context['q'] = filter_query_string(qs) return render(request, 'myrsr/user_management.html', context) + + +@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 + + 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 + + # 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) + + # JSON data + indicator_updates_data = json.dumps(_get_indicator_updates_data(indicator_updates, + project.children())) + + 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, + 'update_timeout': settings.PROJECT_UPDATE_TIMEOUT, + } + + return render(request, 'myrsr/results_data.html', context) diff --git a/akvo/settings/40-pipeline.conf b/akvo/settings/40-pipeline.conf index ed856c7e68..509a1c673f 100644 --- a/akvo/settings/40-pipeline.conf +++ b/akvo/settings/40-pipeline.conf @@ -199,4 +199,10 @@ PIPELINE_JS = { ), 'output_filename': 'scripts/my-projects.min.js', }, + 'results_data': { + 'source_filenames': ( + 'scripts-src/results-data.js', + ), + 'output_filename': 'scripts/results-data.min.js', + }, } diff --git a/akvo/templates/myrsr/my_projects.html b/akvo/templates/myrsr/my_projects.html index 7173787c1c..e6d7c565c4 100644 --- a/akvo/templates/myrsr/my_projects.html +++ b/akvo/templates/myrsr/my_projects.html @@ -65,12 +65,12 @@

- View + {% trans 'View' %} {% has_perm 'rsr.change_project' user project as can_edit_project %} {% if can_edit_project %} {% trans 'Edit' %} {% endif %} - Results data + {% trans 'Results data' %} {% if project.is_published and project.is_public %} {% has_perm 'rsr.post_updates' project as can_add_update %} {% trans 'Update' %} diff --git a/akvo/templates/myrsr/myrsr_base.html b/akvo/templates/myrsr/myrsr_base.html index f574ad6d3e..f91da26f16 100644 --- a/akvo/templates/myrsr/myrsr_base.html +++ b/akvo/templates/myrsr/myrsr_base.html @@ -18,6 +18,9 @@
  • {% trans 'Project editor' %}
  • {% endif %}
  • {% trans 'My reports' %}
  • + {% if '/myrsr/results_data/' in current_path %} +
  • {% trans 'Results data' %}
  • + {% endif %} {% has_perm 'rsr.iati_management' user as can_manage_iati %} {% if can_manage_iati %}
  • {% trans "My IATI" %}
  • diff --git a/akvo/templates/myrsr/results_data.html b/akvo/templates/myrsr/results_data.html new file mode 100644 index 0000000000..2178515ebb --- /dev/null +++ b/akvo/templates/myrsr/results_data.html @@ -0,0 +1,218 @@ +{% extends "myrsr/myrsr_base.html" %} + +{% load compressed i18n rsr_utils rsr_tags rules rsr_filters humanize markup_tags %} + +{% 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 %} +
    +
    +
    +
    +{% endblock %} + +{% block js %} + {{ block.super }} + + {# Updates information #} + + + {# Default values #} + + + {# Translation strings #} + + + {# Slider library #} + + + + {% compressed_js 'results_data' %} + +{% endblock %} \ No newline at end of file diff --git a/akvo/templates/project_main_tabs/results.html b/akvo/templates/project_main_tabs/results.html index eb2cf9ce70..d5552122d7 100644 --- a/akvo/templates/project_main_tabs/results.html +++ b/akvo/templates/project_main_tabs/results.html @@ -151,7 +151,7 @@

    {% trans "Indicator periods" %}

    - + {% trans 'Add a new update' %} + + {% trans 'Add a new update' %} diff --git a/akvo/urls.py b/akvo/urls.py index 5d45b80735..90c4e59deb 100644 --- a/akvo/urls.py +++ b/akvo/urls.py @@ -120,6 +120,9 @@ url(r'^myrsr/reports/$', 'akvo.rsr.views.my_rsr.my_reports', name='my_reports'), + url(r'^myrsr/results_data/(?P\d+)/$', + 'akvo.rsr.views.my_rsr.results_data', name='results_data'), + url(r'^myrsr/user_management/$', 'akvo.rsr.views.my_rsr.user_management', name='user_management'),