\ No newline at end of file
diff --git a/app/views/phases/edit.html.erb b/app/views/phases/edit.html.erb
index c0c5859..92b5bb6 100644
--- a/app/views/phases/edit.html.erb
+++ b/app/views/phases/edit.html.erb
@@ -1,149 +1,11 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/views/plans/_progress.html.erb b/app/views/plans/_progress.html.erb
index db2542a..e95f0b9 100644
--- a/app/views/plans/_progress.html.erb
+++ b/app/views/plans/_progress.html.erb
@@ -1,9 +1,11 @@
<%
nanswers = plan.num_answered_questions()
nquestions = plan.num_questions()
+ value=(nanswers.to_f/nquestions*100).round(2)
%>
-<% answered = %(#{nanswers}/#{nquestions})%>
-
\ No newline at end of file
diff --git a/app/views/questions/_new_edit_question_textfield.html.erb b/app/views/questions/_new_edit_question_textfield.html.erb
new file mode 100644
index 0000000..b1dbd95
--- /dev/null
+++ b/app/views/questions/_new_edit_question_textfield.html.erb
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/app/views/sections/_progress.html.erb b/app/views/sections/_progress.html.erb
index 02c8d69..7eae314 100644
--- a/app/views/sections/_progress.html.erb
+++ b/app/views/sections/_progress.html.erb
@@ -3,6 +3,6 @@
<% num_section_answers = section.num_answered_questions(plan.id) %>
- <%= num_section_answers %> / <%= num_section_questions %>
+ (<%= num_section_answers %> / <%= num_section_questions %>)
\ No newline at end of file
diff --git a/lib/assets/javascripts/application.js b/lib/assets/javascripts/application.js
index a974141..0a2fc35 100644
--- a/lib/assets/javascripts/application.js
+++ b/lib/assets/javascripts/application.js
@@ -1,3 +1,4 @@
+import './views/answers/status';
import './views/contacts/new';
import './views/devise/invitations/edit';
import './views/devise/passwords/edit';
diff --git a/lib/assets/javascripts/views/answers/status.js b/lib/assets/javascripts/views/answers/status.js
index ae9b12d..55e3aa0 100644
--- a/lib/assets/javascripts/views/answers/status.js
+++ b/lib/assets/javascripts/views/answers/status.js
@@ -1,92 +1,130 @@
-$(document).ready(function(){
- /*--------------
- START Autosaving
- ----------------*/
- // debounced object holds a set of debounced functions, one for each form present in the page. Note,
- // each debounced function stored at funcs is created on demand, i.e. once the user changes any element of a form
- var debounced = (function(){
- var funcs = {};
- return {
- has: function(id){
- return funcs[id] !== undefined;
- },
- get: function(id){
+import {
+ isObject,
+ isNumber,
+ isString } from '../../utils/isType';
+import { Tinymce } from '../../utils/tinymce';
+import debounce from '../../utils/debounce';
- return funcs[id];
- },
- set: function(id, func){
- funcs[id] = dmproadmap.utils.debounce(func);
- }
- }
- })();
- // This function triggers a form submit, if and only if the answer has not been optimistically locked
- var autoSaving=function(){
- if($(this).closest('.question-form').find('.answer-locking').children().length === 0){
- $(this).closest('form.answer').submit();
- }
- };
- var listenersForEditor=function(editor){
- editor.on('change', function(){
- var notAnswered = $('#'+editor.id).closest('.question-form').find('.not-answered');
- notAnswered.hide();
- });
- editor.on('blur', function(){
- var id = $('#'+editor.id).closest('form.answer').attr('data-autosave');
- $('#'+editor.id).val(editor.getContent()); //Updates target element of tinyMCE.editor with its content
- if(!debounced.has(id)){
- debounced.set(id, autoSaving);
- }
- debounced.get(id).apply($('#'+editor.id),[id]);
- });
- editor.on('focus', function(){
- var id = $('#'+editor.id).closest('form.answer').attr('data-autosave');
- if(debounced.has(id)){
- debounced.get(id).cancel(); //Cancels the execution of its debounced function either because user transitioned from question with options
- // to the comments or because textarea lost focus and gained again before the delay being met
- }
- });
+$(() => {
+ /*
+ * Shows the closest saving-message HTML element within a question-form
+ * @param { Strin } selector - A valid CSS selector to look for
+ * @return { jQuery }
+ */
+ const showSavingMessage = selector => $(selector).closest('.question-form').find('.saving-message').show();
+ /*
+ * Hides the closest not-answered HTML element within a question-form
+ * @param { String } selector - A valid CSS selector to look for
+ * @return { jQuery }
+ */
+ const hideNotAnswered = selector => $(selector).closest('.question-form').find('.not-answered').hide();
+ /*
+ * Retrieves the question id for the closest form-answer
+ * @param { String } selector - A valid CSS selector to look for
+ * @return { String } representing the question id for a given answer, otherwise undefined
+ */
+ const questionId = selector => $(selector).closest('.form-answer').attr('data-autosave');
+ /*
+ * A map of debounced functions, one for each input, textarea or select change at any
+ * form with class form-answer. The key represents a question id and the value holds
+ * the debounced function for a given input, textarea or select. Note, this map is
+ * populated on demand, i.e. the first time a change is made at a given input, textarea
+ * or select within the form, a new key-value should be created. Succesive times, the
+ * debounced function should be retrieved instead.
+ */
+ const debounceMap = {};
+ const autoSaving = (selector) => {
+ if ($(selector).closest('.question-form').find('.answer-locking').html().length === 0) {
+ $(selector).closest('.form-answer').trigger('submit');
}
- /*--------------
- END Autosaving
- ----------------*/
- // Listener for submit event triggered
- $('.question-form').on('submit', 'form.answer', function(){
- var id = $(this).attr('data-autosave');
- if(debounced.has(id)){
- debounced.get(id).cancel(); //Cancels the execution of its debounced function, if not already, since submit() could have been trigerred through Save button
- }
- var container = $(this).closest('.question-form');
- var saving = container.find('.saving-message');
- saving.show();
+ };
+ // Initialises tinymce for any target element with class tinymce_answer
+ Tinymce.init({ selector: '.tinymce_answer' });
+ // Listeners for change, blur and focus at any target element with class tinymce_answer
+ Tinymce.findEditorsByClassName('tinymce_answer').forEach((editor) => {
+ editor.on('Change', () => {
+ hideNotAnswered(`#${editor.id}`);
});
- // Listener for changes at any element value from question-form
- $('.question-form').on('change', 'form.answer fieldset input, form.answer fieldset select', function(){
- var notAnswered = $(this).closest('.question-form').find('.not-answered');
- notAnswered.hide();
+ editor.on('Blur', () => {
+ const id = questionId(`#${editor.id}`);
+ $(`#${editor.id}`).val(editor.getContent()); // Updates target element of editor with its content
+ if (!debounceMap[id]) {
+ debounceMap[id] = debounce(autoSaving);
+ }
+ debounceMap[id]($(`#${editor.id}`));
});
- // Listener for changes at any element value from question-form. This triggers the debounced function
- $('.question-form').on('change', 'form.answer fieldset input, form.answer fieldset select', function(){
- var id = $(this).closest('form.answer').attr('data-autosave');
- if(!debounced.has(id)){
- debounced.set(id, autoSaving);
- }
- debounced.get(id).apply($(this),[id]);
+ editor.on('Focus', () => {
+ const id = questionId(`#${editor.id}`);
+ if (debounceMap[id]) {
+ /* Cancels the delayed execution of autoSaving, either because user
+ * transitioned from an option_based question to the comment or
+ * because the target element triggered blur and focus before
+ * the delayed execution of autoSaving.
+ */
+ debounceMap[id].cancel();
+ }
});
- // Init function to add listeners for every tinyMCE editor whose target element class is tinymce_answer
- (function(){
- var editors = dmproadmap.utils.tinymce.findEditorsByClassName('tinymce_answer');
- editors.forEach(listenersForEditor);
- // Initialises timeago for each element abbr with class timeago
- $('abbr.timeago').timeago();
- })();
- (function(ctx){
- // function to add listeners for a tinyMCE editor with target element id passed
- ctx.reloadEditorListeners = ctx.reloadEditorListeners || (function(id){
- var editor = dmproadmap.utils.tinymce.findEditorById(id);
- if(editor){
- listenersForEditor(editor);
- $('abbr.timeago').timeago();
+ });
+ // Listener for input or select field
+ $('.question-form').on('change', 'form.answer fieldset input, form.answer fieldset select', (e) => {
+ hideNotAnswered(e.target);
+ const id = questionId(e.target);
+ if (!debounceMap[id]) {
+ debounceMap[id] = debounce(autoSaving);
+ }
+ debounceMap[id]($(e.target));
+ });
+ // Listener for submit button
+ $('.form-answer').on('submit', (e) => {
+ e.preventDefault();
+ const id = questionId(e.target);
+ if (debounceMap[id]) {
+ // Cancels the delated execution of autoSaving
+ // (e.g. user clicks the button before the delay is met)
+ debounceMap[id].cancel();
+ }
+ showSavingMessage(e.target);
+ const formElements = $(e.target).closest('.form-answer').serializeArray();
+ const answerId = formElements.find(el => el.name === 'answer[id]');
+ if (answerId) {
+ // TODO centralise AJAX calls
+ $.ajax({
+ method: 'PUT',
+ url: `/answers/${answerId}`,
+ data: formElements,
+ }).done((data) => {
+ // Validation for the data object received
+ if (isObject(data)) {
+ if (isObject(data.question)) { // Object related to question within data received
+ if (isNumber(data.question.id)) {
+ if (isString(data.question.answer_status)) {
+ $(`#answer-status-${data.question.id}`).html(data.question.answer_status); // TODO check partial render of this view on the server
+ $('abbr.timeago').timeago();
+ }
+ if (isString(data.question.locking)) {
+ $(`#answer-locking-${data.question.id}`).html(data.question.locking);
+ }
+ if (isNumber(data.question.answer_lock_version)) {
+ $(e.target).closest('.form-answer').find('#answer_lock_version').val(data.question.answer_lock_version);
+ }
}
- });
- })(define('dmproadmap.answers.status'));
-});
\ No newline at end of file
+ }
+ if (isObject(data.plan)) { // Object related to plan within data received
+ if (isString(data.plan.progress)) {
+ $('.progress').html(data.plan.progress);
+ }
+ }
+ if (isObject(data.section)) { // Object related to section within data received
+ if (isNumber(data.section.id)) {
+ if (isString(data.section.progress)) {
+ $(`.section-progress-${data.section.id}`).html(data.section.progress);
+ }
+ }
+ }
+ }
+ }, () => {
+ // TODO adequate error handling for network error
+ });
+ }
+ });
+});
diff --git a/lib/assets/javascripts/views/phases/edit.js b/lib/assets/javascripts/views/phases/edit.js
index ffab5ac..f771160 100644
--- a/lib/assets/javascripts/views/phases/edit.js
+++ b/lib/assets/javascripts/views/phases/edit.js
@@ -1,8 +1,16 @@
+import 'bootstrap-sass/assets/javascripts/bootstrap/collapse';
import expandCollapseAll from '../../utils/expandCollapseAll';
$(() => {
// Attach handlers for the expand/collapse all accordions
expandCollapseAll();
+ $('a[data-toggle="collapse"').click((e) => {
+ if ($(e.target).hasClass('fa-plus')) {
+ $(e.target).removeClass('fa-plus').addClass('fa-minus');
+ } else {
+ $(e.target).removeClass('fa-minus').addClass('fa-plus');
+ }
+ });
});
/*
$(document).ready(function(){
diff --git a/lib/assets/stylesheets/application.scss b/lib/assets/stylesheets/application.scss
index 420a724..1df72cd 100644
--- a/lib/assets/stylesheets/application.scss
+++ b/lib/assets/stylesheets/application.scss
@@ -19,3 +19,7 @@
@import "dmproadmap/tables";
@import "dmproadmap/forms";
*/
+
+[class^="bg-"] {
+ padding: 15px;
+}
diff --git a/lib/assets/webpack.config.js b/lib/assets/webpack.config.js
index 71f1ec7..6e20570 100644
--- a/lib/assets/webpack.config.js
+++ b/lib/assets/webpack.config.js
@@ -15,7 +15,7 @@
context: __dirname,
entry: {
- vendor: ['jquery'],
+ vendor: ['jquery', 'timeago/jquery.timeago'],
application: ['./javascripts/application.js', './stylesheets/application.scss'],
},
diff --git a/test/functional/answers_controller_test.rb b/test/functional/answers_controller_test.rb
index 6ed2db9..372ed47 100644
--- a/test/functional/answers_controller_test.rb
+++ b/test/functional/answers_controller_test.rb
@@ -69,11 +69,9 @@
private
def put_answer(answer, attributes, referrer)
- put answer_path(FastGettext.locale, answer, format: "js"), attributes, {'HTTP_REFERER': referrer}
+ put answer_path(FastGettext.locale, answer, format: "json"), attributes, {'HTTP_REFERER': referrer}
assert_response :success
- assert_equal "text/javascript", @response.content_type
-
-# assert_match(/[^\$]*\$\("#answer-locking-[0-9]+"\).html\(""\);[^\$]*\$\("#answer-form-[0-9]+"\)[^\.]*.html\(".+"\);[^\$]*\$\("#answer-status-[0-9]+"\)[^.]*.html\(".+"\);[^\$]*\$.[^$]*\$.[^\$]*\$\(".progress"\).html\(".+"\);[^\$]*\$\("#section-progress-[0-9]+"\)[^.]*.html\(".+"\);/, @response.body)
+ assert_equal "application/json", @response.content_type
end
end
diff --git a/test/integration/answer_locking_test.rb b/test/integration/answer_locking_test.rb
index 0c0bb0f..6dfe674 100644
--- a/test/integration/answer_locking_test.rb
+++ b/test/integration/answer_locking_test.rb
@@ -26,9 +26,9 @@
# Signin as UserA and insert the new answer
sign_in @plan.owner
- put answer_path(FastGettext.locale, userA, format: "js"), obj_to_params(userA.attributes)
+ put answer_path(FastGettext.locale, userA, format: "json"), obj_to_params(userA.attributes)
assert_response :success
- assert_equal "text/javascript", @response.content_type
+ assert_equal "application/json", @response.content_type
updated = Answer.find_by(plan: @plan, question: @question)
assert_equal "Initial answer - by UserA", updated.text
assert_equal @plan.owner.id, updated.user_id
@@ -40,9 +40,9 @@
# Signin as UserB and try to insert the new answer but fail
sign_in @collaborator
- put answer_path(FastGettext.locale, userB, format: "js"), obj_to_params(userB.attributes)
+ put answer_path(FastGettext.locale, userB, format: "json"), obj_to_params(userB.attributes)
assert_response :success
- assert_equal "text/javascript", @response.content_type
+ assert_equal "application/json", @response.content_type
updated = Answer.find_by(plan: @plan, question: @question)
assert_equal "Initial answer - by UserA", updated.text
assert_equal @plan.owner.id, updated.user_id
@@ -63,9 +63,9 @@
sign_in @plan.owner
userA['text'] += " - Updated by userA"
- put answer_path(FastGettext.locale, userA['id'], format: "js"), obj_to_params(userA)
+ put answer_path(FastGettext.locale, userA['id'], format: "json"), obj_to_params(userA)
assert_response :success
- assert_equal "text/javascript", @response.content_type
+ assert_equal "application/json", @response.content_type
updated = Answer.find_by(plan: @plan, question: @question)
assert_equal "Initial answer - by UserA - Updated by userA", updated.text
assert_equal @plan.owner.id, updated.user_id
@@ -79,9 +79,9 @@
sign_in @collaborator
userB['text'] += " - Updated by userB"
- put answer_path(FastGettext.locale, userB['id'], format: "js"), obj_to_params(userB)
+ put answer_path(FastGettext.locale, userB['id'], format: "json"), obj_to_params(userB)
assert_response :success
- assert_equal "text/javascript", @response.content_type
+ assert_equal "application/json", @response.content_type
updated = Answer.find_by(plan: @plan, question: @question)
assert_equal "Initial answer - by UserA - Updated by userA", updated.text
assert_equal @plan.owner.id, updated.user_id