diff --git a/app/controllers/annotations_controller.rb b/app/controllers/annotations_controller.rb index 5e3b826..9191895 100644 --- a/app/controllers/annotations_controller.rb +++ b/app/controllers/annotations_controller.rb @@ -19,7 +19,8 @@ template.dirty = true template.save! end - redirect_to "#{admin_show_phase_path(question.section.phase.id)}?section_id=#{question.section.id}" + tab = params[:r] || 'all-templates' + redirect_to "#{admin_show_phase_path(question.section.phase.id)}?section_id=#{question.section.id}&r=#{tab}" end #delete an annotation @@ -35,7 +36,8 @@ flash[:alert] = failed_destroy_error(annotation, type) end end - redirect_to "#{admin_show_phase_path(parent_ids[0])}?section_id=#{parent_ids[1]}" + tab = params[:r] || 'all-templates' + redirect_to "#{admin_show_phase_path(parent_ids[0])}?section_id=#{parent_ids[1]}&r=#{tab}" end private 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/concerns/paginable.rb b/app/controllers/concerns/paginable.rb index dea6bb3..76340d3 100644 --- a/app/controllers/concerns/paginable.rb +++ b/app/controllers/concerns/paginable.rb @@ -7,15 +7,18 @@ # controller {String} - Represents the name of the controller to handles the pagination # action {String} - Represents the method name within the controller # path_params {Hash} - A hash of additional URL path parameters (e.g. path_paths = { id: 'foo' } for /paginable/templates/:id/history/:page) + # query_params {Hash} - A hash of query parameters used to merge with params object from the controller for which this concern is included # scope {ActiveRecord::Relation} - Represents scope variable # locals {Hash} - A hash objects with any additional local variables to be passed to the partial view - def paginable_renderise(partial: nil, controller: params[:controller], action: params[:action], path_params: {}, scope: nil, locals: {}) + def paginable_renderise(partial: nil, controller: params[:controller], action: params[:action], path_params: {}, query_params: {}, scope: nil, locals: {}) raise ArgumentError, 'scope should be an ActiveRecord::Relation object' unless scope.is_a?(ActiveRecord::Relation) raise ArgumentError, 'path_params should be a Hash object' unless path_params.is_a?(Hash) + raise ArgumentError, 'query_params should be a Hash object' unless query_params.is_a?(Hash) raise ArgumentError, 'locals should be a Hash object' unless locals.is_a?(Hash) @paginable_controller = controller @paginable_action = action @paginable_path_params = path_params + merge_query_params(query_params) refined_scope = refine_query(scope) render(layout: "/layouts/paginable", partial: partial, locals: { controller: controller, @@ -39,7 +42,7 @@ end # Determines whether or not the latest request included the search functionality def searchable? - return params.present? && params[:search].present? + return params[:search].present? end # Generates an HTML link with search functionality (if latest request included the search functionality) # text {String} - Represents the text for the searchable link @@ -51,10 +54,20 @@ end end private + # Attemps to merge query_params into params hash unless a key is already present at params + def merge_query_params(query_params = {}) + query_params.each_pair.reduce(params) do |m, o| + key = o[0].to_sym + if m[key].nil? + m[key] = o[1].to_s + end + m + end + end # Returns the upcase string (e.g ASC or DESC) if sort_direction param is present in any of the forms 'asc', 'desc', 'ASC', 'DESC' # otherwise returns nil def sort_direction - if params.present? && params[:sort_direction].present? + if params[:sort_direction].present? directions = ['asc', 'desc', 'ASC', 'DESC'] return directions.include?(params[:sort_direction]) ? params[:sort_direction].upcase : 'ASC' end @@ -67,15 +80,13 @@ end # Refine a scope passed to this concern if any of the params (search, sort_field or page) are present def refine_query(scope) - if params.present? - scope = scope.search(params[:search]) if params[:search].present? # Can raise NoMethodError if the scope does not define a search method - if params[:sort_field].present? - direction = sort_direction - scope = direction.present? ? scope.order("#{params[:sort_field]} #{direction}") : scope.order("#{params[:sort_field]}") # Can raise ActiveRecord::StatementInvalid (e.g. column does not exist, ambiguity on column, etc) - end - if params[:page] != 'ALL' - scope = scope.page(params[:page]) # Can raise error if page is not a number - end + scope = scope.search(params[:search]) if params[:search].present? # Can raise NoMethodError if the scope does not define a search method + if params[:sort_field].present? + direction = sort_direction + scope = direction.present? ? scope.order("#{params[:sort_field]} #{direction}") : scope.order("#{params[:sort_field]}") # Can raise ActiveRecord::StatementInvalid (e.g. column does not exist, ambiguity on column, etc) + end + if params[:page] != 'ALL' + scope = scope.page(params[:page]) # Can raise error if page is not a number end return scope end @@ -86,20 +97,19 @@ if direction.present? && params[:sort_field] == sort_field className = direction == 'ASC'? 'fa-sort-asc' : 'fa-sort-desc' end - return raw("") + return raw("") end # Returns the sort url for a given sort_field. def sort_link_url(sort_field) - url = paginable_base_url(1)+"?" + page = params[:page] == 'ALL' ? 'ALL' : 1 # Retain ALL param page if latest request included it + url = paginable_base_url(page)+"?" query_string = [] - if params.present? - query_string << "search=#{params[:search]}" if params[:search].present? - direction = sort_direction - if direction.present? && params[:sort_field] == sort_field - query_string << "sort_direction=#{swap_sort_direction(direction)}" - else - query_string << "sort_direction=ASC" - end + query_string << "search=#{params[:search]}" if params[:search].present? + direction = sort_direction + if direction.present? && params[:sort_field] == sort_field + query_string << "sort_direction=#{swap_sort_direction(direction)}" + else + query_string << "sort_direction=ASC" end query_string << "sort_field=#{sort_field}" return url+query_string.join('&') diff --git a/app/controllers/guidances_controller.rb b/app/controllers/guidances_controller.rb index 6351b7c..357889f 100644 --- a/app/controllers/guidances_controller.rb +++ b/app/controllers/guidances_controller.rb @@ -67,8 +67,10 @@ authorize guidance guidance.text = params["guidance-text"] - if guidance.update_attributes(guidance_params) - + attrs = guidance_params + attrs[:theme_ids] = [] unless attrs[:theme_ids] + + if guidance.update_attributes(attrs) if guidance.published? guidance_group = GuidanceGroup.find(guidance.guidance_group_id) if !guidance_group.published? || guidance_group.published.nil? diff --git a/app/controllers/notes_controller.rb b/app/controllers/notes_controller.rb index 996ed7b..94386d9 100644 --- a/app/controllers/notes_controller.rb +++ b/app/controllers/notes_controller.rb @@ -40,7 +40,7 @@ plan = answer.plan owner = plan.owner deliver_if(recipients: owner, key: 'users.new_comment') do |r| - UserMailer.new_comment(r, plan).deliver_now() + UserMailer.new_comment(current_user, plan).deliver_now() end @notice = success_message(_('comment'), _('created')) render(json: { diff --git a/app/controllers/org_admin/plans_controller.rb b/app/controllers/org_admin/plans_controller.rb index 9a303d6..b8bd89d 100644 --- a/app/controllers/org_admin/plans_controller.rb +++ b/app/controllers/org_admin/plans_controller.rb @@ -43,7 +43,7 @@ plans = CSV.generate do |csv| csv << header_cols - org.plans.includes(template: :org).each do |plan| + org.plans.includes(template: :org).order(updated_at: :desc).each do |plan| owner = plan.owner csv << [ "#{plan.title}", diff --git a/app/controllers/org_admin/templates_controller.rb b/app/controllers/org_admin/templates_controller.rb index 54930c9..158016e 100644 --- a/app/controllers/org_admin/templates_controller.rb +++ b/app/controllers/org_admin/templates_controller.rb @@ -30,7 +30,7 @@ published: published, current_org: current_user.org, orgs: Org.all, - current_tab: params[:r] || 'all-templates', + current_tab: params[:r], scopes: { all: all_templates_hash[:scopes], orgs: own_hash[:scopes], funders: customizable_hash[:scopes] } } end @@ -53,10 +53,10 @@ @template.links = (params["template-links"].present? ? JSON.parse(params["template-links"]) : {"funder": [], "sample_plan": []}) if @template.save - redirect_to edit_org_admin_template_path(@template), notice: success_message(_('template'), _('created')) + redirect_to edit_org_admin_template_path(@template), notice: success_message(@template.template_type, _('created')) else @hash = @template.to_hash - flash[:alert] = failed_create_error(@template, _('template')) + flash[:alert] = failed_create_error(@template, @template.template_type) render action: "new" end end @@ -68,30 +68,31 @@ authorize @template @current = Template.current(@template.dmptemplate_id) - + @current_tab = params[:r] || 'all-templates' + if @template == @current if @template.published? new_version = @template.get_new_version if !new_version.nil? - redirect_to(action: 'edit', id: new_version.id) + redirect_to(action: 'edit', id: new_version.id, r: @current_tab) return else - flash[:alert] = _('Unable to create a new version of this template. You are currently working with a published copy.') + flash[:alert] = _("Unable to create a new version of this #{@template.template_type}. You are currently working with a published copy.") end end else - flash[:notice] = _('You are viewing a historical version of this template. You will not be able to make changes.') + flash[:notice] = _("You are viewing a historical version of this #{@template.template_type}. You will not be able to make changes.") end # once the correct template has been generated, we convert it to hash @template_hash = @template.to_hash - @current_tab = params[:r] || 'all-templates' render('container', locals: { partial_path: 'edit', template: @template, current: @current, + edit: @template == @current, template_hash: @template_hash, current_tab: @current_tab }) @@ -107,13 +108,13 @@ # Only allow the current version to be updated if current != @template - render(status: :forbidden, json: { msg: _('You can not edit a historical version of this template.')}) + render(status: :forbidden, json: { msg: _("You can not edit a historical version of this #{@template.template_type}.")}) else template_links = nil begin template_links = JSON.parse(params["template-links"]) if params["template-links"].present? rescue JSON::ParserError - render(status: :bad_request, json: { msg: _('Error parsing links for a template') }) + render(status: :bad_request, json: { msg: _("Error parsing links for a #{@template.template_type}") }) return end @@ -136,10 +137,10 @@ end if @template.update_attributes(params[:template]) - render(status: :ok, json: { msg: success_message(_('template'), _('saved'))}) + render(status: :ok, json: { msg: success_message(@template.template_type, _('saved'))}) else # Note failed_update_error may return HTML tags (e.g.
) and therefore the client should parse them accordingly - render(status: :bad_request, json: { msg: failed_update_error(@template, _('template'))}) + render(status: :bad_request, json: { msg: failed_update_error(@template, @template.template_type)}) end end end @@ -157,19 +158,19 @@ # Only allow the current version to be destroyed if current == @template if @template.destroy - flash[:notice] = success_message(_('template'), _('removed')) + flash[:notice] = success_message(@template.template_type, _('removed')) redirect_to org_admin_templates_path(r: current_tab) else @hash = @template.to_hash - flash[:alert] = failed_destroy_error(@template, _('template')) + flash[:alert] = failed_destroy_error(@template, @template.template_type) redirect_to org_admin_templates_path(r: current_tab) end else - flash[:alert] = _('You cannot delete historical versions of this template.') + flash[:alert] = _("You cannot delete historical versions of this #{@template.template_type}.") redirect_to org_admin_templates_path(r: current_tab) end else - flash[:alert] = _('You cannot delete a template that has been used to create plans.') + flash[:alert] = _("You cannot delete a #{@template.template_type} that has been used to create plans.") redirect_to org_admin_templates_path(r: current_tab) end end @@ -179,7 +180,7 @@ def history @template = Template.find(params[:id]) authorize @template - @templates = Template.where(dmptemplate_id: @template.dmptemplate_id).order(version: :desc) + @templates = Template.where(dmptemplate_id: @template.dmptemplate_id) @current = Template.current(@template.dmptemplate_id) @current_tab = params[:r] || 'all-templates' end @@ -308,10 +309,10 @@ end if new_copy.save - flash[:notice] = 'Template was successfully copied.' + flash[:notice] = "#{@template.template_type.capitalize} was successfully copied." redirect_to edit_org_admin_template_path(id: new_copy.id, edit: true, r: 'organisation-templates'), notice: _('Information was successfully created.') else - flash[:alert] = failed_create_error(new_copy, _('template')) + flash[:alert] = failed_create_error(new_copy, @template.template_type) end end @@ -321,12 +322,11 @@ def publish template = Template.find(params[:id]) authorize template - current = Template.current(template.dmptemplate_id) # Only allow the current version to be updated if current != template - redirect_to org_admin_templates_path, alert: _('You can not publish a historical version of this template.') + redirect_to org_admin_templates_path, alert: _("You can not publish a historical version of this #{template.template_type}.") else # Unpublish the older published version if there is one @@ -340,8 +340,8 @@ template.published = true template.save - flash[:notice] = _('Your template has been published and is now available to users.') - redirect_to "#{org_admin_templates_path}#{template.customization_of.present? ? '#funder-templates' : '#organisation-templates'}" + flash[:notice] = _("Your #{template.template_type} has been published and is now available to users.") + redirect_to "#{org_admin_templates_path}#{template.template_type == 'customisation' ? '#funder-templates' : '#organisation-templates'}" end end @@ -352,14 +352,14 @@ authorize template if template.nil? - flash[:alert] = _('That template is not currently published.') + flash[:alert] = _("That #{template.template_type} is not currently published.") else template.published = false template.save - flash[:notice] = _('Your template is no longer published. Users will not be able to create new DMPs for this template until you re-publish it') + flash[:notice] = _("Your #{template.template_type} is no longer published. Users will not be able to create new DMPs for this #{template.template_type} until you re-publish it") end - redirect_to "#{org_admin_templates_path}#{template.customization_of.present? ? '#funder-templates' : '#organisation-templates'}" + redirect_to "#{org_admin_templates_path}#{template.template_type == 'customisation' ? '#funder-templates' : '#organisation-templates'}" end # PUT /org_admin/template_options (AJAX) diff --git a/app/controllers/paginable/orgs_controller.rb b/app/controllers/paginable/orgs_controller.rb index 16bd7df..fc78fa6 100644 --- a/app/controllers/paginable/orgs_controller.rb +++ b/app/controllers/paginable/orgs_controller.rb @@ -5,7 +5,7 @@ authorize(Org) paginable_renderise( partial: 'index', - scope: Org.includes(:templates, :users).joins(:templates, :users) - ) + scope: Org.includes(:templates, :users), + query_params: { sort_field: 'orgs.name', sort_direction: :asc }) end end diff --git a/app/controllers/paginable/plans_controller.rb b/app/controllers/paginable/plans_controller.rb index d691dda..8d29990 100644 --- a/app/controllers/paginable/plans_controller.rb +++ b/app/controllers/paginable/plans_controller.rb @@ -9,7 +9,8 @@ def organisationally_or_publicly_visible raise Pundit::NotAuthorizedError unless Paginable::PlanPolicy.new(current_user).organisationally_or_publicly_visible? paginable_renderise(partial: 'organisationally_or_publicly_visible', - scope: Plan.organisationally_or_publicly_visible(current_user)) + scope: Plan.organisationally_or_publicly_visible(current_user), + query_params: { sort_field: 'plans.title', sort_direction: :asc }) end # GET /paginable/plans/publicly_visible/:page def publicly_visible diff --git a/app/controllers/paginable/templates_controller.rb b/app/controllers/paginable/templates_controller.rb index 62cede2..b3370c1 100644 --- a/app/controllers/paginable/templates_controller.rb +++ b/app/controllers/paginable/templates_controller.rb @@ -86,6 +86,7 @@ paginable_renderise( partial: 'history', scope: @templates, + query_params: { sort_field: :version, sort_direction: :desc }, locals: { current: @current }) end end diff --git a/app/controllers/phases_controller.rb b/app/controllers/phases_controller.rb index 3d10ca9..14e3210 100644 --- a/app/controllers/phases_controller.rb +++ b/app/controllers/phases_controller.rb @@ -43,7 +43,7 @@ #show and edit a phase of the template def admin_show - @phase = Phase.includes(:sections).order(:number).find(params[:id]) + @phase = Phase.includes(:template, :sections).order(:number).find(params[:id]) authorize @phase @current = Template.current(@phase.template.dmptemplate_id) @@ -58,6 +58,10 @@ @original_org = @phase.template.org end + if @phase.template != @current + flash[:notice] = _('You are viewing a historical version of this template. You will not be able to make changes.') + end + render('/org_admin/templates/container', locals: { partial_path: 'admin_show', @@ -90,6 +94,7 @@ locals: { partial_path: 'admin_add', template: @template, + edit: true, current_tab: params[:r] || 'all-templates' }) end diff --git a/app/controllers/plans_controller.rb b/app/controllers/plans_controller.rb index fddb1e9..48b544d 100644 --- a/app/controllers/plans_controller.rb +++ b/app/controllers/plans_controller.rb @@ -8,7 +8,7 @@ def index authorize Plan @plans = Plan.active(current_user).page(1) - @organisationally_or_publicly_visible = Plan.organisationally_or_publicly_visible(current_user).order(:title => :asc).page(1) + @organisationally_or_publicly_visible = Plan.organisationally_or_publicly_visible(current_user).page(1) end # GET /plans/new @@ -156,18 +156,26 @@ authorize @plan attrs = plan_params - # Save the guidance group selections - guidance_group_ids = params[:guidance_group_ids].blank? ? [] : params[:guidance_group_ids].map(&:to_i).uniq - save_guidance_selections(guidance_group_ids) - respond_to do |format| - if @plan.update_attributes(attrs) - format.html { redirect_to @plan, :editing => false, notice: success_message(_('plan'), _('saved')) } - format.json {render json: {code: 1, msg: success_message(_('plan'), _('saved'))}} - else + begin + # Save the guidance group selections + guidance_group_ids = params[:guidance_group_ids].blank? ? [] : params[:guidance_group_ids].map(&:to_i).uniq + @plan.guidance_groups = GuidanceGroup.where(id: guidance_group_ids) + @plan.save + + if @plan.update_attributes(attrs) + format.html { redirect_to @plan, :editing => false, notice: success_message(_('plan'), _('saved')) } + format.json {render json: {code: 1, msg: success_message(_('plan'), _('saved'))}} + else + flash[:alert] = failed_update_error(@plan, _('plan')) + format.html { render action: "edit" } + format.json {render json: {code: 0, msg: flash[:alert]}} + end + + rescue Exception flash[:alert] = failed_update_error(@plan, _('plan')) format.html { render action: "edit" } - format.json {render json: {code: 0, msg: failed_update_error(@plan, _('plan'))}} + format.json {render json: {code: 0, msg: flash[:alert]}} end end end @@ -243,21 +251,21 @@ @public_plan = false @hash = @plan.as_pdf(@show_coversheet) - @formatting = @plan.settings(:export).formatting + @formatting = params[:export][:formatting] || @plan.settings(:export).formatting file_name = @plan.title.gsub(/ /, "_") respond_to do |format| format.html { render layout: false } format.csv { send_data @plan.as_csv(@show_sections_questions), filename: "#{file_name}.csv" } format.text { send_data render_to_string(partial: 'shared/export/plan_txt'), filename: "#{file_name}.txt" } - format.docx { render docx: 'export', filename: "#{file_name}.docx" } + format.docx { render docx: "#{file_name}.docx", content: render_to_string(partial: 'shared/export/plan') } format.pdf do render pdf: file_name, margin: @formatting[:margin], footer: { center: _('Created using the %{application_name}. Last modified %{date}') % {application_name: Rails.configuration.branding[:application][:name], date: l(@plan.updated_at.to_date, formats: :short)}, font_size: 8, - spacing: (@formatting[:margin][:bottom] / 2) - 4, + spacing: (Integer(@formatting[:margin][:bottom]) / 2) - 4, right: '[page] of [topage]' } end @@ -350,25 +358,6 @@ :data_contact, :data_contact_email, :data_contact_phone, :guidance_group_ids) end - def save_guidance_selections(guidance_group_ids) - all_guidance_groups = @plan.get_guidance_group_options - plan_groups = @plan.guidance_groups - guidance_groups = GuidanceGroup.where(id: guidance_group_ids) - all_guidance_groups.each do |group| - # case where plan group exists but not in selection - if plan_groups.include?(group) && ! guidance_groups.include?(group) - # remove from plan groups - @plan.guidance_groups.delete(group) - end - # case where plan group dosent exist and in selection - if !plan_groups.include?(group) && guidance_groups.include?(group) - # add to plan groups - @plan.guidance_groups << group - end - end - @plan.save - end - # different versions of the same template have the same dmptemplate_id # but different version numbers so for each set of templates with the 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/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index 44a73cb..292d970 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -85,7 +85,11 @@ @section = @question.section @phase = @section.phase template = @phase.template - if @question.update_attributes(params[:question]) + + attrs = params[:question] + attrs[:theme_ids] = [] unless attrs[:theme_ids] + + if @question.update_attributes(attrs) @phase.template.dirty = true @phase.template.save! diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 7984224..aa0380d 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -52,13 +52,13 @@ else existing_user = User.where_case_insensitive('email', sign_up_params[:email]).first if existing_user.present? - if existing_user.accept_terms? + if existing_user.invitation_token.present? && !existing_user.accept_terms? + existing_user.destroy # Destroys the existing user since the accept terms are nil/false. and they have an invitation + # Note any existing role for that user will be deleted too. Added to accommodate issue at: + # https://github.com/DMPRoadmap/roadmap/issues/322 when invited user creates an account outside the invite workflow + else redirect_to after_sign_up_error_path_for(resource), alert: _('That email address is already registered.') return - else - existing_user.destroy # Destroys the existing user since the accept terms are nil/false. - # Note any existing role for that user will be deleted too. Added to accommodate issue at: - # https://github.com/DMPRoadmap/roadmap/issues/322 end end if params[:user][:org_id].blank? @@ -155,10 +155,10 @@ else successfully_updated = current_user.update_with_password(password_update) end - else # user did not change their email so no pwd required + else # This case is never reached since this method when called with require_password = true is because the email changed. The case for password changed goes to do_update_password instead successfully_updated = current_user.update_without_password(update_params) end - else # password not required + else # password not required successfully_updated = current_user.update_without_password(update_params) end else @@ -203,7 +203,7 @@ session[:locale] = current_user.get_locale unless current_user.get_locale.nil? set_gettext_locale #Method defined at controllers/application_controller.rb set_flash_message :notice, success_message(_('password'), _('saved')) - sign_in current_user, bypass: true # Sign in the user bypassing validation in case his password changed + sign_in current_user, bypass: true # TODO this method is deprecated redirect_to "#{edit_user_registration_path}\#password-details", notice: success_message(_('password'), _('saved')) else diff --git a/app/controllers/super_admin/orgs_controller.rb b/app/controllers/super_admin/orgs_controller.rb index 014fa72..72d338b 100644 --- a/app/controllers/super_admin/orgs_controller.rb +++ b/app/controllers/super_admin/orgs_controller.rb @@ -4,7 +4,7 @@ def index authorize Org - render 'index', locals: { orgs: Org.includes(:templates, :users).joins(:templates, :users).order('orgs.name') } + render 'index', locals: { orgs: Org.includes(:templates, :users) } end def new diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1d1e2fe..992c6ab 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -30,6 +30,10 @@ end end + def is_integer?(string) + return string.present? && string.match(/^(\d)+$/) + end + def fingerprinted_asset(name) Rails.env.production? ? "#{name}-#{ASSET_FINGERPRINT}" : name 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/org.rb b/app/models/org.rb index 960d2e1..dee239c 100644 --- a/app/models/org.rb +++ b/app/models/org.rb @@ -151,7 +151,7 @@ end def grant_api!(token_permission_type) - org.token_permission_types << token_permission_type unless org.tokenpermission_types.include? token_permission_type + self.token_permission_types << token_permission_type unless self.token_permission_types.include? token_permission_type end private diff --git a/app/models/plan.rb b/app/models/plan.rb index 9ef0a14..431ceac 100644 --- a/app/models/plan.rb +++ b/app/models/plan.rb @@ -218,7 +218,8 @@ if self.save! # Send an email confirmation to the owners and co-owners - deliver_if(recipients: self.owner_and_coowners, key: 'users.feedback_requested') do |r| + owners = User.joins(:roles).where('roles.plan_id =? AND roles.access IN (?)', self.id, Role.access_values_for(:administrator)) + deliver_if(recipients: owners, key: 'users.feedback_requested') do |r| UserMailer.feedback_confirmation(r, self, user).deliver_now end # Send an email to all of the org admins as well as the Org's administrator email @@ -255,8 +256,9 @@ if self.save! # Send an email confirmation to the owners and co-owners - deliver_if(recipients: self.owner_and_coowners, key: 'users.feedback_provided') do |r| - UserMailer.feedback_notification(r, self, org_admin).deliver_now + owners = User.joins(:roles).where('roles.plan_id =? AND roles.access IN (?)', self.id, Role.access_values_for(:administrator)) + deliver_if(recipients: owners, key: 'users.feedback_provided') do |r| + UserMailer.feedback_complete(r, self, org_admin).deliver_now end true else 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/models/template.rb b/app/models/template.rb index 0da0b23..21e5b9c 100644 --- a/app/models/template.rb +++ b/app/models/template.rb @@ -169,6 +169,7 @@ new_version.version = (self.version + 1) new_version.published = false new_version.visibility = self.visibility # do not change the visibility + new_version.is_default = self.is_default # retain the default template flag new_version.save! new_version else @@ -253,6 +254,10 @@ end return !modifiable end + + def template_type + self.customization_of.present? ? _('customisation') : _('template') + end # -------------------------------------------------------- private @@ -264,7 +269,7 @@ self.migrated = false self.dirty = false self.visibility = 0 # Organisationally visible by default - self.is_default = false + self.is_default = false if self.is_default.nil? self.version = 0 if self.version.nil? self.visibility = Template.visibilities[:organisationally_visible] if self.visibility.nil? diff --git a/app/models/user.rb b/app/models/user.rb index e0eedf1..2090de0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -65,6 +65,8 @@ end } + after_update :when_org_changes + # EVALUATE CLASS AND INSTANCE METHODS BELOW # # What do they do? do they do it efficiently, and do we need them? @@ -114,29 +116,6 @@ user_identifiers.where(identifier_scheme: scheme).first end -# TODO: Check the logic here. Its deleting the permissions if the user does not have permission -# to change orgs and either the incoming or existing org is nil. -# We should also NOT be auto-saving here!!! - ## - # sets a new organisation id for the user - # if the user has any perms such as org_admin or admin, those are removed - # if the user had an api_token, that is removed - # - # @param new_organisation_id [Integer] the id for an organisation - # @return [String] the empty string as a causality of setting api_token - def org_id=(new_org_id) - puts "$$$$ new_org_id: #{new_org_id}" - unless self.can_change_org? || new_org_id.nil? || self.org.nil? || (new_org_id.to_s == self.org.id.to_s) - # rip all permissions from the user - self.perms.delete_all - end - # set the user's new organisation - super(new_org_id) - # self.save! - # rip api permissions from the user - self.remove_token! - end - ## # sets a new organisation for the user # @@ -236,8 +215,7 @@ # modifies the user model def remove_token! unless api_token.blank? - self.api_token = "" - self.save! + update_column(:api_token, "") unless new_record? end end @@ -250,7 +228,7 @@ random_token = SecureRandom.urlsafe_base64(nil, false) break random_token unless User.exists?(api_token: random_token) end - self.save! + update_column(:api_token, api_token) unless new_record? deliver_if(recipients: self, key: 'users.admin_privileges') do |r| UserMailer.api_token_granted_notification(r).deliver_now end @@ -310,4 +288,14 @@ def self.where_case_insensitive(field, val) User.where("lower(#{field}) = ?", val.respond_to?(:downcase) ? val.downcase : val.to_s) end + + private + def when_org_changes + if org_id != org_id_was + unless can_change_org? + perms.delete_all + remove_token! + end + end + end end 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/devise/registrations/_external_identifier.html.erb b/app/views/devise/registrations/_external_identifier.html.erb index ee372c9..b48b983 100644 --- a/app/views/devise/registrations/_external_identifier.html.erb +++ b/app/views/devise/registrations/_external_identifier.html.erb @@ -1,10 +1,13 @@ <% if id.nil? || id.identifier == '' %> <% if scheme.name.downcase == 'orcid' %> - <%= link_to _("Create or connect your ORCID iD"), - Rails.application.routes.url_helpers.send("user_orcid_omniauth_authorize_path"), + <%= link_to Rails.application.routes.url_helpers.send("user_orcid_omniauth_authorize_path"), id: "connect-orcid-button", target: '_blank', title: _("ORCID provides a persistent digital identifier that distinguishes you from other researchers. Learn more at orcid.org"), - 'data-toggle': "tooltip" %> + 'data-toggle': "tooltip" do %> + <%= image_tag 'https://orcid.org/sites/default/files/images/orcid_16x16.png', alt: _('ORCID logo') %> +   + <%= _("Create or connect your ORCID iD") %> + <% end %> <% elsif scheme.name.downcase == 'shibboleth' %> <i class="fa fa-user" title="<%= _('Institutional credentials') %>" aria-hidden="true"></i>   diff --git a/app/views/devise/registrations/_personal_details.html.erb b/app/views/devise/registrations/_personal_details.html.erb index 058dc8c..7029a4f 100644 --- a/app/views/devise/registrations/_personal_details.html.erb +++ b/app/views/devise/registrations/_personal_details.html.erb @@ -38,6 +38,13 @@ </div> <% end %> + <% if current_user.can_org_admin? %> + <div class="form-group col-xs-8"> + <label><%= _('My privileges') %></label> + <p><%= (current_user.can_super_admin? ? _('Super Admin') : _('Organisational Admin')) %></p> + </div> + <% end %> + <% @identifier_schemes.each do |scheme| %> <div class="form-group col-xs-8"> <% if scheme.name.downcase == 'shibboleth' %> diff --git a/app/views/guidances/admin_index.html.erb b/app/views/guidances/admin_index.html.erb index 3478f88..03bf76c 100644 --- a/app/views/guidances/admin_index.html.erb +++ b/app/views/guidances/admin_index.html.erb @@ -17,7 +17,8 @@ partial: '/paginable/guidance_groups/index', controller: 'paginable/guidance_groups', action: 'index', - scope: @guidance_groups) %> + scope: @guidance_groups, + query_params: { sort_field: 'guidance_groups.name', sort_direction: :asc }) %> <div> <a href="<%= admin_new_guidance_group_path %>" class="btn btn-primary"><%= _('Create a guidance group') %></a> </div> @@ -36,7 +37,8 @@ partial: '/paginable/guidances/index', controller: 'paginable/guidances', action: 'index', - scope: @guidances) %> + scope: @guidances, + query_params: { sort_field: 'guidances.text', sort_direction: :asc }) %> <div> <a href="<%= admin_new_guidance_path %>" class="btn btn-primary"><%= _('Create guidance') %></a> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index f3d9ac6..801fba4 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -70,5 +70,41 @@ <footer > <%= render "layouts/footer" %> </footer> + + <% + constants_json = { + PASSWORD_MIN_LENGTH: 8, + PASSWORD_MAX_LENGTH: 128, + MAX_NUMBER_ORG_URLS: 3, + MAX_NUMBER_GUIDANCE_SELECTIONS: 6, + + VALIDATION_MESSAGE_DEFAULT: _('Please enter a valid value.'), + VALIDATION_MESSAGE_EMAIL: _('You must enter a valid email address.'), + VALIDATION_MESSAGE_URL: _('You must enter a valid URL (e.g. https://organisation.org).'), + VALIDATION_MESSAGE_NUMBER: _('Please enter a valid number.'), + VALIDATION_MESSAGE_PASSWORD: _('The password must be between 8 and 128 characters.'), + VALIDATION_MESSAGE_PASSWORDS_MATCH: _('The passwords must match.'), + VALIDATION_MESSAGE_RADIO: _('Please choose one of the options.'), + VALIDATION_MESSAGE_CHECKBOX: _('Please check the box to continue.'), + VALIDATION_MESSAGE_SELECT: _('Please select a value from the list.'), + VALIDATION_MESSAGE_TEXT: _('This field is required.'), + + SHOW_PASSWORD_MESSAGE: _('Show password'), + SHOW_SELECT_ORG_MESSAGE: _('Select an organisation from the list.'), + SHOW_OTHER_ORG_MESSAGE: _('My organisation isn\'t listed'), + + PLAN_VISIBILITY_WHEN_TEST: _('N/A'), + PLAN_VISIBILITY_WHEN_NOT_TEST: _('Private'), + PLAN_VISIBILITY_WHEN_NOT_TEST_TOOLTIP: _('Private: restricted to me and people I invite.'), + + SHIBBOLETH_DISCOVERY_SERVICE_HIDE_LIST: _('Hide list.'), + SHIBBOLETH_DISCOVERY_SERVICE_SHOW_LIST: _('See the full list of partner institutions.'), + + NO_TEMPLATE_FOUND_ERROR: _('Unable to find a suitable template for the research organisation and funder you selected.'), + NEW_PLAN_DISABLED_TOOLTIP: _('Please select a research organisation and funder to continue.') + }.to_json + %> + + <input type="hidden" id="js-constants" value="<%= constants_json %>" /> </body> </html> diff --git a/app/views/org_admin/plans/index.html.erb b/app/views/org_admin/plans/index.html.erb index ebe589e..cd2ff28 100644 --- a/app/views/org_admin/plans/index.html.erb +++ b/app/views/org_admin/plans/index.html.erb @@ -39,7 +39,8 @@ partial: '/paginable/plans/org_admin', controller: 'paginable/plans', action: 'org_admin', - scope: @plans) %> + scope: @plans, + query_params: { sort_field: 'plans.updated_at', sort_direction: :desc }) %> <% end %> </div> </div> diff --git a/app/views/org_admin/templates/_admin_nav_tabs.html.erb b/app/views/org_admin/templates/_admin_nav_tabs.html.erb index 23246f1..012229d 100644 --- a/app/views/org_admin/templates/_admin_nav_tabs.html.erb +++ b/app/views/org_admin/templates/_admin_nav_tabs.html.erb @@ -9,7 +9,7 @@ </li> <% end %> <!-- Add another phase button --> - <% if current_user.can_org_admin? && template.customization_of.nil? %> + <% if current_user.can_org_admin? && template.customization_of.nil? && edit %> <li role="presentation" <%= isActivePage(admin_add_phase_path(template)) ? ' class=active' : '' %>> <%= link_to(_('Add new phase'), admin_add_phase_path(template, r: current_tab), { 'aria-controls': 'add_phase', role: 'tab' }) %> </li> diff --git a/app/views/org_admin/templates/_edit.html.erb b/app/views/org_admin/templates/_edit.html.erb index 20ffefd..befa1c5 100644 --- a/app/views/org_admin/templates/_edit.html.erb +++ b/app/views/org_admin/templates/_edit.html.erb @@ -33,14 +33,15 @@ <div class="panel-group" id="phases_accordion" role="tablist"> <!-- If template has phases--> <% if template_hash[:template][:phases].present? %> + <% i = 0 %> <% template_hash[:template][:phases].each do |phase_no, phase_hash| %> <% phase = phase_hash[:data] %> <div class="panel panel-default"> <div class="heading-button" role="button" data-toggle="collapse" data-parent="phases_accordion" - href="#collapsePhase#<%= phase.id %>" - aria-expanded="false" - aria-controls="#collapsePhase#<%= phase.id %>"> + href="#collapsePhase<%= phase.id %>" + aria-expanded="<%= i == 0 ? 'true' : 'false' %>" + aria-controls="#collapsePhase<%= phase.id %>"> <div class="panel-heading" role="tab" id="<%= "headingPhase#{phase.id}" %>"> <div class="panel-title pull-left"> @@ -52,12 +53,13 @@ <div class="clearfix"></div> </div> </div> - <div id="<%= "collapsePhase#{phase.id}" %>" class="panel-collapse collapse" role="tabpanel" aria-labelledby="<%= "headingPhase#{phase.id}" %>"> + <div id="<%= "collapsePhase#{phase.id}" %>" class="panel-collapse collapse<%= i == 0 ? 'in' : '' %>" role="tabpanel" aria-labelledby="<%= "headingPhase#{phase.id}" %>"<%= i == 0 ? 'aria-expanded="true"' : '' %>> <div class="panel-body"> <%= render partial: 'org_admin/templates/show_phases_sections', locals: { phase: phase, phase_hash: phase_hash, template: template, current: current } %> </div> </div> </div> + <% i += 1 %> <% end %> <% end %> </div> diff --git a/app/views/org_admin/templates/_show_phases_sections.html.erb b/app/views/org_admin/templates/_show_phases_sections.html.erb index e1ab1ad..e0452ec 100644 --- a/app/views/org_admin/templates/_show_phases_sections.html.erb +++ b/app/views/org_admin/templates/_show_phases_sections.html.erb @@ -12,11 +12,9 @@ <% phase_button_text = template.customization_of.nil? ? _('Show Phase') : _('Customize Phase') %> <%= link_to phase_button_text, admin_show_phase_path(id: phase.id), { class: "btn btn-default", role: 'button' } %> <% if template == current && phase.modifiable %> - <%= link_to _('Delete'), admin_destroy_phase_path(phase_id: phase.id), { - confirm: _("You are about to delete '%{phase_title}'. This will affect versions, sections and questions linked to this phase. Are you sure?") % { phase_title: phase.title }, - method: :delete, - class: "btn btn-default", role: "button" } - %> + <%= link_to _('Delete phase'), admin_destroy_phase_path(phase_id: phase.id), + data: { confirm: _("You are about to delete the '%{phase_title}' phase. This will remove all of the sections and questions listed below. Are you sure?") % { phase_title: phase.title }, + length: 20, omission: _('... (continued)') }, method: :delete, class: 'btn btn-default', role: 'button' %> <% end %> </div> </div> diff --git a/app/views/org_admin/templates/history.html.erb b/app/views/org_admin/templates/history.html.erb index fb2452d..850b935 100644 --- a/app/views/org_admin/templates/history.html.erb +++ b/app/views/org_admin/templates/history.html.erb @@ -18,7 +18,8 @@ partial: '/paginable/templates/history', controller: 'paginable/templates', action: 'history', - path_params: { id: @template.id }, + path_params: { id: @template.id }, + query_params: { sort_field: :version, sort_direction: :desc }, scope: @templates, locals: { current: @current }) %> <% else %> diff --git a/app/views/org_admin/templates/index.html.erb b/app/views/org_admin/templates/index.html.erb index 7731189..8fc6a6f 100644 --- a/app/views/org_admin/templates/index.html.erb +++ b/app/views/org_admin/templates/index.html.erb @@ -20,20 +20,21 @@ </p> </div> </div> +<% selected_tab = current_tab || (current_user.can_super_admin? ? 'all-templates' : 'organisation-templates') %> <div class="row"> <div class="col-md-12"> <ul class="nav nav-tabs" role="tablist"> <% if current_user.can_super_admin? %> - <li role="all-templates"<%= current_tab == 'all-templates' ? 'class=active' : '' %>> + <li role="all-templates"<%= selected_tab == 'all-templates' ? 'class=active' : '' %>> <a href="#all-templates" role="tab" aria-controls="all-templates" data-toggle="tab"><%= _('All Templates') %></a> </li> <% end %> - <li role="organisation-templates"<%= current_tab == 'organisation-templates' ? 'class=active' : '' %>> + <li role="organisation-templates"<%= selected_tab == 'organisation-templates' ? 'class=active' : '' %>> <a href="#organisation-templates" role="tab" aria-controls="organisation-templates" data-toggle="tab"><%= current_user.can_super_admin? ? _('%{org_name} Templates') % { org_name: current_user.org.name } : _('Own Templates') %></a> </li> <!-- If the Org is not just a funder then show the customizations table --> <% if !current_org.funder_only? %> - <li role="funder-templates"<%= current_tab == 'funder-templates' ? 'class=active' : '' %>> + <li role="funder-templates"<%= selected_tab == 'funder-templates' ? 'class=active' : '' %>> <a href="#funder-templates" role="tab" aria-controls="funder-templates" data-toggle="tab"><%= _('Customizable Templates') %></a> </li> <% end %> @@ -41,34 +42,37 @@ <div class="tab-content"> <% if current_user.can_super_admin? %> - <div id="all-templates" role="tabpanel" class="tab-pane active"> + <div id="all-templates" role="tabpanel" class="tab-pane<%= (selected_tab == 'all-templates' ? ' active' : '') %>"> <h2><%= _('All Templates') %></h2> <%= paginable_renderise( partial: 'paginable/templates/all', controller: 'paginable/templates', action: 'all', scope: all_templates, + query_params: { sort_field: 'templates.title', sort_direction: :asc }, locals: {current_org: current_org.id, published: published, scopes: scopes[:all], hide_actions: true}) %> </div> <% end %> - <div id="organisation-templates" role="tabpanel" class="tab-pane<%= !current_user.can_super_admin? ? ' active' : '' %>"> + <div id="organisation-templates" role="tabpanel" class="tab-pane<%= selected_tab == 'organisation-templates' ? ' active' : '' %>"> <h2><%= current_user.can_super_admin? ? _('%{org_name} Templates') % { org_name: current_user.org.name } : _('Own Templates') %></h2> <%= paginable_renderise( partial: 'paginable/templates/orgs', controller: 'paginable/templates', action: 'orgs', scope: own_templates, + query_params: { sort_field: 'templates.title', sort_direction: :asc }, locals: {current_org: current_org.id, published: published, scopes: scopes[:orgs], hide_actions: false}) %> </div> <!-- If the Org is not just a funder then show the customizations table --> <% if !current_org.funder_only? %> - <div id="funder-templates" role="tabpanel" class="tab-pane"> + <div id="funder-templates" role="tabpanel" class="tab-pane<%= selected_tab == 'funder-templates' ? ' active' : '' %>"> <h2><%= _('Customizable Templates') %></h2> <%= paginable_renderise( partial: 'paginable/templates/funders', controller: 'paginable/templates', action: 'funders', scope: customizable_templates, + query_params: { sort_field: 'templates.title', sort_direction: :asc }, locals: {current_org: current_org.id, customizations: customized_templates, published: published, scopes: scopes[:funders]}) %> </div> <% end %> diff --git a/app/views/phases/_admin_show.html.erb b/app/views/phases/_admin_show.html.erb index 90f4fe0..d29df78 100644 --- a/app/views/phases/_admin_show.html.erb +++ b/app/views/phases/_admin_show.html.erb @@ -60,7 +60,7 @@ <% if edit && section.modifiable %> <%= render partial: 'sections/edit_section', locals: { template: template, section: section, edit: edit, phase: phase, current_tab: current_tab } %> <% else %> - <%= render partial: 'sections/show_section', locals: { template: template, section: section, current_tab: current_tab } %> + <%= render partial: 'sections/show_section', locals: { template: template, section: section, current_tab: current_tab, edit: edit } %> <% end %> </div> </div> diff --git a/app/views/phases/admin_preview.html.erb b/app/views/phases/admin_preview.html.erb index bde064b..8619c4c 100644 --- a/app/views/phases/admin_preview.html.erb +++ b/app/views/phases/admin_preview.html.erb @@ -12,14 +12,13 @@ <div class="row"> <div class="col-md-12"> <!-- render navigation tabs for the template --> - <%= render partial: "/org_admin/templates/admin_nav_tabs", locals: { template: @template, active: @phase.id, current_tab: @current_tab } %> + <%= render partial: "/org_admin/templates/admin_nav_tabs", locals: { template: @template, active: @phase.id, edit: false, current_tab: @current_tab } %> <!-- render phase below --> <div class="tab-content"> <div role="tabpanel" class="tab-pane active"> <div class="panel panel-default"> <div class="panel-body"> - <%= render partial: '/phases/edit_plan_answers', locals: { plan: nil, phase: @phase, readonly: true, question_guidance: {}, - guidance_groups: [] } %> + <%= render partial: '/phases/edit_plan_answers', locals: { plan: nil, phase: @phase, readonly: true, question_guidance: {}, edit: false, guidance_groups: [] } %> </div> </div> </div> diff --git a/app/views/plans/index.html.erb b/app/views/plans/index.html.erb index b56a8c6..a00ec53 100644 --- a/app/views/plans/index.html.erb +++ b/app/views/plans/index.html.erb @@ -19,7 +19,8 @@ partial: '/paginable/plans/privately_visible', controller: 'paginable/plans', action: 'privately_visible', - scope: @plans) %> + scope: @plans, + query_params: { sort_field: 'plans.updated_at', sort_direction: :desc }) %> </div> </div> <div class="row"> @@ -36,8 +37,8 @@ partial: '/paginable/plans/organisationally_or_publicly_visible', controller: 'paginable/plans', action: 'organisationally_or_publicly_visible', - scope: @organisationally_or_publicly_visible) %> + scope: @organisationally_or_publicly_visible, + query_params: { sort_field: 'plans.title', sort_direction: :asc }) %> <% end %> </div> </div> - diff --git a/app/views/plans/new.html.erb b/app/views/plans/new.html.erb index be0af7f..4a9f9c9 100644 --- a/app/views/plans/new.html.erb +++ b/app/views/plans/new.html.erb @@ -86,6 +86,7 @@ <%= f.hidden_field(:visibility, value: @is_test ? 'is_test' : Rails.application.config.default_plan_visibility) %> <%= f.button(_('Create plan'), class: "btn btn-primary", type: "submit") %> + <%= link_to _('Cancel'), plans_path, class: 'btn btn-default' %> <% end %> </div> </div> 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/questions/_show_question.html.erb b/app/views/questions/_show_question.html.erb index 5e713d5..96bbb48 100644 --- a/app/views/questions/_show_question.html.erb +++ b/app/views/questions/_show_question.html.erb @@ -84,12 +84,12 @@ example_answer: example_answer, guidance: guidance, question: question, - options: { url: admin_update_annotation_path, method: 'PUT' } + options: { url: admin_update_annotation_path(r: current_tab), method: 'PUT' } } %> </div> <% end %> <div class="pull-right"> - <% if (question.modifiable) %> + <% if (question.modifiable && edit) %> <%= link_to _('Edit question'), "#question_edit#{question.id}", class: "btn btn-default question_edit_link", role: "button" %> <%= link_to _('Delete question'), admin_destroy_question_path(question_id: question.id, r: current_tab), confirm: _("You are about to delete '%{question_text}'. Are you sure?") % { :question_text => question.text }, method: :delete, class: "btn btn-default", role:"button" %> diff --git a/app/views/sections/_edit_section.html.erb b/app/views/sections/_edit_section.html.erb index 6e677cc..b97d078 100644 --- a/app/views/sections/_edit_section.html.erb +++ b/app/views/sections/_edit_section.html.erb @@ -44,7 +44,7 @@ <% questions.each do |question| %> <hr /> <div class="question_show" id="<%= "question_show#{question.id}" %>"> - <%= render partial: 'questions/show_question', locals: {template: template, question: question, current_tab: current_tab} %> + <%= render partial: 'questions/show_question', locals: {template: template, question: question, current_tab: current_tab, edit: edit} %> </div> <div class="question_edit" id="<%= "question_edit#{question.id}" %>" style="display: none;"> <%= render partial: 'questions/edit_question', locals: {template: template, question: question, current_tab: current_tab} %> @@ -54,14 +54,15 @@ <% end %> </div> </div> -<div class="row"> - <div class="col-md-12"> - <div class="pull-right"> - <%= link_to(_('Add Question'), '#', { class: 'btn btn-default question_new_link', role: "button" }) %> - </div> - <div class="question_new" style="display: none;"> - <%= render partial: 'questions/add_question', locals: { section: section, current_tab: current_tab } %> +<% if edit %> + <div class="row"> + <div class="col-md-12"> + <div class="pull-right"> + <%= link_to(_('Add Question'), '#', { class: 'btn btn-default question_new_link', role: "button" }) %> + </div> + <div class="question_new" style="display: none;"> + <%= render partial: 'questions/add_question', locals: { section: section, current_tab: current_tab } %> + </div> </div> </div> -</div> - \ No newline at end of file +<% end %> \ No newline at end of file diff --git a/app/views/sections/_show_section.html.erb b/app/views/sections/_show_section.html.erb index 557d968..4848a8c 100644 --- a/app/views/sections/_show_section.html.erb +++ b/app/views/sections/_show_section.html.erb @@ -13,9 +13,9 @@ <% questions.each do |question| %> <hr /> <div class="question_show" id="<%= "question_show#{question.id}" %>"> - <%= render partial: 'questions/show_question', locals: {template: template, question: question, current_tab: current_tab} %> + <%= render partial: 'questions/show_question', locals: {template: template, question: question, current_tab: current_tab, edit: edit} %> </div> - <% if question.modifiable %> + <% if question.modifiable && edit %> <div class="question_edit" id="<%= "question_edit#{question.id}" %>" style="display: none;"> <%= render partial: 'questions/edit_question', locals: {template: template, question: question, current_tab: current_tab} %> </div> diff --git a/app/views/shared/_sign_in_form.html.erb b/app/views/shared/_sign_in_form.html.erb index ba659b3..48428a5 100644 --- a/app/views/shared/_sign_in_form.html.erb +++ b/app/views/shared/_sign_in_form.html.erb @@ -1,11 +1,11 @@ <%= form_for resource, as: 'user', url: user_session_path, html: {id: "sign_in_form"} do |f| %> <div class="form-group"> <%= f.label(:email, _('Email'), class: 'control-label') %> - <%= f.email_field(:email, class: 'form-control', "aria-required": true) %> + <%= f.email_field(:email, class: 'form-control', "aria-required": true, id: nil) %> </div> <div class="form-group"> <%= f.label(:password, _('Password'), class: 'control-label') %> - <%= f.password_field(:password, class: 'form-control', "aria-required": true) %> + <%= f.password_field(:password, class: 'form-control', "aria-required": true, id: nil) %> </div> <div> <%= link_to _('Forgot password?'), new_password_path('user') %> diff --git a/app/views/shared/export/_plan.erb b/app/views/shared/export/_plan.erb index c7eaf8a..f9fdf65 100644 --- a/app/views/shared/export/_plan.erb +++ b/app/views/shared/export/_plan.erb @@ -9,11 +9,11 @@ <% 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 --> - <h1> + <h1 style="text-align:center;"> <%= (@hash[:phases].length > 1 ? "#{@plan.title} - #{phase[:title]}" : @plan.title) %> </h1> @@ -34,22 +34,34 @@ <% end %> </ul> <% else %> - <p><%= raw question[:text][0].gsub(/<tr>(\s|<td>|<\/td>| )*(<\/tr>|<tr>)/,"") %></p> + <p><%= raw question[:text][0].gsub(/<tr>(\s|<td>|<\/td>| )*(<\/tr>|<tr>)/,"") if question[:text].present? && question[:text][0].present? %></p> <% end %> <br> <% end %> <% answer = @plan.answer(question[:id], false) %> - <% blank = answer.present? ? answer.text.gsub(/<\/?p>/, '').gsub(/<br\s?\/?>/, '').chomp.blank? : true %> - <% if blank && @show_unanswered %> + <% blank = (answer.present? && answer.is_valid?) ? answer.text.gsub(/<\/?p>/, '').gsub(/<br\s?\/?>/, '').chomp.blank? : true %> + <% 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/app/views/shared/export/_plan_styling.erb b/app/views/shared/export/_plan_styling.erb index c013651..f33b2c2 100644 --- a/app/views/shared/export/_plan_styling.erb +++ b/app/views/shared/export/_plan_styling.erb @@ -1,22 +1,24 @@ +<% font_size = @formatting[:font_size].kind_of?(Integer) ? Integer(@formatting[:font_size]) : 10 %> + <style> html { font-family: <%= @formatting[:font_face].tr('"', '') -%>; - font-size: <%= @formatting[:font_size] -%>pt; + font-size: <%= font_size -%>pt; margin: <%= @formatting[:margin][:top] %>px <%= @formatting[:margin][:right] %>px <%= @formatting[:margin][:bottom] %>px <%= @formatting[:margin][:left] %>px; } h1 { - font-size: <%= @formatting[:font_size] + 8 -%>pt; + font-size: <%= font_size + 8 -%>pt; font-face:bold; padding: 0; } h2 { - font-size: <%= @formatting[:font_size] + 3 -%>pt; + font-size: <%= font_size + 3 -%>pt; font-face:bold; padding: 0; margin: 1em 0 0 0; } h3 { - font-size: <%= @formatting[:font_size] + 2 -%>pt; + font-size: <%= font_size + 2 -%>pt; font-face:bold; padding: 0; margin: 1em 0 0 0; @@ -43,7 +45,7 @@ margin-right: auto; } .cover-page { - font-size: <%= @formatting[:font_size] + 1 -%>pt; + font-size: <%= font_size + 1 -%>pt; } .question { margin-top: 15px; diff --git a/app/views/shared/export/_plan_txt.erb b/app/views/shared/export/_plan_txt.erb index 76c5d87..ff2891e 100644 --- a/app/views/shared/export/_plan_txt.erb +++ b/app/views/shared/export/_plan_txt.erb @@ -35,11 +35,11 @@ <%= txt + "\n" %> <% end %> <% else %> -<%= question[:text][0].gsub(/<tr>(\s|<td>|<\/td>| )*(<\/tr>|<tr>)/,"") + "\n" %> +<%= question[:text][0].gsub(/<tr>(\s|<td>|<\/td>| )*(<\/tr>|<tr>)/,"") + "\n" if question[:text].present? && question[:text][0].present? %> <% end %> <% end %> <% answer = @plan.answer(question[:id], false) %> - <% blank = answer.present? ? answer.text.gsub(/<\/?p>/, '').gsub(/<br\s?\/?>/, '').chomp.blank? : true %> + <% blank = (answer.present? && answer.is_valid?) ? answer.text.gsub(/<\/?p>/, '').gsub(/<br\s?\/?>/, '').chomp.blank? : true %> <% if blank && @show_unanswered %> <%= " #{_("Question not answered.")}\n\n" %> <% elsif !blank %> @@ -48,7 +48,7 @@ <%= " #{opt.text}\n" %> <% end %> <% else %> -<%= " #{answer.text.gsub(/<\/?p>/, '').gsub(/<br\s?\/?>/, '').chomp}\n\n" %> +<%= " #{answer.text.gsub(/<\/?p>/, '').gsub(/<br\s?\/?>/, '').chomp}\n\n" if answer.text.present? %> <% end %> <% end %> <% end %> diff --git a/app/views/super_admin/orgs/index.html.erb b/app/views/super_admin/orgs/index.html.erb index 91d4aba..845c1b2 100644 --- a/app/views/super_admin/orgs/index.html.erb +++ b/app/views/super_admin/orgs/index.html.erb @@ -15,7 +15,8 @@ partial: '/paginable/orgs/index', controller: 'paginable/orgs', action: 'index', - scope: orgs) %> + scope: orgs, + query_params: { sort_field: 'orgs.name', sort_direction: :asc }) %> <div> </div> </div> \ No newline at end of file diff --git a/app/views/super_admin/themes/index.html.erb b/app/views/super_admin/themes/index.html.erb index b064d79..938a1e8 100644 --- a/app/views/super_admin/themes/index.html.erb +++ b/app/views/super_admin/themes/index.html.erb @@ -13,7 +13,8 @@ partial: '/paginable/themes/index', controller: 'paginable/themes', action: 'index', - scope: themes) + scope: themes, + query_params: { sort_field: 'themes.title', sort_direction: :asc }) %> </div> </div> diff --git a/app/views/usage/index.html.erb b/app/views/usage/index.html.erb index 26ef8ef..d80642d 100644 --- a/app/views/usage/index.html.erb +++ b/app/views/usage/index.html.erb @@ -1,6 +1,7 @@ <div class="row"> <div class="col-md-12"> <h1>Usage statistics</h1> + <p><%= _('Use the filters to generate organisational usage statistics for a custom date range. The graphs display new users and plans for your organisation over the past year. You can download a CSV report for each graph.') %></p> </div> </div> <div class="row"> @@ -62,7 +63,7 @@ <div class="panel panel-default"> <div class="panel-body"> <h4 style="margin-top:0px" style="display: none" data-topic="users"><%= _('New users') %></h4> - <h4 style="margin-top:0px" style="display: none" data-topic="plans"><%= _('Plans') %></h4> + <h4 style="margin-top:0px" style="display: none" data-topic="plans"><%= _('New plans') %></h4> <strong data-range style="font-size: 36px;"></strong> </div> </div> diff --git a/app/views/user_mailer/feedback_complete.html.erb b/app/views/user_mailer/feedback_complete.html.erb index d86c1df..2d7a663 100644 --- a/app/views/user_mailer/feedback_complete.html.erb +++ b/app/views/user_mailer/feedback_complete.html.erb @@ -6,6 +6,6 @@ %> <p><%= _('Hello %{recipient_name}') % { recipient_name: recipient_name } %></p> -<p><%= _('%{commenter} has finished providing feedback on the plan “%{plan_title}”. To view the comments, please visit the My Dashboard page in %{tool_name} and open your plan.') % { commenter: requestor, plan_title: plan_name, tool_name: tool_name } %></p> +<p><%= _('%{commenter} has finished providing feedback on the plan "%{plan_title}". To view the comments, please visit the My Dashboard page in %{tool_name} and open your plan.') % { commenter: requestor, plan_title: plan_name, tool_name: tool_name } %></p> <%= render partial: 'email_signature' %> \ No newline at end of file diff --git a/app/views/users/admin_index.html.erb b/app/views/users/admin_index.html.erb index 33e7914..9fdc1b1 100644 --- a/app/views/users/admin_index.html.erb +++ b/app/views/users/admin_index.html.erb @@ -14,7 +14,8 @@ partial: '/paginable/users/index', controller: 'paginable/users', action: 'index', - scope: @users, locals: { page: 1 }) %> + scope: @users, + locals: { page: 1 }) %> </div> </div> </div> \ No newline at end of file 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/environment.rb b/config/environment.rb index 3488440..a925c89 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,8 +1,5 @@ # Load the Rails application. require File.expand_path('../application', __FILE__) -#init a debugger -Rails.logger = Logger.new(STDOUT) - # Initialize the Rails application. -Rails.application.initialize! \ No newline at end of file +Rails.application.initialize! diff --git a/config/environments/production.rb b/config/environments/production.rb index 00b1214..02ea30f 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -61,8 +61,11 @@ # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify - # Use default logging formatter so that PID and timestamp are not suppressed. - config.log_formatter = ::Logger::Formatter.new + # Include working directory name in log for servers running multiple Rails instances. + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = ::Logger::Formatter.new + config.logger = ActiveSupport::TaggedLogging.new(logger) + config.log_tags = [ Rails.root.to_s.split('/').last ] # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false @@ -72,4 +75,4 @@ config.assets.debug = false config.assets.compile = false config.assets.quiet = true -end \ No newline at end of file +end 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 40a8cae..bb5acc9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -13,371 +13,416 @@ ActiveRecord::Schema.define(version: 20180212124444) do - # These are extensions that must be enabled in order to support this database - enable_extension "plpgsql" - create_table "annotations", force: :cascade do |t| - t.integer "question_id" - t.integer "org_id" - t.text "text" - t.integer "type", default: 0, null: false + t.integer "question_id", limit: 4 + t.integer "org_id", limit: 4 + t.text "text", limit: 65535 + t.integer "type", limit: 4, default: 0, null: false t.datetime "created_at" t.datetime "updated_at" end - add_index "annotations", ["question_id"], name: "index_annotations_on_question_id", using: :btree + add_index "annotations", ["org_id"], name: "fk_rails_aca7521f72" + add_index "annotations", ["question_id"], name: "fk_rails_0e08e753b6" + add_index "annotations", ["question_id"], name: "index_annotations_on_question_id" create_table "answers", force: :cascade do |t| - t.text "text" - t.integer "plan_id" - t.integer "user_id" - t.integer "question_id" + t.text "text", limit: 65535 + t.integer "plan_id", limit: 4 + t.integer "user_id", limit: 4 + t.integer "question_id", limit: 4 t.datetime "created_at" t.datetime "updated_at" - t.integer "lock_version", default: 0 + t.integer "lock_version", limit: 4, default: 0 + t.string "label_id", limit: 255 end + add_index "answers", ["plan_id"], name: "fk_rails_84a6005a3e" + add_index "answers", ["question_id"], name: "fk_rails_3d5ed4418f" + add_index "answers", ["user_id"], name: "fk_rails_584be190c2" + create_table "answers_question_options", id: false, force: :cascade do |t| - t.integer "answer_id", null: false - t.integer "question_option_id", null: false + t.integer "answer_id", limit: 4, null: false + t.integer "question_option_id", limit: 4, null: false end - add_index "answers_question_options", ["answer_id"], name: "index_answers_question_options_on_answer_id", using: :btree + add_index "answers_question_options", ["answer_id"], name: "index_answers_question_options_on_answer_id" create_table "exported_plans", force: :cascade do |t| - t.integer "plan_id" - t.integer "user_id" - t.string "format" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "phase_id" + t.integer "plan_id", limit: 4 + t.integer "user_id", limit: 4 + t.string "format", limit: 255 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "phase_id", limit: 4 end create_table "file_types", force: :cascade do |t| - t.string "name" - t.string "icon_name" - t.integer "icon_size" - t.string "icon_location" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "name", limit: 255 + t.string "icon_name", limit: 255 + t.integer "icon_size", limit: 4 + t.string "icon_location", limit: 255 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "file_uploads", force: :cascade do |t| - t.string "name" - t.string "title" - t.text "description" - t.integer "size" + t.string "name", limit: 255 + t.string "title", limit: 255 + t.text "description", limit: 65535 + t.integer "size", limit: 4 t.boolean "published" - t.string "location" - t.integer "file_type_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "location", limit: 255 + t.integer "file_type_id", limit: 4 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "friendly_id_slugs", force: :cascade do |t| - t.string "slug", null: false - t.integer "sluggable_id", null: false + t.string "slug", limit: 255, null: false + t.integer "sluggable_id", limit: 4, null: false t.string "sluggable_type", limit: 40 t.datetime "created_at" end - add_index "friendly_id_slugs", ["slug", "sluggable_type"], name: "index_friendly_id_slugs_on_slug_and_sluggable_type", unique: true, using: :btree - add_index "friendly_id_slugs", ["sluggable_id"], name: "index_friendly_id_slugs_on_sluggable_id", using: :btree - add_index "friendly_id_slugs", ["sluggable_type"], name: "index_friendly_id_slugs_on_sluggable_type", using: :btree + add_index "friendly_id_slugs", ["slug", "sluggable_type"], name: "index_friendly_id_slugs_on_slug_and_sluggable_type", unique: true + add_index "friendly_id_slugs", ["sluggable_id"], name: "index_friendly_id_slugs_on_sluggable_id" + add_index "friendly_id_slugs", ["sluggable_type"], name: "index_friendly_id_slugs_on_sluggable_type" create_table "guidance_groups", force: :cascade do |t| - t.string "name" - t.integer "org_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "name", limit: 255 + t.integer "org_id", limit: 4 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.boolean "optional_subset" t.boolean "published" end - add_index "guidance_groups", ["org_id"], name: "index_guidance_groups_on_org_id", using: :btree + add_index "guidance_groups", ["org_id"], name: "fk_rails_819c1dbbc7" + add_index "guidance_groups", ["org_id"], name: "index_guidance_groups_on_org_id" create_table "guidances", force: :cascade do |t| - t.text "text" - t.integer "guidance_group_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "question_id" + t.text "text", limit: 65535 + t.integer "guidance_group_id", limit: 4 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "question_id", limit: 4 t.boolean "published" end - add_index "guidances", ["guidance_group_id"], name: "index_guidances_on_guidance_group_id", using: :btree + add_index "guidances", ["guidance_group_id"], name: "fk_rails_20d29da787" + add_index "guidances", ["guidance_group_id"], name: "index_guidances_on_guidance_group_id" create_table "identifier_schemes", force: :cascade do |t| - t.string "name" - t.string "description" + t.string "name", limit: 255 + t.string "description", limit: 255 t.boolean "active" t.datetime "created_at" t.datetime "updated_at" - t.text "logo_url" - t.text "user_landing_url" + t.text "logo_url", limit: 255 + t.text "user_landing_url", limit: 255 end create_table "languages", force: :cascade do |t| - t.string "abbreviation" - t.string "description" - t.string "name" + t.string "abbreviation", limit: 255 + t.string "description", limit: 255 + t.string "name", limit: 255 t.boolean "default_language" end create_table "notes", force: :cascade do |t| - t.integer "user_id" - t.text "text" + t.integer "user_id", limit: 4 + t.text "text", limit: 65535 t.boolean "archived" - t.integer "answer_id" - t.integer "archived_by" + t.integer "answer_id", limit: 4 + t.integer "archived_by", limit: 4 t.datetime "created_at" t.datetime "updated_at" end - add_index "notes", ["answer_id"], name: "index_notes_on_answer_id", using: :btree + add_index "notes", ["answer_id"], name: "fk_rails_907f8d48bf" + add_index "notes", ["answer_id"], name: "index_notes_on_answer_id" + add_index "notes", ["user_id"], name: "fk_rails_7f2323ad43" create_table "org_identifiers", force: :cascade do |t| - t.string "identifier" - t.string "attrs" + t.string "identifier", limit: 255 + t.string "attrs", limit: 255 t.datetime "created_at" t.datetime "updated_at" - t.integer "org_id" - t.integer "identifier_scheme_id" + t.integer "org_id", limit: 4 + t.integer "identifier_scheme_id", limit: 4 end + add_index "org_identifiers", ["identifier_scheme_id"], name: "fk_rails_189ad2e573" + add_index "org_identifiers", ["org_id"], name: "fk_rails_36323c0674" + create_table "org_token_permissions", force: :cascade do |t| - t.integer "org_id" - t.integer "token_permission_type_id" + t.integer "org_id", limit: 4 + t.integer "token_permission_type_id", limit: 4 t.datetime "created_at" t.datetime "updated_at" end - add_index "org_token_permissions", ["org_id"], name: "index_org_token_permissions_on_org_id", using: :btree + add_index "org_token_permissions", ["org_id"], name: "fk_rails_e1db1b22c5" + add_index "org_token_permissions", ["org_id"], name: "index_org_token_permissions_on_org_id" + add_index "org_token_permissions", ["token_permission_type_id"], name: "fk_rails_2aa265f538" create_table "orgs", force: :cascade do |t| - t.string "name" - t.string "abbreviation" - t.string "target_url" - t.string "wayfless_entity" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "parent_id" + t.string "name", limit: 255 + t.string "abbreviation", limit: 255 + t.string "target_url", limit: 255 + t.string "wayfless_entity", limit: 255 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "parent_id", limit: 4 t.boolean "is_other" - t.string "sort_name" - t.text "banner_text" - t.string "logo_file_name" - t.integer "region_id" - t.integer "language_id" - t.string "logo_uid" - t.string "logo_name" - t.string "contact_email" - t.integer "org_type", default: 0, null: false - t.text "links", default: "[]" - t.string "contact_name" - t.boolean "feedback_enabled", default: false - t.string "feedback_email_subject" - t.text "feedback_email_msg" + t.string "sort_name", limit: 255 + t.text "banner_text", limit: 65535 + t.string "logo_file_name", limit: 255 + t.integer "region_id", limit: 4 + t.integer "language_id", limit: 4 + t.string "logo_uid", limit: 255 + t.string "logo_name", limit: 255 + t.string "contact_email", limit: 255 + t.integer "org_type", limit: 4, default: 0, null: false + t.string "contact_name", limit: 255 + t.text "links", limit: 255, default: "[]" + t.boolean "feedback_enabled", default: false + t.string "feedback_email_subject", limit: 255 + t.text "feedback_email_msg", limit: 65535 end + add_index "orgs", ["language_id"], name: "fk_rails_5640112cab" + add_index "orgs", ["region_id"], name: "fk_rails_5a6adf6bab" + create_table "perms", force: :cascade do |t| - t.string "name" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "name", limit: 255 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end - add_index "perms", ["name"], name: "index_perms_on_name", using: :btree - add_index "perms", ["name"], name: "index_roles_on_name_and_resource_type_and_resource_id", using: :btree + add_index "perms", ["name"], name: "index_perms_on_name" + add_index "perms", ["name"], name: "index_roles_on_name_and_resource_type_and_resource_id" create_table "phases", force: :cascade do |t| - t.string "title" - t.text "description" - t.integer "number" - t.integer "template_id" + t.string "title", limit: 255 + t.text "description", limit: 65535 + t.integer "number", limit: 4 + t.integer "template_id", limit: 4 t.datetime "created_at" t.datetime "updated_at" - t.string "slug" + t.string "slug", limit: 255 t.boolean "modifiable" end - add_index "phases", ["template_id"], name: "index_phases_on_template_id", using: :btree + add_index "phases", ["template_id"], name: "index_phases_on_template_id" create_table "plans", force: :cascade do |t| - t.string "title" - t.integer "template_id" + t.string "title", limit: 255 + t.integer "template_id", limit: 4 t.datetime "created_at" t.datetime "updated_at" - t.string "slug" - t.string "grant_number" - t.string "identifier" - t.text "description" - t.string "principal_investigator" - t.string "principal_investigator_identifier" - t.string "data_contact" - t.string "funder_name" - t.integer "visibility", null: false - t.string "data_contact_email" - t.string "data_contact_phone" - t.string "principal_investigator_email" - t.string "principal_investigator_phone" - t.boolean "feedback_requested", default: false - t.boolean "complete", default: false + t.string "slug", limit: 255 + t.string "data_contact_phone", limit: 255 + t.string "grant_number", limit: 255 + t.string "identifier", limit: 255 + t.text "description", limit: 65535 + t.string "principal_investigator", limit: 255 + t.string "principal_investigator_identifier", limit: 255 + t.string "data_contact", limit: 255 + t.string "funder_name", limit: 255 + t.integer "visibility", limit: 4, null: false + t.string "data_contact_email", limit: 255 + t.string "principal_investigator_email", limit: 255 + t.string "principal_investigator_phone", limit: 255 + t.boolean "feedback_requested", default: false + t.boolean "complete", default: false end - add_index "plans", ["template_id"], name: "index_plans_on_template_id", using: :btree + add_index "plans", ["template_id"], name: "index_plans_on_template_id" create_table "plans_guidance_groups", force: :cascade do |t| - t.integer "guidance_group_id" - t.integer "plan_id" + t.integer "guidance_group_id", limit: 4 + t.integer "plan_id", limit: 4 end + add_index "plans_guidance_groups", ["guidance_group_id"], name: "fk_rails_ec1c5524d7" + add_index "plans_guidance_groups", ["plan_id"], name: "fk_rails_13d0671430" + create_table "prefs", force: :cascade do |t| - t.text "settings" - t.integer "user_id" + t.text "settings", limit: 65535 + t.integer "user_id", limit: 4 + end + + create_table "question_format_labels", id: false, force: :cascade do |t| + t.integer "id", limit: 4 + t.string "description", limit: 255 + t.integer "question_id", limit: 4 + t.integer "number", limit: 4 + t.datetime "created_at" + t.datetime "updated_at" end create_table "question_formats", force: :cascade do |t| - t.string "title" - t.text "description" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "option_based", default: false - t.integer "formattype", default: 0 + t.string "title", limit: 255 + t.text "description", limit: 65535 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "option_based", default: false + t.integer "formattype", limit: 4, default: 0 end create_table "question_options", force: :cascade do |t| - t.integer "question_id" - t.string "text" - t.integer "number" + t.integer "question_id", limit: 4 + t.string "text", limit: 255 + t.integer "number", limit: 4 t.boolean "is_default" t.datetime "created_at" t.datetime "updated_at" end - add_index "question_options", ["question_id"], name: "index_question_options_on_question_id", using: :btree + add_index "question_options", ["question_id"], name: "fk_rails_b9c5f61cf9" + add_index "question_options", ["question_id"], name: "index_question_options_on_question_id" create_table "questions", force: :cascade do |t| - t.text "text" - t.text "default_value" - t.integer "number" - t.integer "section_id" + t.text "text", limit: 65535 + t.text "default_value", limit: 65535 + t.integer "number", limit: 4 + t.integer "section_id", limit: 4 t.datetime "created_at" t.datetime "updated_at" - t.integer "question_format_id" - t.boolean "option_comment_display", default: true + t.integer "question_format_id", limit: 4 + t.boolean "option_comment_display", default: true t.boolean "modifiable" end - add_index "questions", ["section_id"], name: "index_questions_on_section_id", using: :btree + add_index "questions", ["question_format_id"], name: "fk_rails_4fbc38c8c7" + add_index "questions", ["section_id"], name: "index_questions_on_section_id" create_table "questions_themes", id: false, force: :cascade do |t| - t.integer "question_id", null: false - t.integer "theme_id", null: false + t.integer "question_id", limit: 4, null: false + t.integer "theme_id", limit: 4, null: false end - add_index "questions_themes", ["question_id"], name: "index_questions_themes_on_question_id", using: :btree + add_index "questions_themes", ["question_id"], name: "index_questions_themes_on_question_id" create_table "regions", force: :cascade do |t| - t.string "abbreviation" - t.string "description" - t.string "name" - t.integer "super_region_id" + t.string "abbreviation", limit: 255 + t.string "description", limit: 255 + t.string "name", limit: 255 + t.integer "super_region_id", limit: 4 end create_table "roles", force: :cascade do |t| - t.integer "user_id" - t.integer "plan_id" + t.integer "user_id", limit: 4 + t.integer "plan_id", limit: 4 t.datetime "created_at" t.datetime "updated_at" - t.integer "access", default: 0, null: false - t.boolean "active", default: true + t.integer "access", limit: 4, default: 0, null: false + t.boolean "active", default: true end - add_index "roles", ["plan_id"], name: "index_roles_on_plan_id", using: :btree - add_index "roles", ["user_id"], name: "index_roles_on_user_id", using: :btree + add_index "roles", ["plan_id"], name: "fk_rails_a1ce6c2772" + add_index "roles", ["plan_id"], name: "index_roles_on_plan_id" + add_index "roles", ["user_id"], name: "fk_rails_ab35d699f0" + add_index "roles", ["user_id"], name: "index_roles_on_user_id" + + create_table "sample_plans", id: false, force: :cascade do |t| + t.integer "id", limit: 4 + t.string "url", limit: 255 + t.string "label", limit: 255 + t.integer "template_id", limit: 4 + t.datetime "created_at" + t.datetime "updated_at" + end create_table "sections", force: :cascade do |t| - t.string "title" - t.text "description" - t.integer "number" + t.string "title", limit: 255 + t.text "description", limit: 65535 + t.integer "number", limit: 4 t.datetime "created_at" t.datetime "updated_at" t.boolean "published" - t.integer "phase_id" + t.integer "phase_id", limit: 4 t.boolean "modifiable" end - add_index "sections", ["phase_id"], name: "index_sections_on_phase_id", using: :btree + add_index "sections", ["phase_id"], name: "index_sections_on_phase_id" create_table "settings", force: :cascade do |t| - t.string "var", null: false - t.text "value" - t.integer "target_id", null: false - t.string "target_type", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "var", limit: 255, null: false + t.text "value", limit: 65535 + t.integer "target_id", limit: 4, null: false + t.string "target_type", limit: 255, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end - add_index "settings", ["target_type", "target_id", "var"], name: "index_settings_on_target_type_and_target_id_and_var", unique: true, using: :btree + add_index "settings", ["target_type", "target_id", "var"], name: "index_settings_on_target_type_and_target_id_and_var", unique: true create_table "splash_logs", force: :cascade do |t| - t.string "destination" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "destination", limit: 255 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "templates", force: :cascade do |t| - t.string "title" - t.text "description" + t.string "title", limit: 255 + t.text "description", limit: 65535 t.boolean "published" - t.integer "org_id" - t.string "locale" + t.integer "org_id", limit: 4 + t.string "locale", limit: 255 t.boolean "is_default" t.datetime "created_at" t.datetime "updated_at" - t.integer "version" - t.integer "visibility" - t.integer "customization_of" - t.integer "dmptemplate_id" + t.integer "version", limit: 4 + t.integer "visibility", limit: 4 + t.integer "customization_of", limit: 4 + t.integer "dmptemplate_id", limit: 4 t.boolean "migrated" - t.boolean "dirty", default: false - t.text "links", default: "{\"funder\":[], \"sample_plan\":[]}" + t.boolean "dirty", default: false + t.text "links", limit: 255, default: "{\"funder\":[], \"sample_plan\":[]}" end - add_index "templates", ["org_id", "dmptemplate_id"], name: "template_organisation_dmptemplate_index", using: :btree - add_index "templates", ["org_id"], name: "index_templates_on_org_id", using: :btree + add_index "templates", ["org_id", "dmptemplate_id"], name: "template_organisation_dmptemplate_index" + add_index "templates", ["org_id"], name: "index_templates_on_org_id" create_table "themes", force: :cascade do |t| - t.string "title" - t.text "description" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "locale" + t.string "title", limit: 255 + t.text "description", limit: 65535 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "locale", limit: 255 end create_table "themes_in_guidance", id: false, force: :cascade do |t| - t.integer "theme_id" - t.integer "guidance_id" + t.integer "theme_id", limit: 4 + t.integer "guidance_id", limit: 4 end - add_index "themes_in_guidance", ["guidance_id"], name: "index_themes_in_guidance_on_guidance_id", using: :btree - add_index "themes_in_guidance", ["theme_id"], name: "index_themes_in_guidance_on_theme_id", using: :btree + add_index "themes_in_guidance", ["guidance_id"], name: "fk_rails_a5ab9402df" + add_index "themes_in_guidance", ["guidance_id"], name: "index_themes_in_guidance_on_guidance_id" + add_index "themes_in_guidance", ["theme_id"], name: "fk_rails_7d708f6f1e" + add_index "themes_in_guidance", ["theme_id"], name: "index_themes_in_guidance_on_theme_id" create_table "token_permission_types", force: :cascade do |t| - t.string "token_type" - t.text "text_description" + t.string "token_type", limit: 255 + t.text "text_description", limit: 65535 t.datetime "created_at" t.datetime "updated_at" end create_table "user_identifiers", force: :cascade do |t| - t.string "identifier" + t.string "identifier", limit: 255 t.datetime "created_at" t.datetime "updated_at" - t.integer "user_id" - t.integer "identifier_scheme_id" + t.integer "user_id", limit: 4 + t.integer "identifier_scheme_id", limit: 4 end - add_index "user_identifiers", ["user_id"], name: "index_user_identifiers_on_user_id", using: :btree + add_index "user_identifiers", ["identifier_scheme_id"], name: "fk_rails_fe95df7db0" + add_index "user_identifiers", ["user_id"], name: "fk_rails_65c9a98cdb" + add_index "user_identifiers", ["user_id"], name: "index_user_identifiers_on_user_id" create_table "users", force: :cascade do |t| t.string "firstname" @@ -385,80 +430,45 @@ t.string "email", default: "", null: false t.string "orcid_id" t.string "shibboleth_id" - t.datetime "created_at" - t.datetime "updated_at" + 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", default: 0 + t.integer "sign_in_count", limit: 4, default: 0 t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" - t.string "current_sign_in_ip" - t.string "last_sign_in_ip" - t.string "confirmation_token" + t.string "current_sign_in_ip", limit: 255 + t.string "last_sign_in_ip", limit: 255 + t.string "confirmation_token", limit: 255 t.datetime "confirmed_at" t.datetime "confirmation_sent_at" - t.string "invitation_token" + t.string "invitation_token", limit: 255 t.datetime "invitation_created_at" t.datetime "invitation_sent_at" t.datetime "invitation_accepted_at" - t.string "other_organisation" + t.string "other_organisation", limit: 255 t.boolean "accept_terms" - t.integer "org_id" - t.string "api_token" - t.integer "invited_by_id" - t.string "invited_by_type" - t.integer "language_id" - t.string "recovery_email" + t.integer "org_id", limit: 4 + t.string "api_token", limit: 255 + t.integer "invited_by_id", limit: 4 + t.string "invited_by_type", limit: 255 + t.integer "language_id", limit: 4 + t.string "recovery_email", limit: 255 end - add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree - add_index "users", ["org_id"], name: "index_users_on_org_id", using: :btree + add_index "users", ["email"], name: "index_users_on_email", unique: true + add_index "users", ["language_id"], name: "fk_rails_45f4f12508" + add_index "users", ["org_id"], name: "fk_rails_e73753bccb" + add_index "users", ["org_id"], name: "index_users_on_org_id" create_table "users_perms", id: false, force: :cascade do |t| - t.integer "user_id" - t.integer "perm_id" + t.integer "user_id", limit: 4 + t.integer "perm_id", limit: 4 end - add_index "users_perms", ["user_id"], name: "index_users_perms_on_user_id", using: :btree + add_index "users_perms", ["perm_id"], name: "fk_rails_457217c31c" + add_index "users_perms", ["user_id"], name: "index_users_perms_on_user_id" - add_foreign_key "annotations", "orgs" - add_foreign_key "annotations", "questions" - add_foreign_key "answers", "plans" - add_foreign_key "answers", "questions" - add_foreign_key "answers", "users" - add_foreign_key "answers_question_options", "answers" - add_foreign_key "answers_question_options", "question_options" - add_foreign_key "guidance_groups", "orgs" - add_foreign_key "guidances", "guidance_groups" - add_foreign_key "notes", "answers" - add_foreign_key "notes", "users" - add_foreign_key "org_identifiers", "identifier_schemes" - add_foreign_key "org_identifiers", "orgs" - add_foreign_key "org_token_permissions", "orgs" - add_foreign_key "org_token_permissions", "token_permission_types" - add_foreign_key "orgs", "languages" - add_foreign_key "orgs", "regions" - add_foreign_key "phases", "templates" - add_foreign_key "plans", "templates" - add_foreign_key "plans_guidance_groups", "guidance_groups" - add_foreign_key "plans_guidance_groups", "plans" - add_foreign_key "question_options", "questions" - add_foreign_key "questions", "question_formats" - add_foreign_key "questions", "sections" - add_foreign_key "questions_themes", "questions" - add_foreign_key "questions_themes", "themes" - add_foreign_key "roles", "plans" - add_foreign_key "roles", "users" - add_foreign_key "sections", "phases" - add_foreign_key "templates", "orgs" - add_foreign_key "themes_in_guidance", "guidances" - add_foreign_key "themes_in_guidance", "themes" - add_foreign_key "user_identifiers", "identifier_schemes" - add_foreign_key "user_identifiers", "users" - add_foreign_key "users", "languages" - add_foreign_key "users", "orgs" - add_foreign_key "users_perms", "perms" - add_foreign_key "users_perms", "users" end 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/constants.js b/lib/assets/javascripts/constants.js index 82109c3..9a953c6 100644 --- a/lib/assets/javascripts/constants.js +++ b/lib/assets/javascripts/constants.js @@ -1,33 +1,11 @@ -export const PASSWORD_MIN_LENGTH = 8; -export const PASSWORD_MAX_LENGTH = 128; +import { isObject, isString } from './utils/isType'; -// Maximum number of URLs allowed on the Org details page. These links appear at the -// top of the screen in the lower navigation row next to the Org Logo -export const MAX_NUMBER_ORG_URLS = 3; - -export const MAX_NUMBER_GUIDANCE_SELECTIONS = 6; - -export const VALIDATION_MESSAGE_DEFAULT = 'Please enter a valid value.'; -export const VALIDATION_MESSAGE_EMAIL = 'You must enter a valid email address.'; -export const VALIDATION_MESSAGE_URL = 'You must enter a valid URL (e.g. https://organisation.org).'; -export const VALIDATION_MESSAGE_NUMBER = 'Please enter a valid number.'; -export const VALIDATION_MESSAGE_PASSWORD = 'The password must be between 8 and 128 characters.'; -export const VALIDATION_MESSAGE_PASSWORDS_MATCH = 'The passwords must match.'; -export const VALIDATION_MESSAGE_RADIO = 'Please choose one of the options.'; -export const VALIDATION_MESSAGE_CHECKBOX = 'Please check the box to continue.'; -export const VALIDATION_MESSAGE_SELECT = 'Please select a value from the list.'; -export const VALIDATION_MESSAGE_TEXT = 'This field is required.'; - -export const SHOW_PASSWORD_MESSAGE = 'Show password'; -export const SHOW_SELECT_ORG_MESSAGE = 'Select an organisation from the list.'; -export const SHOW_OTHER_ORG_MESSAGE = 'My organisation isn\'t listed'; - -export const PLAN_VISIBILITY_WHEN_TEST = 'N/A'; -export const PLAN_VISIBILITY_WHEN_NOT_TEST = 'Private'; -export const PLAN_VISIBILITY_WHEN_NOT_TEST_TOOLTIP = 'Private: restricted to me and people I invite.'; - -export const SHIBBOLETH_DISCOVERY_SERVICE_HIDE_LIST = 'Hide list.'; -export const SHIBBOLETH_DISCOVERY_SERVICE_SHOW_LIST = 'See the full list of partner institutions.'; - -export const NO_TEMPLATE_FOUND_ERROR = 'Unable to find a suitable template for the research organisation and funder you selected.'; -export const NEW_PLAN_DISABLED_TOOLTIP = 'Please select a research organisation and funder to continue.'; +let constants = {}; +export default key => constants[key]; +$(() => { + // js-constants is defined in views/layouts/application.html.erb + const target = $('#js-constants'); + if (isObject(target) && isString(target.val())) { + constants = JSON.parse(target.val()); + } +}); diff --git a/lib/assets/javascripts/utils/ariatiseForm.js b/lib/assets/javascripts/utils/ariatiseForm.js index fda3c8f..0264863 100644 --- a/lib/assets/javascripts/utils/ariatiseForm.js +++ b/lib/assets/javascripts/utils/ariatiseForm.js @@ -32,7 +32,7 @@ */ import { Tinymce } from './tinymce'; import { isObject, isString, isBoolean } from './isType'; -import * as constants from '../constants'; +import getConstant from '../constants'; import * as validator from './isValidInputType'; const validatableFields = (selector) => { @@ -135,23 +135,23 @@ const getDefaultValidationMessage = (type) => { switch (type) { case 'text': - return constants.VALIDATION_MESSAGE_TEXT; + return getConstant('VALIDATION_MESSAGE_TEXT'); case 'textarea': - return constants.VALIDATION_MESSAGE_TEXT; + return getConstant('VALIDATION_MESSAGE_TEXT'); case 'number': - return constants.VALIDATION_MESSAGE_NUMBER; + return getConstant('VALIDATION_MESSAGE_NUMBER'); case 'email': - return constants.VALIDATION_MESSAGE_EMAIL; + return getConstant('VALIDATION_MESSAGE_EMAIL'); case 'password': - return constants.VALIDATION_MESSAGE_PASSWORD; + return getConstant('VALIDATION_MESSAGE_PASSWORD'); case 'radio': - return constants.VALIDATION_MESSAGE_RADIO; + return getConstant('VALIDATION_MESSAGE_RADIO'); case 'checkbox': - return constants.VALIDATION_MESSAGE_CHECKBOX; + return getConstant('VALIDATION_MESSAGE_CHECKBOX'); case 'js-combobox': - return constants.VALIDATION_MESSAGE_SELECT; + return getConstant('VALIDATION_MESSAGE_SELECT'); default: - return constants.VALIDATION_MESSAGE_DEFAULT; + return getConstant('VALIDATION_MESSAGE_DEFAULT'); } }; diff --git a/lib/assets/javascripts/utils/isValidInputType.js b/lib/assets/javascripts/utils/isValidInputType.js index e2edc4a..0a54d99 100644 --- a/lib/assets/javascripts/utils/isValidInputType.js +++ b/lib/assets/javascripts/utils/isValidInputType.js @@ -1,8 +1,5 @@ import { isString, isNumber } from './isType'; -import { - PASSWORD_MIN_LENGTH, - PASSWORD_MAX_LENGTH } from '../constants'; - +import getConstant from '../constants'; /* Validates whether or not the value passed matches to a valid email @param value String to search for a match @@ -10,7 +7,7 @@ */ export const isValidEmail = (value) => { if (isString(value)) { - return /[^@\s]+@(?:[-a-z0-9]+\.)+[a-z]{2,}$/.test(value); + return /[^@\s]+@(?:[-a-zA-Z0-9]+\.)+[a-zA-Z]{2,}$/.test(value); } return false; }; @@ -49,8 +46,8 @@ export const isValidPassword = (value) => { if (isString(value)) { const trimmed = value.trim(); - return trimmed.length >= PASSWORD_MIN_LENGTH && - trimmed.length <= PASSWORD_MAX_LENGTH; + return trimmed.length >= getConstant('PASSWORD_MIN_LENGTH') && + trimmed.length <= getConstant('PASSWORD_MAX_LENGTH'); } return false; }; diff --git a/lib/assets/javascripts/utils/passwordHelper.js b/lib/assets/javascripts/utils/passwordHelper.js index b2153ab..3ed80b0 100644 --- a/lib/assets/javascripts/utils/passwordHelper.js +++ b/lib/assets/javascripts/utils/passwordHelper.js @@ -1,10 +1,10 @@ -import { VALIDATION_MESSAGE_PASSWORDS_MATCH } from '../constants'; +import getConstant from '../constants'; import { isObject, isString, isArray } from './isType'; const getHelpBlock = (id) => { if (isString(id)) { return `<span id="${id}" class="help-block" style="display: none;"> - ${VALIDATION_MESSAGE_PASSWORDS_MATCH}</span>`; + ${getConstant('VALIDATION_MESSAGE_PASSWORDS_MATCH')}</span>`; } return ''; }; diff --git a/lib/assets/javascripts/utils/validation.js b/lib/assets/javascripts/utils/validation.js index 480517e..09ff999 100644 --- a/lib/assets/javascripts/utils/validation.js +++ b/lib/assets/javascripts/utils/validation.js @@ -1,6 +1,6 @@ import { Tinymce } from './tinymce'; import { isObject, isString, isBoolean } from './isType'; -import * as constants from '../constants'; +import getConstant from '../constants'; import * as validator from './isValidInputType'; const isValidatableField = ctx => $(ctx).attr('data-validatable') === 'true'; @@ -100,25 +100,25 @@ const getDefaultValidationMessage = (type) => { switch (type) { case 'text': - return constants.VALIDATION_MESSAGE_TEXT; + return getConstant('VALIDATION_MESSAGE_TEXT'); case 'textarea': - return constants.VALIDATION_MESSAGE_TEXT; + return getConstant('VALIDATION_MESSAGE_TEXT'); case 'number': - return constants.VALIDATION_MESSAGE_NUMBER; + return getConstant('VALIDATION_MESSAGE_NUMBER'); case 'email': - return constants.VALIDATION_MESSAGE_EMAIL; + return getConstant('VALIDATION_MESSAGE_EMAIL'); case 'url': - return constants.VALIDATION_MESSAGE_URL; + return getConstant('VALIDATION_MESSAGE_URL'); case 'password': - return constants.VALIDATION_MESSAGE_PASSWORD; + return getConstant('VALIDATION_MESSAGE_PASSWORD'); case 'radio': - return constants.VALIDATION_MESSAGE_RADIO; + return getConstant('VALIDATION_MESSAGE_RADIO'); case 'checkbox': - return constants.VALIDATION_MESSAGE_CHECKBOX; + return getConstant('VALIDATION_MESSAGE_CHECKBOX'); case 'js-combobox': - return constants.VALIDATION_MESSAGE_SELECT; + return getConstant('VALIDATION_MESSAGE_SELECT'); default: - return constants.VALIDATION_MESSAGE_DEFAULT; + return getConstant('VALIDATION_MESSAGE_DEFAULT'); } }; const getValidationMessage = (el) => { 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/orgs/shibboleth_ds.js b/lib/assets/javascripts/views/orgs/shibboleth_ds.js index f186b93..d1c7f56 100644 --- a/lib/assets/javascripts/views/orgs/shibboleth_ds.js +++ b/lib/assets/javascripts/views/orgs/shibboleth_ds.js @@ -1,9 +1,6 @@ import ariatiseForm from '../../utils/ariatiseForm'; import initAutoComplete from '../../utils/autoComplete'; -import { - SHIBBOLETH_DISCOVERY_SERVICE_HIDE_LIST, - SHIBBOLETH_DISCOVERY_SERVICE_SHOW_LIST, -} from '../../constants'; +import getConstant from '../../constants'; $(() => { initAutoComplete(); @@ -13,10 +10,10 @@ e.preventDefault(); if ($('#full_list').is('.hidden')) { $('#full_list').removeClass('hidden').attr('aria-hidden', 'false'); - $(e.currentTarget).html(SHIBBOLETH_DISCOVERY_SERVICE_HIDE_LIST); + $(e.currentTarget).html(getConstant('SHIBBOLETH_DISCOVERY_SERVICE_HIDE_LIST')); } else { $('#full_list').addClass('hidden').attr('aria-hidden', 'true'); - $(e.currentTarget).html(SHIBBOLETH_DISCOVERY_SERVICE_SHOW_LIST); + $(e.currentTarget).html(getConstant('SHIBBOLETH_DISCOVERY_SERVICE_SHOW_LIST')); } }); }); diff --git a/lib/assets/javascripts/views/plans/edit_details.js b/lib/assets/javascripts/views/plans/edit_details.js index f63eb51..3b6865e 100644 --- a/lib/assets/javascripts/views/plans/edit_details.js +++ b/lib/assets/javascripts/views/plans/edit_details.js @@ -1,6 +1,6 @@ import { Tinymce } from '../../utils/tinymce'; import ariatiseForm from '../../utils/ariatiseForm'; -import { MAX_NUMBER_GUIDANCE_SELECTIONS } from '../../constants'; +import getConstant from '../../constants'; $(() => { Tinymce.init(); @@ -22,30 +22,60 @@ }); showHideDataContact($('#show_data_contact')); - // Keep the modal window's guidance selections in line with selections on the main page - const syncGuidance = () => { - const choices = $('#priority-guidance-orgs, #other-guidance-orgs').find('input[type="checkbox"]:checked') - .map((i, el) => $(el).val()).get() - .filter((v, i, a) => a.indexOf(v) === i); - + // Toggle the disabled flags + const toggleCheckboxes = (selections) => { $('#priority-guidance-orgs, #other-guidance-orgs').find('input[type="checkbox"]').each((i, el) => { - const target = $(el); - if (choices.indexOf(target.val()) >= 0) { - target.attr('checked'); - } else { - target.removeAttr('checked'); - - // Disable the checkbox if it is unchecked and the user has already selected the max - if (choices.length >= MAX_NUMBER_GUIDANCE_SELECTIONS) { - target.attr('disabled', 'disabled'); + const checkbox = $(el); + if (selections.length >= getConstant('MAX_NUMBER_GUIDANCE_SELECTIONS')) { + if (checkbox.is(':checked')) { + checkbox.removeAttr('disabled'); } else { - target.removeAttr('disabled'); + checkbox.prop('disabled', true); } + } else { + checkbox.prop('disabled', false); } }); }; - $('#other-guidance-orgs').find('input[type="checkbox"]').click(syncGuidance); - $('#priority-guidance-orgs').find('input[type="checkbox"]').click(syncGuidance); + // Keep the modal window's guidance selections in line with selections on the main page + const syncGuidance = (ctx) => { + const currentList = $(ctx); + const otherList = (currentList.attr('id') === 'priority-guidance-orgs' ? $('#other-guidance-orgs') : $('#priority-guidance-orgs')); + const selections = currentList.find('input[type="checkbox"]:checked').map((i, el) => $(el).val()).get(); + otherList.find('input[type="checkbox"]').each((i, el) => { + const checkbox = $(el); + // Toggle the checked flag to match the current guidance list + if (selections.indexOf(checkbox.val()) >= 0) { + checkbox.prop('checked', true); + } else { + checkbox.prop('checked', false); + } + }); + toggleCheckboxes(selections); + }; + + $('#other-guidance-orgs').find('input[type="checkbox"]').click((e) => { + const checkbox = $(e.target); + // Since this is the modal window, copy any selections over to the priority list + if (checkbox.is(':checked')) { + const priorityList = $('#priority-guidance-orgs'); + if (priorityList.find(`input[value="${checkbox.val()}"]`).length <= 0) { + const li = checkbox.closest('li'); + // If its a subgroup copy the whole group otherwise just copy the line + if (li.children('.sublist').length > 0) { + priorityList.append(li.closest('ul').parent().clone()); + } else { + priorityList.append(li.clone()); + } + } + } + syncGuidance(checkbox.closest('ul[id]')); + }); + $('#priority-guidance-orgs').find('input[type="checkbox"]').click((e) => { + syncGuidance($(e.target).closest('ul[id]')); + }); + + toggleCheckboxes($('#priority-guidance-orgs input[type="checkbox"]:checked').map((i, el) => $(el).val()).get()); }); diff --git a/lib/assets/javascripts/views/plans/index.js b/lib/assets/javascripts/views/plans/index.js index b408954..b030b67 100644 --- a/lib/assets/javascripts/views/plans/index.js +++ b/lib/assets/javascripts/views/plans/index.js @@ -1,10 +1,5 @@ import * as notifier from '../../utils/notificationHelper'; - -import { - PLAN_VISIBILITY_WHEN_TEST, - PLAN_VISIBILITY_WHEN_NOT_TEST, - PLAN_VISIBILITY_WHEN_NOT_TEST_TOOLTIP, -} from '../../constants'; +import getConstant from '../../constants'; import { paginableSelector } from '../../utils/paginable'; $(() => { @@ -21,11 +16,11 @@ } if (form.find('input[type="checkbox"]').is(':checked')) { - form.parent().siblings('.plan-visibility').html(PLAN_VISIBILITY_WHEN_TEST) + form.parent().siblings('.plan-visibility').html(getConstant('PLAN_VISIBILITY_WHEN_TEST')) .attr('title', ''); } else { - form.parent().siblings('.plan-visibility').html(PLAN_VISIBILITY_WHEN_NOT_TEST) - .attr('title', PLAN_VISIBILITY_WHEN_NOT_TEST_TOOLTIP); + form.parent().siblings('.plan-visibility').html(getConstant('PLAN_VISIBILITY_WHEN_NOT_TEST')) + .attr('title', getConstant('PLAN_VISIBILITY_WHEN_NOT_TEST_TOOLTIP')); } }); $(paginableSelector).on('ajax:error', '.set_test_plan', () => { diff --git a/lib/assets/javascripts/views/plans/new.js b/lib/assets/javascripts/views/plans/new.js index 6c2d066..39e1518 100644 --- a/lib/assets/javascripts/views/plans/new.js +++ b/lib/assets/javascripts/views/plans/new.js @@ -1,9 +1,9 @@ import debounce from '../../utils/debounce'; import ariatiseForm from '../../utils/ariatiseForm'; import initAutoComplete from '../../utils/autoComplete'; +import getConstant from '../../constants'; import { isObject, isArray, isString } from '../../utils/isType'; import { isValidText } from '../../utils/isValidInputType'; -import { NO_TEMPLATE_FOUND_ERROR, NEW_PLAN_DISABLED_TOOLTIP } from '../../constants'; import { renderAlert, hideNotifications } from '../../utils/notificationHelper'; $(() => { @@ -14,13 +14,13 @@ .removeAttr('data-toggle').removeAttr('title'); } else { $('#new_plan button[type="submit"]').attr('disabled', true) - .attr('data-toggle', 'tooltip').attr('title', NEW_PLAN_DISABLED_TOOLTIP); + .attr('data-toggle', 'tooltip').attr('title', getConstant('NEW_PLAN_DISABLED_TOOLTIP')); } }; // AJAX error function for available template search const error = () => { - renderAlert(NO_TEMPLATE_FOUND_ERROR); + renderAlert(getConstant('NO_TEMPLATE_FOUND_ERROR')); }; // AJAX success function for available template search 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/javascripts/views/shared/sign_in_form.js b/lib/assets/javascripts/views/shared/sign_in_form.js index f265b67..9a954db 100644 --- a/lib/assets/javascripts/views/shared/sign_in_form.js +++ b/lib/assets/javascripts/views/shared/sign_in_form.js @@ -1,32 +1,37 @@ import * as Cookies from 'js-cookie'; import ariatiseForm from '../../utils/ariatiseForm'; -import { togglisePasswords } from '../../utils/passwordHelper'; +import { isValidText } from '../../utils/isValidInputType'; $(() => { - ariatiseForm({ selector: '#sign_in_form' }); - togglisePasswords({ selector: '#sign_in_form' }); - - const email = Cookies.get('dmproadmap_email'); - - // If the user's email was stored in the browser's cookies the pre-populate the field - if (email && email !== '') { - $('#sign_in_form #remember_email').attr('checked', 'checked'); - $('#sign_in_form #user_email').val(email); - } - - // When the user checks the 'remember email' box store the value in the browser storage - $('#sign_in_form #remember_email').click((e) => { - if ($(e.currentTarget).is(':checked')) { - Cookies.set('dmproadmap_email', $('#sign_in_form #user_email').val(), { expires: 14 }); + // Constants and methods + const rememberEmail = $('#sign_in_form input[name="remember_email"]'); + const userEmail = $('#sign_in_form input[name="user[email]"]'); + const emailCookieKey = 'dmproadmap_email'; + const getEmailCookie = () => Cookies.get(emailCookieKey); + const setEmailCookie = (value = null) => { + if (value === null) { + Cookies.remove(emailCookieKey); } else { - Cookies.remove('dmproadmap_email'); + Cookies.set(emailCookieKey, value, { expires: 14 }); + } + }; + // Event handlers + rememberEmail.click(() => { + if (rememberEmail.is(':checked')) { + setEmailCookie(userEmail.val()); + } else { + setEmailCookie(null); } }); - - // If the email is changed and the user has asked to remember it update the browser storage - $('#sign_in_form #user_email').change((e) => { - if ($('#sign_in_form #remember_email').is(':checked')) { - Cookies.set('dmproadmap_email', $(e.currentTarget).val(), { expires: 14 }); + userEmail.change(() => { + if (rememberEmail.is(':checked')) { + setEmailCookie(userEmail.val()); } }); + // Initialisation + if (isValidText(getEmailCookie())) { + rememberEmail.attr('checked', 'checked'); + userEmail.val(getEmailCookie()); + } + ariatiseForm({ selector: '#sign_in_form' }); }); 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/bugfix.rake b/lib/tasks/bugfix.rake index 67ba32d..090407b 100644 --- a/lib/tasks/bugfix.rake +++ b/lib/tasks/bugfix.rake @@ -90,18 +90,21 @@ desc "Allow Statistics API Usage for Org Admin Users" task stats_api_org_admin: :environment do - perms = Perm.where(name: ['modify_templates','modify_guidance','change_org_details','grant_permissions']).include(users: {org: :token_permission_types}) - users = perms.map {|perm| perm.users} - users.flatten!.uniq! - orgs = users.map {|user| user.org} - orgs.uniq! - # ensure orgs have access to statistics controller + Rake::Task['bugfix:add_missing_token_permission_types'].execute + orgs = Org.where(is_other: nil) orgs.each do |org| org.grant_api!(TokenPermissionType::STATISTICS) end - # leave tokens intact + users = User.joins(:perms).where("org_id IN (?) AND (api_token IS NULL OR api_token = '')", orgs.collect(&:id)) users.each do |user| - user.keep_or_generate_token! + if user.can_org_admin? + # Generate the tokens directly instead of via the User.keep_or_generate_token! method so that we do not spam users!! + user.api_token = loop do + random_token = SecureRandom.urlsafe_base64(nil, false) + break random_token unless User.exists?(api_token: random_token) + end + user.save! + end end end end 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 diff --git a/test/functional/annotations_controller_test.rb b/test/functional/annotations_controller_test.rb index 1a02d74..f8ea039 100644 --- a/test/functional/annotations_controller_test.rb +++ b/test/functional/annotations_controller_test.rb @@ -30,7 +30,7 @@ sign_in @user put admin_update_annotation_path(id: @question.section.phase.id), @create_hash assert_response :redirect - assert_redirected_to "#{admin_show_phase_path(@question.section.phase.id)}?section_id=#{@question.section.id}" + assert_redirected_to "#{admin_show_phase_path(@question.section.phase.id)}?section_id=#{@question.section.id}&r=all-templates" assert flash[:notice].start_with?('Successfully') && flash[:notice].include?('example answer') && flash[:notice].include?('guidance') assert_equal 'New example', Annotation.find_by(@example_answer_qry).text, "expected example answer to have been created." assert_equal 'New guidance', Annotation.find_by(@guidance_qry).text, "expected guidance to have been created." @@ -40,7 +40,7 @@ put admin_update_annotation_path(id: @question.section.phase.id), {question_id: @question.id, example_answer_text: "New example"} assert flash[:notice].start_with?('Successfully') && flash[:notice].include?('updated') assert_response :redirect - assert_redirected_to "#{admin_show_phase_path(@question.section.phase.id)}?section_id=#{@question.section.id}" + assert_redirected_to "#{admin_show_phase_path(@question.section.phase.id)}?section_id=#{@question.section.id}&r=all-templates" assert_equal 'New example', Annotation.find_by(@example_answer_qry).text, "expected example answer to have been created." assert Annotation.find_by(@guidance_qry).nil?, "expected no guidance to have been created." end @@ -49,7 +49,7 @@ put admin_update_annotation_path(id: @question.section.phase.id), {question_id: @question.id, guidance_text: "New guidance"} assert flash[:notice].start_with?('Successfully') && flash[:notice].include?('updated') assert_response :redirect - assert_redirected_to "#{admin_show_phase_path(@question.section.phase.id)}?section_id=#{@question.section.id}" + assert_redirected_to "#{admin_show_phase_path(@question.section.phase.id)}?section_id=#{@question.section.id}&r=all-templates" assert Annotation.find_by(@example_answer_qry).nil?, "expected no example answer to have been created." assert_equal 'New guidance', Annotation.find_by(@guidance_qry).text, "expected guidance to have been created." end @@ -60,7 +60,7 @@ put admin_update_annotation_path(id: @question.section.phase.id), {question_id: @question.id, example_answer_text: "Updated example", guidance_text: "Updated guidance"} assert flash[:notice].start_with?('Successfully') && flash[:notice].include?('updated') assert_response :redirect - assert_redirected_to "#{admin_show_phase_path(@question.section.phase.id)}?section_id=#{@question.section.id}" + assert_redirected_to "#{admin_show_phase_path(@question.section.phase.id)}?section_id=#{@question.section.id}&r=all-templates" assert_equal 'Updated example', Annotation.find_by(@example_answer_qry).text, "expected example answer to have been updated." assert_equal 'Updated guidance', Annotation.find_by(@guidance_qry).text, "expected guidance to have been updated." end @@ -70,7 +70,7 @@ put admin_update_annotation_path(id: @question.section.phase.id), {question_id: @question.id, guidance_text: "Updated guidance"} assert flash[:notice].start_with?('Successfully') && flash[:notice].include?('updated') assert_response :redirect - assert_redirected_to "#{admin_show_phase_path(@question.section.phase.id)}?section_id=#{@question.section.id}" + assert_redirected_to "#{admin_show_phase_path(@question.section.phase.id)}?section_id=#{@question.section.id}&r=all-templates" assert Annotation.find_by(@example_answer_qry).nil?, "expected example answer to have been removed." assert_equal 'Updated guidance', Annotation.find_by(@guidance_qry).text, "expected guidance to have been updated." end @@ -80,7 +80,7 @@ put admin_update_annotation_path(id: @question.section.phase.id), {question_id: @question.id, example_answer_text: "Updated example"} assert flash[:notice].start_with?('Successfully') && flash[:notice].include?('updated') assert_response :redirect - assert_redirected_to "#{admin_show_phase_path(@question.section.phase.id)}?section_id=#{@question.section.id}" + assert_redirected_to "#{admin_show_phase_path(@question.section.phase.id)}?section_id=#{@question.section.id}&r=all-templates" assert_equal 'Updated example', Annotation.find_by(@example_answer_qry).text, "expected example answer to have been updated." assert Annotation.find_by(@guidance_qry).nil?, "expected guidance to have been removed." end @@ -91,7 +91,7 @@ delete admin_destroy_annotation_path(Annotation.find_by(@example_answer_qry)) assert flash[:notice].start_with?('Successfully') && flash[:notice].include?('deleted') assert_response :redirect - assert_redirected_to "#{admin_show_phase_path(@question.section.phase.id)}?section_id=#{@question.section.id}" + assert_redirected_to "#{admin_show_phase_path(@question.section.phase.id)}?section_id=#{@question.section.id}&r=all-templates" assert Annotation.find_by(@example_answer_qry).nil? assert_equal 'New guidance', Annotation.find_by(@guidance_qry).text, "expected guidance to have been unchanged." end diff --git a/test/integration/paginable_flows_test.rb b/test/integration/paginable_flows_test.rb index 1ec0ea9..50fa26a 100644 --- a/test/integration/paginable_flows_test.rb +++ b/test/integration/paginable_flows_test.rb @@ -36,9 +36,9 @@ # Fails if search form does not exists under paginable-search refute_empty(css_select('.paginable-search form')) # Fails if sort link for email does not exist - refute_empty(css_select('a[href$="1?search=User&sort_direction=ASC&sort_field=email"]')) + refute_empty(css_select('a[href$="ALL?search=User&sort_direction=ASC&sort_field=email"]')) # Fails if sort link for last_sign_in_at does not exist - refute_empty(css_select('a[href$="1?search=User&sort_direction=ASC&sort_field=last_sign_in_at"]')) + refute_empty(css_select('a[href$="ALL?search=User&sort_direction=ASC&sort_field=last_sign_in_at"]')) link_view_less_search_results = css_select('a[href$="/1?search=User"]').first refute_nil(link_view_less_search_results) @@ -77,9 +77,9 @@ # Fails if search form does not exists under paginable-search refute_empty(css_select('.paginable-search form')) # Fails if sort link for email does not exist - refute_empty(css_select('a[href$="1?sort_direction=ASC&sort_field=email"]')) + refute_empty(css_select('a[href$="ALL?sort_direction=ASC&sort_field=email"]')) # Fails if sort link for last_sign_in_at does not exist - refute_empty(css_select('a[href$="1?sort_direction=ASC&sort_field=last_sign_in_at"]')) + refute_empty(css_select('a[href$="ALL?sort_direction=ASC&sort_field=last_sign_in_at"]')) link = css_select('a[href$="/1"]').first # Fails if link ending with /1 does not exist diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 9817def..73d966f 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -326,5 +326,22 @@ language = Language.new(name: 'esperonto', abbreviation: 'zz') verify_belongs_to_relationship(@user, language) end - + test "after_save removes API token and its perms associated" do + previous_api_token = @user.api_token + @user.perms = [Perm.add_orgs, Perm.grant_permissions] + previous_perms = @user.perms.to_a + @user.org = Org.where.not(id: @user.org_id).first + @user.save + assert_not_equal(previous_api_token, @user.api_token) + assert_not_equal(previous_perms, @user.perms.to_a) + end + test "after_save does not remove API token and its perms associated if user can_change_org" do + previous_api_token = @user.api_token + @user.perms = [Perm.add_orgs, Perm.grant_permissions, Perm.change_affiliation] + previous_perms = @user.perms + @user.org = Org.where.not(id: @user.org_id).first + @user.save + assert_equal(previous_api_token, @user.api_token) + assert_equal(previous_perms, @user.perms) + end end