diff --git a/app/controllers/answers_controller.rb b/app/controllers/answers_controller.rb index a657eb5..9c9f70e 100644 --- a/app/controllers/answers_controller.rb +++ b/app/controllers/answers_controller.rb @@ -19,6 +19,7 @@ { msg: _('There is no plan with id %{id} for which to create or update an answer') %{ :id => p_params[:plan_id] }}) return end + q = Question.find(p_params[:question_id]) Answer.transaction do begin @@ -28,10 +29,17 @@ if p_params[:question_option_ids].present? @answer.touch() # Saves the record with the updated_at set to the current time. Needed if only answer.question_options is updated end + if q.question_format.rda_metadata? + @answer.update_answer_hash(JSON.parse(params[:standards]), p_params[:text]) + @answer.save! + end rescue ActiveRecord::RecordNotFound @answer = Answer.new(p_params.merge({ user_id: current_user.id })) @answer.lock_version = 1 authorize @answer + if q.question_format.rda_metadata? + @answer.update_answer_hash(JSON.parse(params[:standards]), p_params[:text]) + end @answer.save! rescue ActiveRecord::StaleObjectError @stale_answer = @answer diff --git a/app/controllers/question_formats_controller.rb b/app/controllers/question_formats_controller.rb new file mode 100644 index 0000000..ced3624 --- /dev/null +++ b/app/controllers/question_formats_controller.rb @@ -0,0 +1,10 @@ +class QuestionFormatsController < ApplicationController + # do we need authorizaton on this? it will only return the URL for the rda api + # down the line we will add more methods for other external api's + def rda_api_address + render json: { + 'url' => QuestionFormat.rda_metadata.first.description + }.to_json + end + +end diff --git a/app/models/answer.rb b/app/models/answer.rb index c9e3d6c..a2a625f 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -10,9 +10,9 @@ else plan.touch # Force updated_at changes if nothing changed since save only saves if changes were made to the record end - end + end end - + ## # Associations belongs_to :question @@ -63,7 +63,7 @@ answer_copy.save! return answer_copy end - + # This method helps to decide if an answer option (:radiobuttons, :checkbox, etc ) in form views should be checked or not # Returns true if the given option_id is present in question_options, otherwise returns false def has_question_option(option_id) @@ -86,4 +86,37 @@ def non_archived_notes return notes.select{ |n| n.archived.blank? }.sort!{ |x,y| y.updated_at <=> x.updated_at } end + + + ## + # Returns the parsed JSON hash for the current answer object + # Generates a new hash if none exists for rda_questions + # + # @return [Hash] the parsed hash of the answer. + # Should have keys 'standards', 'text' + # 'standards' is a list of : pairs + # 'text' is the text from the comments box + def answer_hash + default = {'standards' => {}, 'text' => ''} + begin + h = self.text.nil? ? default : JSON.parse(self.text) + rescue JSON::ParserError => e + h = default + end + return h + end + + ## + # Given a hash of standards and a comment value, this updates answer + # text for rda_questions + # + # @param [standards] a hash of standards + # @param [text] option comment text + # nothing returned, but the status of the text field of the answer is changed + def update_answer_hash(standards={},text="") + h = {} + h['standards'] = standards + h['text'] = text + self.text = h.to_json + end end diff --git a/app/models/concerns/exportable_plan.rb b/app/models/concerns/exportable_plan.rb index fe34e50..c55aa2f 100644 --- a/app/models/concerns/exportable_plan.rb +++ b/app/models/concerns/exportable_plan.rb @@ -1,15 +1,15 @@ module ExportablePlan extend ActiveSupport::Concern - + included do - + def as_pdf(coversheet = false) prepare(coversheet) end - + def as_csv(headings = true, unanswered = true) hash = prepare(false) - + CSV.generate do |csv| hdrs = (hash[:phases].length > 1 ? [_('Phase')] : []) if headings @@ -17,7 +17,7 @@ else csv << _('Answer') end - + csv << hdrs.flatten hash[:phases].each do |phase| phase[:sections].each do |section| @@ -31,24 +31,24 @@ else flds << [ sanitize_text(answer_text) ] end - + csv << flds.flatten end end end end end - - private + + private def prepare(coversheet = false) hash = coversheet ? prepare_coversheet : {} template = Template.includes(phases: { sections: {questions: :question_format } }). joins(phases: { sections: { questions: :question_format } }). where(id: self.template_id).first - + hash[:title] = self.title hash[:answers] = self.answers - + # add the relevant questions/answers phases = [] template.phases.each do |phase| @@ -65,19 +65,19 @@ else txt << question.text end - sctn[:questions] << { id: question.id, text: txt } + sctn[:questions] << { id: question.id, text: txt, format: question.question_format } end phs[:sections] << sctn end phases << phs end hash[:phases] = phases - + record_plan_export(:pdf) - + hash end - + def prepare_coversheet hash = {} # name of owner and any co-owners @@ -86,10 +86,10 @@ attribution << role.user.name(false) end hash[:attribution] = attribution - + # Org name of plan owner's org hash[:affiliation] = self.owner.present? ? self.owner.org.name : '' - + # set the funder name hash[:funder] = self.funder_name.present? ? self.funder_name : (self.template.org.present? ? self.template.org.name : '') @@ -104,7 +104,7 @@ hash[:customizer] = customizer hash end - + def record_plan_export(format) exported_plan = ExportedPlan.new.tap do |ep| ep.plan = self @@ -118,9 +118,9 @@ end exported_plan.save end - + def sanitize_text(text) if (!text.nil?) then ActionView::Base.full_sanitizer.sanitize(text.gsub(/ /i,"")) end end end -end \ No newline at end of file +end diff --git a/app/models/exported_plan.rb b/app/models/exported_plan.rb index bc42e4b..bb79837 100644 --- a/app/models/exported_plan.rb +++ b/app/models/exported_plan.rb @@ -11,11 +11,11 @@ VALID_FORMATS = ['csv', 'html', 'pdf', 'text', 'docx'] - validates :format, inclusion: { + validates :format, inclusion: { in: VALID_FORMATS, - message: -> (object, data) do - _('%{value} is not a valid format') % { :value => data[:value] } - end + message: -> (object, data) do + _('%{value} is not a valid format') % { :value => data[:value] } + end } validates :plan, :format, presence: {message: _("can't be blank")} diff --git a/app/models/question_format.rb b/app/models/question_format.rb index b7d6afc..b6d1e36 100644 --- a/app/models/question_format.rb +++ b/app/models/question_format.rb @@ -4,11 +4,11 @@ # Associations has_many :questions - enum formattype: [ :textarea, :textfield, :radiobuttons, :checkbox, :dropdown, :multiselectbox, :date ] + enum formattype: [ :textarea, :textfield, :radiobuttons, :checkbox, :dropdown, :multiselectbox, :date, :rda_metadata ] attr_accessible :formattype - + validates :title, presence: {message: _("can't be blank")}, uniqueness: {message: _("must be unique")} - + ## # Possibly needed for active_admin # -relies on protected_attributes gem as syntax depricated in rails 4.2 @@ -16,7 +16,7 @@ # Retrieves the id for a given formattype passed scope :id_for, -> (formattype) { where(formattype: formattype).pluck(:id).first } - + ## # Define Bit Field Values so we can test a format without doing string comps # Column type diff --git a/app/views/answers/_new_edit.html.erb b/app/views/answers/_new_edit.html.erb index caccfc1..ca97561 100644 --- a/app/views/answers/_new_edit.html.erb +++ b/app/views/answers/_new_edit.html.erb @@ -2,14 +2,59 @@ <!-- This partial creates a form for each type of question. The local variables are: plan, answer, question, readonly --> +<!-- Question text --> +<p> + <strong><%= raw question.text %></strong> +</p> +<% q_format = question.question_format %> +<% if q_format.rda_metadata? %> + <% answer_hash = answer.answer_hash %> + <div class="rda_metadata"><button class="remove-standard" style="display:none;"></button> + <div class="selected_standards"><strong><%=_("Your Selected Standards:")%></strong></br><ul class="list bullet"></ul></div> + <div class="rda_right" style="float:right;width:50%;margin-bottom:5px;display:none;"> + OR Search:</br> + <input type="text" data-provide="typeahead" class="standards-typeahead"></input></br> + <button class="btn btn-primary select_standard_typeahead"><%=_("Add Standard")%></button> + </div> + <div class="subject"><%=_("Please select a subject")%></br> + <select name="subject" class="form-control"></select> + </div> + <div class="sub-subject"><%=_("Please select a sub-subject")%></br> + <select name="sub-subject" class="form-control"></select> + </div> + </br> + <div class="suggested-answer-div"> + <span class="suggested-answer-intro"> + <strong><%=_("Browse Standards") %></strong> + </span> + + <div class="browse-standards-border"> + <p class="suggested-answer"> + <strong><%=_("Please wait, Standards are loading")%></strong> + </p> + </div> + <div> + <a href="#" class="custom-standard"><strong>Standard not listed? Add your own.</strong></a> + <div class="add-custom-standard" style="display:none;"> + <input type="text" class="custom-standard-name"></input> + <button class="btn btn-primary submit_custom_standard">Add Standard</button> + </div> + </div> + </div> + </div> +<% end %> + <%= form_for answer, url: {controller: :answers, action: :create_or_update}, html: {method: :post, 'data-autosave': question.id, class: 'form-answer' } do |f| %> <% if !readonly %> <%= f.hidden_field :plan_id %> <%= f.hidden_field :question_id %> <%= f.hidden_field :lock_version %> + <% if q_format.rda_metadata? %> + <%= hidden_field_tag :standards, answer_hash['standards'].to_json %> + <% end %> <% end %> <fieldset <%= 'disabled' if readonly %>> - <% if question.option_based? %> + <% if question.option_based? || question.question_format.rda_metadata? %> <%= render(partial: 'questions/new_edit_question_option_based', locals: { f: f, question: question, answer: answer }) %> <% elsif question.question_format.textfield?%> <%= render(partial: 'questions/new_edit_question_textfield', locals: { f: f, question: question, answer: answer }) %> @@ -30,4 +75,4 @@ </div> </div> <% end %> -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/questions/_add_question.html.erb b/app/views/questions/_add_question.html.erb index 72045fa..5a9c3f1 100644 --- a/app/views/questions/_add_question.html.erb +++ b/app/views/questions/_add_question.html.erb @@ -31,7 +31,7 @@ :id, :title, QuestionFormat.id_for(QuestionFormat.formattypes[:textarea])), - {}, + {}, class: "form-control", 'data-toggle': 'tooltip', 'data-html': true, @@ -44,13 +44,13 @@ <div class="col-md-offset-2"> <%= render "/question_options/option_fields", f: f, q: @new_question %> <!--display for selecting comment box when multiple choice is being used--> - <div class="checkbox"> - <label> - <%= f.check_box :option_comment_display, as: :check_boxes %><%= _('Display additional comment area.') %> - </label> - </div> </div> </div> + <div class="form-group col-md-10" data-attribute="option_comment" style="display:none;"> + <label> + <%= f.check_box :option_comment_display, as: :check_boxes %><%= _('Display additional comment area.') %> + </label> + </div> <!--Question default_value --> <div class="form-group col-md-10" data-attribute="default_value"> <%= f.label(:default_value, _('Default answer'), class: "control-label") %> @@ -90,4 +90,3 @@ </div> </div> <% end %> - diff --git a/app/views/questions/_edit_question.html.erb b/app/views/questions/_edit_question.html.erb index 7d354f9..3de20c4 100644 --- a/app/views/questions/_edit_question.html.erb +++ b/app/views/questions/_edit_question.html.erb @@ -30,7 +30,7 @@ :id, :title, question.question_format_id), - {}, + {}, class: "form-control", 'data-toggle': 'tooltip', 'data-html': true, @@ -42,13 +42,14 @@ <div class="col-md-offset-2"> <%= render "/question_options/option_fields", f: f, q: question %> <!--display for selecting comment box when multiple choice is being used--> - <div class="checkbox"> - <label> - <%= f.check_box :option_comment_display, as: :check_boxes %><%= _('Display additional comment area.') %> - </label> - </div> </div> </div> + <% comment_disp = question.question_format.option_based? || question.question_format.rda_metadata? %> + <div class="form-group col-md-10" data-attribute="option_comment" style="<%=comment_disp ? '' : 'display:none;'%>"> + <label> + <%= f.check_box :option_comment_display, as: :check_boxes %><%= _('Display additional comment area.') %> + </label> + </div> <!--Question default_value --> <div class="form-group col-md-10" data-attribute="default_value" style="<%= question.question_format.textfield? || question.question_format.textarea? ? '' : 'display: none;' %>"> <%= f.label(:default_value, _('Default answer'), class: "control-label") %> @@ -87,7 +88,7 @@ </div> <!-- Themes --> <div class="form-group col-md-10"> - <%= render partial: 'org_admin/shared/theme_selector', + <%= render partial: 'org_admin/shared/theme_selector', locals: { f: f, all_themes: Theme.all.order("title"), popover_message: _('Select one or more themes that are relevant to this question. This will allow similarly themed organisation-level guidance to appear alongside your question.') } %> </div> diff --git a/app/views/questions/_new_edit_question_option_based.html.erb b/app/views/questions/_new_edit_question_option_based.html.erb index b418360..171bf4d 100644 --- a/app/views/questions/_new_edit_question_option_based.html.erb +++ b/app/views/questions/_new_edit_question_option_based.html.erb @@ -21,12 +21,12 @@ </div> <% end %> <% elsif question.question_format.dropdown? || question.question_format.multiselectbox? %> - <% + <% options_html = "" options.each do |op| options_html += answer.has_question_option(op.id) ? "<option value=#{op.id} selected=\"selected\">#{op.text}</option>" : - "<option value=#{op.id}>#{op.text}</option>" + "<option value=#{op.id}>#{op.text}</option>" end %> <%= f.label(:question_option_ids, raw(question.text), class: 'control-label') %> @@ -38,7 +38,8 @@ <% end %> <div class="form-group"> <% if question.option_comment_display %><!-- Comment text area for option_based questions --> + <% text = question.question_format.rda_metadata? ? answer.answer_hash['text'] : answer.text %> <%= label_tag('answer[text]', _('Additional Information'), class: 'control-label') %> - <%= text_area_tag('answer[text]', answer.text, id: "answer-text-#{question.id}", class: "form-control tinymce_answer") %> + <%= text_area_tag('answer[text]', text, id: "answer-text-#{question.id}", class: "form-control tinymce_answer") %> <% end %> </div> diff --git a/app/views/shared/export/_plan.erb b/app/views/shared/export/_plan.erb index e62522f..f9fdf65 100644 --- a/app/views/shared/export/_plan.erb +++ b/app/views/shared/export/_plan.erb @@ -9,7 +9,7 @@ <% if @show_coversheet %> <%= render partial: '/shared/export/plan_coversheet' %> <% end %> - + <% @hash[:phases].each do |phase| %> <div style="page-break-before:always;"></div> <!-- Page break before each phase --> <!-- If there is more than one phase show the plan title and phase title --> @@ -41,15 +41,27 @@ <% answer = @plan.answer(question[:id], false) %> <% blank = (answer.present? && answer.is_valid?) ? answer.text.gsub(/<\/?p>/, '').gsub(/<br\s?\/?>/, '').chomp.blank? : true %> - <% if blank && @show_unanswered %> + <% options = answer.present? ? answer.question_options : [] %> + <% if blank && @show_unanswered && options.blank? %> <p><%= _('Question not answered.') -%></p> <% elsif !blank %> - <% if answer.question_options.length > 0 %> + <% if options.present?%> <ul> - <% answer.question_options.each do |opt| %> + <% options.each do |opt| %> <li><%= opt.text %></li> <% end %> </ul> + <% end %> + <% if question[:format].rda_metadata? %> + <% ah = answer.answer_hash %> + <% if ah['standards'].present? %> + <ul> + <% ah['standards'].each do |id, title| %> + <li><%= title %></li> + <% end %> + </ul> + <% end %> + <p><%= raw ah['text'] %></p> <% else %> <p><%= raw answer.text %></p> <% end %> @@ -59,4 +71,4 @@ <% end %> <!-- sections.each --> <% end %> </body> -</html> \ No newline at end of file +</html> diff --git a/config/branding_example.yml b/config/branding_example.yml index dfd82d3..454bd9d 100644 --- a/config/branding_example.yml +++ b/config/branding_example.yml @@ -48,7 +48,7 @@ admin_privileges: true added_as_coowner: true feedback_requested: true - feedback_provided: true + feedback_provided: true owners_and_coowners: visibility_changed: true admins: diff --git a/config/routes.rb b/config/routes.rb index ee376eb..4de6205 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -172,6 +172,9 @@ post 'create_or_update', on: :collection end + # Question Formats controller, currently just the one action + get 'question_formats/rda_api_address' => 'question_formats#rda_api_address' + resources :notes, only: [:create, :update, :archive] do member do patch 'archive' @@ -302,7 +305,7 @@ get 'unpublish', action: :unpublish, constraints: {format: [:json]} end end - + get 'template_options' => 'templates#template_options', constraints: {format: [:json]} get 'download_plans' => 'plans#download_plans' end diff --git a/db/schema.rb b/db/schema.rb index 2ca0b37..bb5acc9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -425,15 +425,15 @@ add_index "user_identifiers", ["user_id"], name: "index_user_identifiers_on_user_id" create_table "users", force: :cascade do |t| - t.string "firstname", limit: 255 - t.string "surname", limit: 255 - t.string "email", limit: 255, default: "", null: false - t.string "orcid_id", limit: 255 - t.string "shibboleth_id", limit: 255 - t.datetime "created_at" - t.datetime "updated_at" - t.string "encrypted_password", limit: 255, default: "" - t.string "reset_password_token", limit: 255 + t.string "firstname" + t.string "surname" + t.string "email", default: "", null: false + t.string "orcid_id" + t.string "shibboleth_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "encrypted_password", default: "" + t.string "reset_password_token" t.datetime "reset_password_sent_at" t.datetime "remember_created_at" t.integer "sign_in_count", limit: 4, default: 0 diff --git a/lib/assets/javascripts/application.js b/lib/assets/javascripts/application.js index 9671418..f0c99af 100644 --- a/lib/assets/javascripts/application.js +++ b/lib/assets/javascripts/application.js @@ -8,6 +8,7 @@ // Page specific JS import './views/answers/edit'; +import './views/answers/rda_metadata'; import './views/annotations/add'; import './views/annotations/edit'; import './views/contacts/new'; diff --git a/lib/assets/javascripts/views/answers/rda_metadata.js b/lib/assets/javascripts/views/answers/rda_metadata.js new file mode 100644 index 0000000..f0f6a71 --- /dev/null +++ b/lib/assets/javascripts/views/answers/rda_metadata.js @@ -0,0 +1,406 @@ +import { isUndefined, isObject } from '../../utils/isType'; + +$(() => { + // url for the api we will be querying + let url = ''; + + // key/value lookup for standards + const descriptions = {}; + // cleaned up structure of the API results + const minTree = {}; + // keeps track of how many waiting api-requests still need to run + let noWaiting = 0; + + + // prune the min_tree where there are no standards + // opporates on the principal that no two subjects have the same name + function removeUnused(name) { + const num = Object.keys(minTree).find(x => minTree[x].name === name); + // if not top level standard + if (isUndefined(num)) { + // for each top level standard + Object.keys(minTree).forEach((knum) => { + const child = Object.keys(minTree[knum].children).find(x => + minTree[knum].children[x].name === name); + if (isObject(child)) { + delete minTree[knum].children[child]; + $(`.rda_metadata .sub-subject select option[value="${name}"]`).remove(); + } + }); + } else { + delete minTree[num]; + // remove min_tree[num] from top-level dropdowns + $(`.rda_metadata .subject select option[value="${name}"]`).remove(); + } + } + + + function getDescription(id) { + $.ajax({ + url: url + id.slice(4), + type: 'GET', + crossDomain: true, + dataType: 'json', + }).done((results) => { + descriptions[id] = {}; + descriptions[id].title = results.title; + descriptions[id].description = results.description; + noWaiting -= 1; + }); + } + + + // init descriptions lookup table based on passed ids + function initDescriptions(ids) { + ids.forEach((id) => { + if (!(id in descriptions)) { + noWaiting += 1; + getDescription(id); + } + }); + } + + // takes in a subset of the min_tree which has name and standards properties + // initializes the standards property to the result of an AJAX POST + function getStandards(name, num, child) { + // slice -4 from url to remove '/api/' + noWaiting += 1; + $.ajax({ + url: `${url.slice(0, -4)}query/schemes`, + type: 'POST', + crossDomain: true, + data: `keyword=${name}`, + dataType: 'json', + }).done((result) => { + if (isUndefined(child)) { + minTree[num].standards = result.ids; + } else { + minTree[num].children[child].standards = result.ids; + } + if (result.ids.length < 1) { + removeUnused(name); + } + noWaiting -= 1; + initDescriptions(result.ids); + }); + } + + // clean up the data initially returned from the API + function cleanTree(apiTree) { + // iterate over api_tree + Object.keys(apiTree).forEach((num) => { + minTree[num] = {}; + minTree[num].name = apiTree[num].name; + minTree[num].children = []; + if (apiTree[num].children !== undefined) { + Object.keys(apiTree[num].children).forEach((child) => { + minTree[num].children[child] = {}; + minTree[num].children[child].name = apiTree[num].children[child].name; + minTree[num].children[child].standards = []; + getStandards(minTree[num].children[child].name, num, child); + }); + } + // init a standards on top level + minTree[num].standards = []; + getStandards(minTree[num].name, num, undefined); + }); + } + + + // create object for typeahead + function initTypeahead() { + const data = []; + const simpdat = []; + Object.keys(descriptions).forEach((id) => { + data.push({ value: descriptions[id].title, id }); + simpdat.push(descriptions[id].title); + }); + const typ = $('.standards-typeahead'); + typ.typeahead({ source: simpdat }); + } + + function initStandards() { + // for each metadata question, init selected standards according to html + $('.rda_metadata').each(function () { + // list of selected standards + const selectedStandards = $(this).find('.selected_standards .list'); + // form listing of standards + const formStandards = $(this).next('form').find('#standards'); + // need to pull in the value from frm_stds + const standardsArray = JSON.parse(formStandards.val()); + // init the data value + formStandards.data('standard', standardsArray); + Object.keys(standardsArray).forEach((key) => { + // add the standard to list + if (key === standardsArray[key]) { + selectedStandards.append(`<li class="${key}">${key}<button class="remove-standard"><i class="fa fa-times-circle"></i></button></li`); + } else { + selectedStandards.append(`<li class="${key}">${descriptions[key].title}<button class="remove-standard"><i class="fa fa-times-circle"></i></button></li>`); + } + }); + }); + } + + function waitAndUpdate() { + if (noWaiting > 0) { + // if we are waiting on api responces, call this function in 1 seccond + setTimeout(waitAndUpdate, 1000); + } else { + // update all the dropdowns/ standards explore box (calling on subject + // will suffice since it will necisarily update sub-subject) + $('.rda_metadata .subject select').change(); + initStandards(); + initTypeahead(); + } + } + + // given a subject name, returns the portion of the min_tree applicable + function getSubject(subjectText) { + const num = Object.keys(minTree).find(x => minTree[x].name === subjectText); + return minTree[num]; + } + + // given a subsubject name and an array of children, data, return the + // applicable child + function getSubSubject(subsubjectText, data) { + const child = Object.keys(data).find(x => data[x].name === subsubjectText); + return data[child]; + } + + function updateSaveStatus(group) { + // update save/autosave status + group.next('form').find('fieldset input').change(); + } + + // change sub-subjects and standards based on selected subject + $('.rda_metadata').on('change', '.subject select', (e) => { + const target = $(e.currentTarget); + const group = target.closest('.rda_metadata'); + const subSubject = group.find('.sub-subject select'); + const subjectText = target.find(':selected').text(); + // find subject in min_tree + const subject = getSubject(subjectText); + // check to see if this object has no children(and thus it's own standards) + if (subject.children.length === 0) { + // hide sub-subject since there's no data for it + subSubject.closest('div').hide(); + // update the standards display selector + $('.rda_metadata .sub-subject select').change(); + } else { + // show the sub-subject incase it was previously hidden + subSubject.closest('div').show(); + // update the sub-subject display selector + subSubject.find('option').remove(); + subject.children.forEach((child) => { + $('<option />', { value: child.name, text: child.name }).appendTo(subSubject); + }); + // once we have updated the sub-standards, ensure the standards displayed + // get updated as well + $('.rda_metadata .sub-subject select').change(); + } + }); + + // change standards based on selected sub-subject + $('.rda_metadata').on('change', '.sub-subject select', (e) => { + const target = $(e.currentTarget); + const group = target.closest('.rda_metadata'); + const subject = group.find('.subject select'); + const subSubject = group.find('.sub-subject select'); + const subjectText = subject.find(':selected').text(); + const subjectData = getSubject(subjectText); + const standards = group.find('.browse-standards-border'); + let standardsData; + if (subjectData.children.length === 0) { + // update based on subject's standards + standardsData = subjectData.standards; + } else { + // update based on sub-subject's standards + const subsubjectText = subSubject.find(':selected').text(); + standardsData = getSubSubject(subsubjectText, subjectData.children).standards; + } + // clear list of standards + standards.empty(); + // update list of standards + Object.keys(standardsData).forEach((num) => { + const standard = descriptions[standardsData[num]]; + standards.append(`<div style="background-color:#EAEAEA;border-radius:3px"><strong>${standard.title}</strong><div style="float:right"><button class="btn btn-primary select_standard" data-standard="${standardsData[num]}">Add Standard</button></br></div><p>${standard.description}</p></div>`); + }); + }); + + // when 'Add Standard' button next to the search is clicked, we need to add + // this to the user's selected list of standards. + // update the data and val of hidden standards field in form + $('.rda_metadata').on('click', '.select_standard_typeahead', (e) => { + const target = $(e.currentTarget); + const group = target.closest('.rda_metadata'); + const selected = group.find('ul.typeahead li.active'); + const selectedStandards = group.find('.selected_standards .list'); + // the title of the standard + const standardTitle = selected.data('value'); + // need to find the standard + let standard; + Object.keys(descriptions).forEach((standardId) => { + if (descriptions[standardId].title === standardTitle) { + standard = standardId; + } + }); + selectedStandards.append(`<li class="${standard}">${descriptions[standard].title}<button class="remove-standard"><i class="fa fa-times-circle"></i></button></li>`); + const formStandards = group.next('form').find('#standards'); + // get the data for selected standards from the data attribute 'standard' + // of the hidden field #standards within the answer form + let frmStdsDat = formStandards.data('standard'); + // need to init data object for first time + if (typeof frmStdsDat === 'undefined') { + frmStdsDat = {}; + } + // init the key to standard id and value to standard. + // NOTE: is there any point in storing the title or description here? + // storing the title could make export easier as we wolnt need to query api + // but queries to the api would be 1 per-standard if we dont store these + frmStdsDat[standard] = descriptions[standard].title; + // update data value + formStandards.data('standard', frmStdsDat); + // update input value + formStandards.val(JSON.stringify(frmStdsDat)); + updateSaveStatus(group); + }); + + // when a 'Add standard' button is clicked, we need to add this to the user's + // selected list of standards + // update the data and val of hidden standards field in form + $('.rda_metadata').on('click', '.select_standard', (e) => { + const target = $(e.currentTarget); + const group = target.closest('.rda_metadata'); + const selectedStandards = group.find('.selected_standards .list'); + // the identifier for the standard which was selected + const standard = target.data('standard'); + // append the standard to the displayed list of selected standards + selectedStandards.append(`<li class="${standard}">${descriptions[standard].title}<button class="remove-standard"><i class="fa fa-times-circle"></i></button></li>`); + const formStandards = group.next('form').find('#standards'); + // get the data for selected standards from the data attribute 'standard' + // of the hidden field #standards within the answer form + let frmStdsDat = formStandards.data('standard'); + // need to init data object for first time + if (isUndefined(frmStdsDat)) { + frmStdsDat = {}; + } + // init the key to standard id and value to standard. + frmStdsDat[standard] = descriptions[standard].title; + // update data value + formStandards.data('standard', frmStdsDat); + // update input value + formStandards.val(JSON.stringify(frmStdsDat)); + updateSaveStatus(group); + }); + + // when a 'Remove Standard' button is clicked, we need to remove this from the + // user's selected list of standards. Additionally, we need to remove the + // standard from the data/val fields of standards in hidden form + $('.rda_metadata').on('click', '.remove-standard', (e) => { + const target = $(e.currentTarget); + const group = target.closest('.rda_metadata'); + const listedStandard = target.closest('li'); + const standardId = listedStandard.attr('class'); + // remove the standard from the list + listedStandard.remove(); + // update the data for the form + const formStandards = group.next('form').find('#standards'); + const frmStdsDat = formStandards.data('standard'); + delete frmStdsDat[standardId]; + // update data value + formStandards.data('standard', frmStdsDat); + // update input value + formStandards.val(JSON.stringify(frmStdsDat)); + updateSaveStatus(group); + }); + + // show the add custom standard div when standard not listed clicked + $('.rda_metadata').on('click', '.custom-standard', (e) => { + e.preventDefault(); + const target = $(e.currentTarget); + const group = target.closest('.rda_metadata'); + const addStandardDiv = $(group.find('.add-custom-standard')); + addStandardDiv.show(); + }); + + // when this button is clicked, we add the typed standard to the list of + // selected standards + $('.rda_metadata').on('click', '.submit_custom_standard', (e) => { + e.preventDefault(); + const target = $(e.currentTarget); + const group = target.closest('.rda_metadata'); + const selectedStandards = group.find('.selected_standards .list'); + const standardName = group.find('.custom-standard-name').val(); + selectedStandards.append(`<li class="${standardName}">${standardName}<button class="remove-standard"><i class="fa fa-times-circle"></i></button></li>`); + const formStandards = group.next('form').find('#standards'); + // get the data for selected standards from the data attribute 'standard' + // of the hidden field #standards within the answer form + let frmStdsDat = formStandards.data('standard'); + // need to init data object for first time + if (typeof frmStdsDat === 'undefined') { + frmStdsDat = {}; + } + // init the key to standard id and value to standard. + frmStdsDat[standardName] = standardName; + // update data value + formStandards.data('standard', frmStdsDat); + // update input value + formStandards.val(JSON.stringify(frmStdsDat)); + updateSaveStatus(group); + }); + + + function initMetadataQuestions() { + // find all elements with rda_metadata div + $('.rda_metadata').each((idx, el) => { + // $(this) is the element + const sub = $(el).find('.subject select'); + // var sub_subject = $(this).find(".sub-subject select"); + Object.keys(minTree).forEach((num) => { + $('<option />', { value: minTree[num].name, text: minTree[num].name }).appendTo(sub); + }); + }); + waitAndUpdate();// $(".rda_metadata .subject select").change(); + } + + + // callback from url+subject-index + // define api_tree and call to initMetadataQuestions + function subjectCallback(data) { + // remove unused standards/substandards + cleanTree(data); + // initialize the dropdowns/selected standards for the page + initMetadataQuestions(); + } + + // callback from get request to rda_api_address + // define url and make a call to url+subject-index + function urlCallback(data) { + // init url + url = data.url; + // get api_tree structure from api + $.ajax({ + url: `${url}subject-index`, + type: 'GET', + crossDomain: true, + dataType: 'json', + }).done((result) => { + subjectCallback(result); + }); + } + + // get the url we will be using for the api + // only do this if page has an rda_metadata div + if ($('.rda_metadata').length) { + $.getJSON('/question_formats/rda_api_address', urlCallback); + } + // when the autosave or save action occurs, this clears out both the list of + // selected standards, and the selectors for new standards, as it re-renders + // the partial. This "autosave" event is triggered by that JS in order to + // allow us to know when the save has happened and re-init the question + $('.rda_metadata').on('autosave', (e) => { + e.preventDefault(); + // re-initialize the metadata question + initMetadataQuestions(); + }); +}); diff --git a/lib/assets/javascripts/views/questions/sharedEventHandlers.js b/lib/assets/javascripts/views/questions/sharedEventHandlers.js index e29cfea..484dece 100644 --- a/lib/assets/javascripts/views/questions/sharedEventHandlers.js +++ b/lib/assets/javascripts/views/questions/sharedEventHandlers.js @@ -3,15 +3,18 @@ const selected = source.value; const defaultValue = $(source).closest('form').find('[data-attribute="default_value"]'); const questionOptions = $(source).closest('form').find('[data-attribute="question_options"]'); + const opComment = $(source).closest('form').find('[data-attribute="option_comment"]'); switch (selected) { case '1': questionOptions.hide(); + opComment.hide(); defaultValue.show(); defaultValue.find('[data-attribute="textfield"]').hide(); defaultValue.find('[data-attribute="textarea"]').show(); break; case '2': questionOptions.hide(); + opComment.hide(); defaultValue.show(); defaultValue.find('[data-attribute="textarea"]').hide(); defaultValue.find('[data-attribute="textfield"]').show(); @@ -22,6 +25,12 @@ case '6': defaultValue.hide(); questionOptions.show(); + opComment.show(); + break; + case '7': + defaultValue.hide(); + questionOptions.hide(); + opComment.show(); break; default : break; @@ -29,4 +38,3 @@ }; export { onChangeQuestionFormat as default }; - diff --git a/lib/assets/stylesheets/overrides.scss b/lib/assets/stylesheets/overrides.scss index cce87cb..d8a3e33 100644 --- a/lib/assets/stylesheets/overrides.scss +++ b/lib/assets/stylesheets/overrides.scss @@ -164,19 +164,19 @@ margin-right: 25px; } -/* Overrides ul list */ +/* Overrides ul list */ #modal-permissions ul { list-style-type: none; } -/* Overrides list-group */ +/* Overrides list-group */ #modal-permissions .list-group li:nth-child(1){ border-top: 0 none; } -/* Overrides list-group-item */ +/* Overrides list-group-item */ #modal-permissions .list-group-item { - height: 30px; + height: 30px; padding: 5px 15px; } @@ -383,7 +383,7 @@ .tooltip-inner { min-width: 320px; - max-width: 100%; + max-width: 100%; text-align: left; } @@ -493,3 +493,25 @@ .input-group-addon { border-radius: 0px; } + +/* rda styling */ +.browse-standards-border { + border: 1px solid $grey; + -webkit-border-radius: 0px; + -moz-border-radius: 0px; + border-radius: 0px; + padding: 10px 10px 5px 10px; + margin-bottom:10px; + overflow-y: scroll; + max-height: 300px; + width: 100%; +} + +.rda_metadata .form-control { + width: auto; +} + +.remove-standard { + border: none; + background: none; +} diff --git a/lib/tasks/initialize_data.rake b/lib/tasks/initialize_data.rake new file mode 100644 index 0000000..6813718 --- /dev/null +++ b/lib/tasks/initialize_data.rake @@ -0,0 +1,23 @@ +namespace :initialize_data do + desc "Add RDA Question Type" + task rda_ques: :environment do + rda_q_title = 'RDA Metadata Standards' + # check if already in the database + rda_q = QuestionFormat.find_by(title: rda_q_title) + if rda_q.blank? + rda_q = QuestionFormat.new() + rda_q.title = rda_q_title + puts 'Question format does not exist, adding' + else + puts 'Question format already exists, updating' + end + rda_q.option_based = false # keeping this false as options not stored locally + rda_q.formattype = QuestionFormat.formattypes[:rda_metadata] + rda_q.description = "https://dmponline-test.dcc.ac.uk/rda/api/" # TODO: Update to permanant API address once HTTPS is added + if rda_q.save + puts 'Sucessfully added/updated' + else + puts 'QuestionFormat not added/updated' + end + end +end