diff --git a/app/controllers/concerns/template_methods.rb b/app/controllers/concerns/template_methods.rb new file mode 100644 index 0000000..bebd824 --- /dev/null +++ b/app/controllers/concerns/template_methods.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# This module holds helper controller methods for controllers that deal with Templates +# +module TemplateMethods + + private + + def template_type(template) + template.customization_of.present? ? _("customisation") : _("template") + end + +end diff --git a/app/controllers/org_admin/phase_versions_controller.rb b/app/controllers/org_admin/phase_versions_controller.rb new file mode 100644 index 0000000..644dcf5 --- /dev/null +++ b/app/controllers/org_admin/phase_versions_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class OrgAdmin::PhaseVersionsController < ApplicationController + + include Versionable + + def create + @phase = Phase.find(params[:phase_id]) + authorize @phase, :create? + @new_phase = get_modifiable(@phase) + flash[:notice] = if @new_phase == @phase + "This template is already a draft" + else + "New version of Template created" + end + redirect_to org_admin_template_phase_url(@new_phase.template, @new_phase) + end + +end diff --git a/app/controllers/org_admin/phases_controller.rb b/app/controllers/org_admin/phases_controller.rb index 79a3f97..de20786 100644 --- a/app/controllers/org_admin/phases_controller.rb +++ b/app/controllers/org_admin/phases_controller.rb @@ -47,7 +47,8 @@ template: phase.template, phase: phase, prefix_section: phase.prefix_section, - sections: phase.sections.order(:number).select(:id, :title, :modifiable), + sections: phase.sections.order(:number) + .select(:id, :title, :modifiable, :phase_id), suffix_sections: phase.suffix_sections.order(:number), current_section: Section.find_by(id: params[:section], phase_id: phase.id) }) @@ -57,13 +58,10 @@ # preview a phase # GET /org_admin/phases/[:id]/preview def preview - phase = Phase.includes(:template).find(params[:id]) - authorize phase - render("/org_admin/phases/preview", - locals: { - template: phase.template, - phase: phase - }) + @phase = Phase.includes(:template).find(params[:id]) + authorize @phase + @template = @phase.template + @guidance_presenter = GuidancePresenter.new(Plan.new(template: @phase.template)) end # add a new phase to a passed template diff --git a/app/controllers/org_admin/sections_controller.rb b/app/controllers/org_admin/sections_controller.rb index d220adb..bb8d0b7 100644 --- a/app/controllers/org_admin/sections_controller.rb +++ b/app/controllers/org_admin/sections_controller.rb @@ -21,7 +21,7 @@ template: phase.template, phase: phase, prefix_section: phase.prefix_section, - sections: phase.sections, + sections: phase.sections.order(:number), suffix_sections: phase.suffix_sections, current_section: phase.sections.first, modifiable: edit, @@ -31,14 +31,12 @@ # GET /org_admin/templates/[:template_id]/phases/[:phase_id]/sections/[:id] def show - section = Section.find(params[:id]) - authorize section - section = Section.includes(questions: [:annotations, :question_options]) - .find(params[:id]) - render partial: "show", locals: { - template: Template.find(params[:template_id]), - section: section - } + @section = Section.find(params[:id]) + authorize @section + @section = Section.includes(questions: [:annotations, :question_options]) + .find(params[:id]) + @template = Template.find(params[:template_id]) + render partial: "show", locals: { template: @template, section: @section } end # GET /org_admin/templates/[:template_id]/phases/[:phase_id]/sections/[:id]/edit diff --git a/app/controllers/org_admin/template_copies_controller.rb b/app/controllers/org_admin/template_copies_controller.rb new file mode 100644 index 0000000..6933b02 --- /dev/null +++ b/app/controllers/org_admin/template_copies_controller.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class OrgAdmin::TemplateCopiesController < ApplicationController + + include TemplateMethods + + after_action :verify_authorized + + # POST /org_admin/templates/:id/copy (AJAX) + def create + @template = Template.find(params[:template_id]) + authorize @template, :copy? + begin + new_copy = @template.generate_copy!(current_user.org) + flash[:notice] = "#{template_type(@template).capitalize} was successfully copied." + redirect_to edit_org_admin_template_path(new_copy) + rescue StandardError => e + flash[:alert] = failed_create_error(@template, template_type(@template)) + if request.referrer.present? + redirect_to :back + else + redirect_to org_admin_templates_path + end + end + end + +end diff --git a/app/controllers/org_admin/template_customization_transfers_controller.rb b/app/controllers/org_admin/template_customization_transfers_controller.rb new file mode 100644 index 0000000..473c950 --- /dev/null +++ b/app/controllers/org_admin/template_customization_transfers_controller.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class OrgAdmin::TemplateCustomizationTransfersController < ApplicationController + + after_action :verify_authorized + + # POST /org_admin/templates/:id/transfer_customization + # the funder template's id is passed through here + def create + @template = Template.includes(:org).find(params[:template_id]) + authorize @template, :transfer_customization? + if @template.upgrade_customization? + begin + new_customization = @template.upgrade_customization! + redirect_to org_admin_template_path(new_customization) + rescue StandardError => e + flash[:alert] = _("Unable to transfer your customizations.") + if request.referrer.present? + redirect_to :back + else + redirect_to org_admin_templates_path + end + end + else + flash[:notice] = _("That template is no longer customizable.") + if request.referrer.present? + redirect_to :back + else + redirect_to org_admin_templates_path + end + end + end + +end diff --git a/app/controllers/org_admin/template_customizations_controller.rb b/app/controllers/org_admin/template_customizations_controller.rb new file mode 100644 index 0000000..f4e6f9d --- /dev/null +++ b/app/controllers/org_admin/template_customizations_controller.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class OrgAdmin::TemplateCustomizationsController < ApplicationController + + include Paginable + include Versionable + after_action :verify_authorized + + # POST /org_admin/templates/:id/customize + def create + @template = Template.find(params[:template_id]) + authorize(@template, :customize?) + if @template.customize?(current_user.org) + begin + @customisation = @template.customize!(current_user.org) + redirect_to org_admin_template_path(@customisation) + return + rescue ArgumentError => e + flash[:alert] = _("Unable to customize that template.") + end + else + flash[:notice] = _("That template is not customizable.") + end + redirect_to :back + end + +end diff --git a/app/controllers/org_admin/templates_controller.rb b/app/controllers/org_admin/templates_controller.rb index c9251e5..3a9e399 100644 --- a/app/controllers/org_admin/templates_controller.rb +++ b/app/controllers/org_admin/templates_controller.rb @@ -6,6 +6,8 @@ include Paginable include Versionable + include TemplateMethods + after_action :verify_authorized # The root version of index which returns all templates @@ -241,77 +243,6 @@ } end - # POST /org_admin/templates/:id/customize - def customize - template = Template.find(params[:id]) - authorize template - if template.customize?(current_user.org) - begin - customisation = template.customize!(current_user.org) - redirect_to org_admin_template_path(customisation) - rescue StandardError => e - flash[:alert] = _("Unable to customize that template.") - if request.referrer.present? - redirect_to request.referrer - else - redirect_to org_admin_templates_path - end - end - else - flash[:notice] = _("That template is not customizable.") - if request.referrer.present? - redirect_to request.referrer - else - redirect_to org_admin_templates_path - end - end - end - - # POST /org_admin/templates/:id/transfer_customization - # the funder template's id is passed through here - def transfer_customization - template = Template.includes(:org).find(params[:id]) - authorize template - if template.upgrade_customization? - begin - new_customization = template.upgrade_customization! - redirect_to org_admin_template_path(new_customization) - rescue StandardError => e - flash[:alert] = _("Unable to transfer your customizations.") - if request.referrer.present? - redirect_to request.referrer - else - redirect_to org_admin_templates_path - end - end - else - flash[:notice] = _("That template is no longer customizable.") - if request.referrer.present? - redirect_to request.referrer - else - redirect_to org_admin_templates_path - end - end - end - - # POST /org_admin/templates/:id/copy (AJAX) - def copy - template = Template.find(params[:id]) - authorize template - begin - new_copy = template.generate_copy!(current_user.org) - flash[:notice] = "#{template_type(template).capitalize} was successfully copied." - redirect_to edit_org_admin_template_path(new_copy) - rescue StandardError => e - flash[:alert] = failed_create_error(template, template_type(template)) - if request.referrer.present? - redirect_to request.referrer - else - redirect_to org_admin_templates_path - end - end - end - # PATCH /org_admin/templates/:id/publish (AJAX) def publish template = Template.find(params[:id]) @@ -412,10 +343,6 @@ params.require(:template).permit(:title, :description, :visibility, :links) end - def template_type(template) - template.customization_of.present? ? _("customisation") : _("template") - end - def get_referrer(template, referrer) return org_admin_templates_path unless referrer.present? if referrer.end_with?(new_org_admin_template_path) || diff --git a/app/controllers/plans_controller.rb b/app/controllers/plans_controller.rb index 9a756ea..6875978 100644 --- a/app/controllers/plans_controller.rb +++ b/app/controllers/plans_controller.rb @@ -24,13 +24,13 @@ @funders = Org.funder .joins(:templates) .where(templates: { published: true }).uniq.sort(&:name) - @orgs = (Org.organisation + Org.institution + Org.managing_orgs) - .flatten.uniq.sort { |x, y| x.name <=> y.name } + @orgs = (Org.organisation + Org.institution + Org.managing_orgs).flatten + .uniq.sort(&:name) # Get the current user's org @default_org = current_user.org if @orgs.include?(current_user.org) - if params[:test] + if params.key?(:test) flash[:notice] = "#{_('This is a')} #{_('test plan')}" end @is_test = params[:test] ||= false @@ -105,11 +105,13 @@ msg += " #{_('This plan is based on the default template.')}" elsif !@plan.template.customization_of.nil? + # rubocop:disable Metrics/LineLength # We used a customized version of the the funder template # rubocop:disable Metrics/LineLength msg += " #{_('This plan is based on the')} #{plan_params[:funder_name]}: '#{@plan.template.title}' #{_('template with customisations by the')} #{plan_params[:org_name]}" # rubocop:enable Metrics/LineLength else + # rubocop:disable Metrics/LineLength # We used the specified org's or funder's template # rubocop:disable Metrics/LineLength msg += " #{_('This plan is based on the')} #{@plan.template.org.name}: '#{@plan.template.title}' template." @@ -175,15 +177,13 @@ @all_ggs_grouped_by_org = @all_ggs_grouped_by_org.sort_by do |org, gg| (org.nil? ? "" : org.name) end - @selected_guidance_groups = @selected_guidance_groups.collect do |gg| - gg.id - end + @selected_guidance_groups = @selected_guidance_groups.ids + @based_on = if @plan.template.customization_of.nil? @plan.template else Template.where(family_id: @plan.template.customization_of).first end - respond_to :html end @@ -213,7 +213,6 @@ end @plan.guidance_groups = GuidanceGroup.where(id: guidance_group_ids) @plan.save - if @plan.update_attributes(attrs) format.html do redirect_to overview_plan_path(@plan), @@ -301,16 +300,12 @@ def export @plan = Plan.includes(:answers).find(params[:id]) authorize @plan - @selected_phase = @plan.phases.find(params[:phase_id]) - @show_coversheet = params[:export][:project_details].present? @show_sections_questions = params[:export][:question_headings].present? @show_unanswered = params[:export][:unanswered_questions].present? @show_custom_sections = params[:export][:custom_sections].present? - @public_plan = false - @hash = @plan.as_pdf(@show_coversheet) @formatting = params[:export][:formatting] || @plan.settings(:export).formatting file_name = @plan.title.gsub(/ /, "_") @@ -340,7 +335,6 @@ # rubocop:enable Metrics/BlockLength end - def duplicate plan = Plan.find(params[:id]) authorize plan @@ -379,10 +373,11 @@ end else # rubocop:disable Metrics/LineLength - render status: :forbidden, json: { - msg: _("Unable to change the plan's status since it is needed at least "\ - "%{percentage} percentage responded") % { percentage: Rails.application.config.default_plan_percentage_answered } } + msg: _("Unable to change the plan's status since it is needed at least %{percentage} percentage responded") % { + percentage: Rails.application.config.default_plan_percentage_answered + } + } # rubocop:enable Metrics/LineLength end else @@ -419,7 +414,9 @@ authorize plan render(:overview, locals: { plan: plan }) rescue ActiveRecord::RecordNotFound - flash[:alert] = _("There is no plan associated with id %{id}") % { id: params[:id] } + flash[:alert] = _("There is no plan associated with id %{id}") % { + id: params[:id] + } redirect_to(action: :index) end end @@ -498,7 +495,7 @@ readonly: readonly, guidance_groups: guidance_groups, answers: answers, - guidance_service: GuidanceService.new(plan) + guidance_presenter: GuidancePresenter.new(plan) }) end diff --git a/app/helpers/sections_helper.rb b/app/helpers/sections_helper.rb index 6b16842..e3bd8e9 100644 --- a/app/helpers/sections_helper.rb +++ b/app/helpers/sections_helper.rb @@ -1,4 +1,7 @@ +# frozen_string_literal: true + module SectionsHelper + # HREF attribute value for headers in the section partials. If the section # is modifiable, returns the section path, otherwise the edit section path. # @@ -18,4 +21,9 @@ id: section.id) end end + + def draggable_for_section?(section) + section.template.latest? && section.modifiable? + end + end diff --git a/app/models/plan.rb b/app/models/plan.rb index 414f186..511682e 100644 --- a/app/models/plan.rb +++ b/app/models/plan.rb @@ -290,15 +290,15 @@ firstname: user.org.contact_name) UserMailer.feedback_notification(contact, self, user).deliver_now end - true + return true else puts "save was false" - false + return false end rescue Exception => e Rails.logger.error e - false + return false end end end diff --git a/app/models/template.rb b/app/models/template.rb index b0ce36a..5822eb9 100644 --- a/app/models/template.rb +++ b/app/models/template.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # == Schema Information # # Table name: templates @@ -20,11 +22,10 @@ # # Indexes # -# index_templates_on_customization_of_and_version_and_org_id (customization_of,version,org_id) UNIQUE -# index_templates_on_family_id (family_id) -# index_templates_on_family_id_and_version (family_id,version) UNIQUE -# index_templates_on_org_id (org_id) -# template_organisation_dmptemplate_index (org_id,family_id) +# index_templates_on_family_id (family_id) +# index_templates_on_family_id_and_version (family_id,version) UNIQUE +# index_templates_on_org_id (org_id) +# template_organisation_dmptemplate_index (org_id,family_id) # # Foreign Keys # @@ -32,6 +33,7 @@ # class Template < ActiveRecord::Base + include GlobalHelpers include ValidationMessages include ValidationValues @@ -166,7 +168,7 @@ .pluck(:family_id) + [default.family_id] published(family_ids.flatten) - .where('visibility = :visibility OR is_default = :is_default', + .where("visibility = :visibility OR is_default = :is_default", visibility: visibilities[:publicly_visible], is_default: true) } @@ -194,7 +196,7 @@ enum visibility: %i[organisationally_visible publicly_visible] # defines the export setting for a template object - has_settings :export, class_name: 'Settings::Template' do |s| + has_settings :export, class_name: "Settings::Template" do |s| s.key :export, defaults: Settings::Template::DEFAULT_SETTINGS end @@ -226,7 +228,7 @@ elsif template.latest? && !template.generate_version? template else - raise _('A historical template cannot be retrieved for being modified') + raise _("A historical template cannot be retrieved for being modified") end end @@ -288,8 +290,9 @@ end end - # Returns whether or not this is the latest version of the current template's - # family + # Is this the latest version of the current Template's family? + # + # Returns Boolean def latest? id == Template.latest_version(family_id).pluck(:id).first end @@ -334,7 +337,8 @@ # Returns a new unpublished copy of self with a new family_id, version = zero # for the specified org def generate_copy!(org) - raise _('generate_copy! requires an organisation target') unless org.is_a?(Org) # Assume customizing_org is persisted + # Assume customizing_org is persisted + raise _("generate_copy! requires an organisation target") unless org.is_a?(Org) template = deep_copy( attributes: { version: 0, @@ -342,7 +346,7 @@ family_id: new_family_id, org: org, is_default: false, - title: format(_('Copy of %{template}'), template: title) + title: format(_("Copy of %{template}"), template: title) }, modifiable: true, save: true ) template @@ -350,7 +354,7 @@ # Generates a new copy of self with an incremented version number def generate_version! - raise _('generate_version! requires a published template') unless published + raise _("generate_version! requires a published template") unless published template = deep_copy( attributes: { version: version + 1, @@ -365,12 +369,12 @@ def customize!(customizing_org) # Assume customizing_org is persisted unless customizing_org.is_a?(Org) - raise _('customize! requires an organisation target') + raise ArgumentError, _("customize! requires an organisation target") end # Assume self has org associated if !org.funder_only? && !is_default - raise _('customize! requires a template from a funder') + raise ArgumentError, _("customize! requires a template from a funder") end customization = deep_copy( @@ -391,12 +395,14 @@ # template is customized_of def upgrade_customization! if customization_of.blank? - raise _('upgrade_customization! requires a customised template') + raise _("upgrade_customization! requires a customised template") end funder_template = Template.published(customization_of).first if funder_template.blank? + # rubocop:disable Metrics/LineLength raise _("upgrade_customization! cannot be carried out since there is no published template of its current funder") + # rubocop:enable Metrics/LineLength end # preserves modifiable flags from the self template copied @@ -421,6 +427,7 @@ # Merges modifiable sections or questions from source into customization # template object + # rubocop:disable Metrics/BlockLength customization.phases.each do |customization_phase| # Search for the phase in the source template whose number matches the # customization_phase @@ -436,7 +443,9 @@ modifiable_sections = candidate_phase.sections.select(&:modifiable) # Attaches modifiable sections into the customization_phase - modifiable_sections.each { |modifiable_section| customization_phase.sections << modifiable_section } + modifiable_sections.each do |modifiable_section| + customization_phase.sections << modifiable_section + end # Sorts the sections for the customization_phase sorted_sections = customization_phase.sections.sort_by(&:number) @@ -448,23 +457,38 @@ # Search for modifiable questions within the unmodifiable_section # from candidate_phase modifiable_questions = unmodifiable_section.questions.select(&:modifiable) - customization_section = sorted_sections.bsearch { |section| unmodifiable_section.number <=> section.number } + customization_section = sorted_sections.bsearch do |section| + unmodifiable_section.number <=> section.number + end # The funder could have deleted the section if customization_section.present? - modifiable_questions.each { |modifiable_question| customization_section.questions << modifiable_question; } + modifiable_questions.each do |modifiable_question| + customization_section.questions << modifiable_question + end end - # Search for unmodifiable questions within the unmodifiable_section in case source template added annotations + # Search for unmodifiable questions within the unmodifiable_section in case + # source template added annotations unmodifiable_questions = unmodifiable_section.questions.reject(&:modifiable) sorted_questions = customization_section.questions.sort_by(&:number) unmodifiable_questions.each do |unmodifiable_question| - customization_question = sorted_questions.bsearch { |question| unmodifiable_question.number <=> question.number } - if customization_question.present? # The funder could have deleted the question - annotations_added_by_customiser = unmodifiable_question.annotations.select { |annotation| annotation.org_id == source.org_id } - annotations_added_by_customiser.each { |annotation| customization_question.annotations << annotation } + customization_question = sorted_questions.bsearch do |question| + unmodifiable_question.number <=> question.number + end + # The funder could have deleted the question + if customization_question.present? + annotations_added_by_customiser = unmodifiable_question.annotations + .select do |annotation| + annotation.org_id == source.org_id + end + annotations_added_by_customiser.each do |annotation| + customization_question.annotations << annotation + end end end end end + # rubocop:enable Metrics/BlockLength + # Appends the modifiable phases from source source.phases.select(&:modifiable).each do |modifiable_phase| customization.phases << modifiable_phase @@ -493,7 +517,13 @@ self.archived ||= false self.is_default ||= false self.version ||= 0 - self.visibility = (org.present? && org.funder_only?) || is_default? ? Template.visibilities[:publicly_visible] : Template.visibilities[:organisationally_visible] if id.blank? + unless id? + self.visibility = if (org.present? && org.funder_only?) || is_default? + Template.visibilities[:publicly_visible] + else + Template.visibilities[:organisationally_visible] + end + end self.customization_of ||= nil self.family_id ||= new_family_id self.archived ||= false @@ -509,4 +539,5 @@ .where.not(id: id) .update_all(published: false) end + end diff --git a/app/presenters/guidance_presenter.rb b/app/presenters/guidance_presenter.rb new file mode 100644 index 0000000..1cc90a7 --- /dev/null +++ b/app/presenters/guidance_presenter.rb @@ -0,0 +1,229 @@ +# frozen_string_literal: true + +class GuidancePresenter + + attr_accessor :plan + attr_accessor :guidance_groups + + def initialize(plan) + @plan = plan + @guidance_groups = plan.guidance_groups.where(published: true) + end + + def any?(org: nil, question: nil) + if org.nil? + if question.present? + # check each annotation/guidance group for a response to this question + # Would be nice not to have to crawl the entire list each time we want to know + # this + anno = orgs.reduce(false) do |found, o| + found || guidance_annotations?(org: o, question: question) + end + if !anno + return orgs.reduce(anno) do |found, o| + found || guidance_groups_by_theme?(org: o, question: question) + end + else + return anno + end + else # question.nil? + return hashified_annotations? || hashified_guidance_groups? + end + end + guidance_annotations?(org: org, question: question) || + guidance_groups_by_theme?(org: org, question: question) + end + + # filters through the orgs with annotations and guidance groups to create a + # set of tabs with display names and any guidance/annotations to show + # + # question - The question to which guidance pretains + # + # Returns an array of tab hashes. These + def tablist(question) + # start with orgs + # filter into hash with annotation_presence, main_group presence, and + display_tabs = [] + orgs.each do |org| + annotations = guidance_annotations(org: org, question: question) + groups = guidance_groups_by_theme(org: org, question: question) + main_groups = groups.select { |group| group.optional_subset == false } + subsets = groups.reject { |group| group.optional_subset == false } + if annotations.present? || main_groups.present? # annotations and main group + # Tab with org.abbreviation + display_tabs << { name: org.abbreviation, groups: main_groups, + annotations: annotations } + end + if subsets.present? + subsets.each_pair do |group, theme| + display_tabs << { name: group.name.truncate(15), groups: { group => theme } } + end + end + end + display_tabs + end + + private + + # Returns an Array of orgs according to the guidance related to plan + # Note the Array is sorted in the following order: + # First funder org (if the template from the plan is a customization of another) + # Second template owner's org + # The orgs from every guidance group selected for this plan + def orgs + return @orgs if defined?(@orgs) + @orgs = [] + orgs_from_annotations.each { |org| @orgs << org unless org_found(@orgs, org) } + orgs_from_guidance_groups.each { |org| @orgs << org unless org_found(@orgs, org) } + @orgs + end + + def org_found(orgs, org) + orgs.find do |lookup_org| + lookup_org.id == org.id + end.present? + end + + # Returns true if exists any guidance_group applicable to the org and question passed + def guidance_groups_by_theme?(org: nil, question: nil) + return false unless question.respond_to?(:themes) + return false unless hashified_guidance_groups.has_key?(org) + result = guidance_groups_by_theme(org: org, question: question) + .detect do |gg, theme_hash| + if theme_hash.present? + theme_hash.detect { |theme, guidances| guidances.present? } + else + false + end + end + result.present? + end + + # Returns true if exists any annotation applicable to the org and question passed + def guidance_annotations?(org: nil, question: nil) + return false unless question.respond_to?(:id) + return false unless hashified_annotations.has_key?(org) + hashified_annotations[org].find do |annotation| + (annotation.question_id == question.id) && (annotation.type == "guidance") + end.present? + end + + # Returns a hash of guidance groups for an org and question passed with the following + # structure: + # { guidance_group: { theme: [guidance, ...], ... }, ... } + def guidance_groups_by_theme(org: nil, question: nil) + raise ArgumentError unless question.respond_to?(:themes) + question = Question.includes(:themes).find(question.id) + return {} unless hashified_guidance_groups.has_key?(org) + hashified_guidance_groups[org].each_key.reduce({}) do |acc, gg| + filtered_gg = hashified_guidance_groups[org][gg].each_key.reduce({}) do |acc, theme| + if question.themes.include?(theme) + acc[theme] = hashified_guidance_groups[org][gg][theme] + end + acc + end + acc[gg] = filtered_gg if filtered_gg.present? + acc + end + end + + # Returns a collection of annotations (type guidance) for an org and question passed + def guidance_annotations(org: nil, question: nil) + raise ArgumentError unless question.respond_to?(:id) + return [] unless hashified_annotations.has_key?(org) + hashified_annotations[org].select do |annotation| + (annotation.question_id == question.id) && (annotation.type == "guidance") + end + end + + def orgs_from_guidance_groups + return @orgs_from_guidance_groups if defined?(@orgs_from_guidance_groups) + @orgs_from_guidance_groups = Org.joins(:guidance_groups) + .where(guidance_groups: { id: guidance_groups.ids }) + .distinct("orgs.id") + @orgs_from_guidance_groups + end + + def orgs_from_annotations + return @orgs_from_annotations if defined?(@orgs_from_annotations) + @orgs_from_annotations = [] + if plan.template.customization_of.present? + family_id = plan.template.customization_of + @orgs_from_annotations << Template.find_by(family_id: family_id).org + end + @orgs_from_annotations << plan.template.org + @orgs_from_annotations + end + + def hashified_guidance_groups + @hashified_guidance_groups ||= hashify_guidance_groups + end + + def hashified_guidance_groups? + result = hashified_guidance_groups.detect do |org, gg_hash| + if gg_hash.present? + gg_hash.detect do |gg, theme_hash| + if theme_hash.present? + theme_hash.detect { |theme, guidances| guidances.present? } + else + false + end + end + else + false + end + end + result.present? + end + + def hashified_annotations + @hashified_annotations ||= hashify_annotations + end + + def hashified_annotations? + hashified_annotations.detect { |org, annotations| annotations.present? }.present? + end + + # Hashifies guidance groups for a plan according to the distinct orgs into the + # following structure: + # { org: { guidance_group: { theme: [guidance, ...], ... }, ... }, ... } + def hashify_guidance_groups + hashified_guidances = hashify_guidances + orgs_from_guidance_groups.reduce({}) do |acc, org| + org_guidance_groups = hashified_guidances.each_key.select do |gg| + gg.org_id == org.id + end + acc[org] = org_guidance_groups.reduce({}) do |acc, gg| + acc[gg] = hashified_guidances[gg] + acc + end + acc + end + end + + # Hashifies guidances from a collection of guidance_groups passed into the following + # structure: + # { guidance_group: { theme: [guidance, ...], ... }, ... } + def hashify_guidances + guidance_groups.reduce({}) do |acc, gg| + themes = Theme.includes(:guidances) + .joins(:guidances) + .merge(Guidance.where(guidance_group_id: gg.id, published: true)) + acc[gg] = themes.reduce({}) do |acc, theme| + acc[theme] = theme.guidances + acc + end + acc + end + end + + def hashify_annotations + orgs_from_annotations.reduce({}) do |acc, org| + annotations = Annotation.where(org_id: org.id, + question_id: plan.template.question_ids) + acc[org] = annotations.select { |annotation| annotation.org_id = org.id } + acc + end + end + +end diff --git a/app/services/guidance_service.rb b/app/services/guidance_service.rb deleted file mode 100644 index 947c4ed..0000000 --- a/app/services/guidance_service.rb +++ /dev/null @@ -1,183 +0,0 @@ -class GuidanceService - attr_accessor :plan - attr_accessor :guidance_groups - - def initialize(plan) - @plan = plan - @guidance_groups = plan.guidance_groups.where(published: true) - end - - # Returns an Array of orgs according to the guidance related to plan - # Note the Array is sorted in the following order: - # First funder org (if the template from the plan is a customization of another) - # Second template owner's org - # The orgs from every guidance group selected for this plan - def orgs - if !defined?(@orgs) - @orgs = [] - org_found = lambda{ |orgs, org| return orgs.find{ |lookup_org| lookup_org.id == org.id }.present? } - orgs_from_annotations.each{ |org| @orgs << org unless org_found.call(@orgs, org) } - orgs_from_guidance_groups.each{ |org| @orgs << org unless org_found.call(@orgs, org) } - end - return @orgs - end - - def any?(org:nil, question:nil) - if org.nil? - if question.present? - # check each annotation/guidance group for a response to this question - # Would be nice not to have to crawl the entire list each time we want to know this - anno = orgs.reduce(false) {|found, o| found || guidance_annotations?(org: o, question: question)} - if !anno - return orgs.reduce(anno) {|found, o| found || guidance_groups_by_theme?(org: o, question: question)} - else - return anno - end - else # question.nil? - return hashified_annotations? || hashified_guidance_groups? - end - end - return guidance_annotations?(org: org, question: question) || - guidance_groups_by_theme?(org: org, question: question) - end - # Returns true if exists any guidance_group applicable to the org and question passed - def guidance_groups_by_theme?(org: nil, question: nil) - return false unless question.respond_to?(:themes) - return false unless hashified_guidance_groups.has_key?(org) - result = guidance_groups_by_theme(org: org, question: question).detect do |gg, theme_hash| - if theme_hash.present? - theme_hash.detect{ |theme, guidances| guidances.present? } - else - false - end - end - return result.present? - end - # Returns true if exists any annotation applicable to the org and question passed - def guidance_annotations?(org: nil, question: nil) - return false unless question.respond_to?(:id) - return false unless hashified_annotations.has_key?(org) - return hashified_annotations[org].find{ |annotation| (annotation.question_id == question.id) && (annotation.type == "guidance")}.present? - end - # Returns a hash of guidance groups for an org and question passed with the following structure: - # { guidance_group: { theme: [guidance, ...], ... }, ... } - def guidance_groups_by_theme(org: nil, question: nil) - raise ArgumentError unless question.respond_to?(:themes) - question = Question.includes(:themes).find(question.id) - return {} unless hashified_guidance_groups.has_key?(org) - return hashified_guidance_groups[org].each_key.reduce({}) do |acc, gg| - filtered_gg = hashified_guidance_groups[org][gg].each_key.reduce({}) do |acc, theme| - acc[theme] = hashified_guidance_groups[org][gg][theme] if question.themes.include?(theme) - acc - end - acc[gg] = filtered_gg if filtered_gg.present? - acc - end - end - # Returns a collection of annotations (type guidance) for an org and question passed - def guidance_annotations(org: nil, question: nil) - raise ArgumentError unless question.respond_to?(:id) - return [] unless hashified_annotations.has_key?(org) - return hashified_annotations[org].select{ |annotation| (annotation.question_id == question.id) && (annotation.type == "guidance")} - end - - - # filters through the orgs with annotations and guidance groups to create a - # set of tabs with display names and any guidance/annotations to show - # - # question - The question to which guidance pretains - # - # Returns an array of tab hashes. These - def tablist(question) - # start with orgs - # filter into hash with annotation_presence, main_group presence, and - display_tabs = [] - orgs.each do |org| - annotations = guidance_annotations(org: org,question: question) - groups = guidance_groups_by_theme(org: org,question: question) - main_groups = groups.select{|group| group.optional_subset == false} - subsets = groups.reject{|group| group.optional_subset == false} - if annotations.present? || main_groups.present? # annotations and main group - # Tab with org.abbreviation - display_tabs << {name: org.abbreviation, groups: main_groups, - annotations: annotations} - end - if subsets.present? - subsets.each_pair do |group,theme| - display_tabs << {name: group.name.truncate(15), groups:{group => theme}} - end - end - end - return display_tabs - end - - private - def orgs_from_guidance_groups - if !defined?(@orgs_from_guidance_groups) - @orgs_from_guidance_groups = Org.joins(:guidance_groups).where("guidance_groups.id": guidance_groups.map(&:id)).distinct("orgs.id") - end - return @orgs_from_guidance_groups - end - def orgs_from_annotations - if !defined?(@orgs_from_annotations) - @orgs_from_annotations = [] - @orgs_from_annotations << Template.find_by(family_id: plan.template.customization_of).org if plan.template.customization_of.present? - @orgs_from_annotations << plan.template.org - end - return @orgs_from_annotations - end - def hashified_guidance_groups - @hashified_guidance_groups ||= hashify_guidance_groups - end - def hashified_guidance_groups? - result = hashified_guidance_groups.detect do |org, gg_hash| - if gg_hash.present? - gg_hash.detect do |gg, theme_hash| - if theme_hash.present? - theme_hash.detect{ |theme, guidances| guidances.present? } - else - false - end - end - else - false - end - end - return result.present? - end - def hashified_annotations - @hashified_annotations ||= hashify_annotations - end - def hashified_annotations? - return hashified_annotations.detect{ |org, annotations| annotations.present? }.present? - end - # Hashifies guidance groups for a plan according to the distinct orgs into the following structure: - # { org: { guidance_group: { theme: [guidance, ...], ... }, ... }, ... } - def hashify_guidance_groups - hashified_guidances = hashify_guidances - return orgs_from_guidance_groups.reduce({}) do |acc, org| - org_guidance_groups = hashified_guidances.each_key.select{ |gg| gg.org_id == org.id } - acc[org] = org_guidance_groups.reduce({}){ |acc, gg| acc[gg] = hashified_guidances[gg]; acc } - acc - end - end - # Hashifies guidances from a collection of guidance_groups passed into the following structure: - # { guidance_group: { theme: [guidance, ...], ... }, ... } - def hashify_guidances - return guidance_groups.reduce({}) do |acc, gg| - themes = Theme.includes(:guidances).joins(:guidances).merge(Guidance.where(guidance_group_id: gg.id, published: true)) - acc[gg] = themes.reduce({}) do |acc, theme| - acc[theme] = theme.guidances - acc - end - acc - end - end - def hashify_annotations - return orgs_from_annotations.reduce({}) do |acc, org| - annotations = Annotation.where(org_id: org.id, question_id: plan.template.question_ids) - acc[org] = annotations.select{ |annotation| annotation.org_id = org.id } - acc - end - end -end diff --git a/app/views/org_admin/phases/_phase.html.erb b/app/views/org_admin/phases/_phase.html.erb index 26fa056..37736ac 100644 --- a/app/views/org_admin/phases/_phase.html.erb +++ b/app/views/org_admin/phases/_phase.html.erb @@ -26,7 +26,7 @@