diff --git a/.github/workflows/mysql.yml b/.github/workflows/mysql.yml index 11d2a19bd6..740bd53d1f 100644 --- a/.github/workflows/mysql.yml +++ b/.github/workflows/mysql.yml @@ -42,7 +42,7 @@ jobs: # Try to retrieve the gems from the cache - name: 'Cache Gems' - uses: actions/cache@v1 + uses: actions/cache@v2.1.5 with: path: vendor/bundle key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} @@ -62,7 +62,7 @@ jobs: # Try to retrieve the yarn JS dependencies from the cache - name: 'Cache Yarn Packages' - uses: actions/cache@v1 + uses: actions/cache@v2.1.5 with: path: node_modules/ key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} diff --git a/.github/workflows/postgres.yml b/.github/workflows/postgres.yml index 29454be8a5..5681a97364 100644 --- a/.github/workflows/postgres.yml +++ b/.github/workflows/postgres.yml @@ -59,7 +59,7 @@ jobs: # Try to retrieve the gems from the cache - name: 'Cache Gems' - uses: actions/cache@v1 + uses: actions/cache@v2.1.5 with: path: vendor/bundle key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} @@ -79,7 +79,7 @@ jobs: # Try to retrieve the yarn JS dependencies from the cache - name: 'Cache Yarn Packages' - uses: actions/cache@v1 + uses: actions/cache@v2.1.5 with: path: node_modules/ key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} diff --git a/app/assets/images/spinner.gif b/app/assets/images/spinner.gif new file mode 100644 index 0000000000..0b3ba6284d Binary files /dev/null and b/app/assets/images/spinner.gif differ diff --git a/app/assets/stylesheets/blocks/_spinner.scss b/app/assets/stylesheets/blocks/_spinner.scss new file mode 100644 index 0000000000..a812084c90 --- /dev/null +++ b/app/assets/stylesheets/blocks/_spinner.scss @@ -0,0 +1,10 @@ +.spinner-border { + position: fixed; + top: 48%; + left: 43%; +} + +.spinner-border img { + height: 80px; + width: 80px; +} \ No newline at end of file diff --git a/app/controllers/api/v0/plans_controller.rb b/app/controllers/api/v0/plans_controller.rb index 6841e1592a..5e30069bda 100644 --- a/app/controllers/api/v0/plans_controller.rb +++ b/app/controllers/api/v0/plans_controller.rb @@ -67,7 +67,9 @@ def index params[:per_page] = max_pages if params[:per_page].to_i > max_pages end - @plans = @user.org.plans.includes([{ roles: :user }, { answers: :question_options }, + # Get all the Org Admin plans + org_admin_plans = @user.org.org_admin_plans + @plans = org_admin_plans.includes([{ roles: :user }, { answers: :question_options }, template: [{ phases: { sections: { questions: %i[question_format themes] } } }, :org]]) diff --git a/app/controllers/concerns/paginable.rb b/app/controllers/concerns/paginable.rb index 738d6af215..aea376ed12 100644 --- a/app/controllers/concerns/paginable.rb +++ b/app/controllers/concerns/paginable.rb @@ -37,7 +37,7 @@ module Paginable # one approach to just include everything in the double splat `**options` param # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists - def paginable_renderise(partial: nil, controller: nil, action: nil, + def paginable_renderise(partial: nil, template: nil, controller: nil, action: nil, path_params: {}, query_params: {}, scope: nil, locals: {}, **options) unless scope.is_a?(ActiveRecord::Relation) @@ -50,6 +50,7 @@ def paginable_renderise(partial: nil, controller: nil, action: nil, # Default options @paginable_options = {}.merge(options) @paginable_options[:view_all] = options.fetch(:view_all, true) + @paginable_options[:remote] = options.fetch(:remote, true) # Assignment for paginable_params based on arguments passed to the method @args = paginable_params.to_h @args[:controller] = controller if controller @@ -72,14 +73,17 @@ def paginable_renderise(partial: nil, controller: nil, action: nil, locals = locals.merge( scope: @refined_scope, paginable_params: @args, - search_term: @args[:search] + search_term: @args[:search], + remote: @paginable_options[:remote] ) # If this was an ajax call then render as JSON if options[:format] == :json render json: { html: render_to_string(layout: "/layouts/paginable", partial: partial, locals: locals) } - else + elsif partial.present? render(layout: "/layouts/paginable", partial: partial, locals: locals) + else + render(template: template, locals: locals) end end end @@ -104,7 +108,7 @@ def paginable_sort_link(sort_field) sort_link_name(sort_field), sort_link_url(sort_field), class: "paginable-action", - data: { remote: true }, + data: { remote: @paginable_options[:remote] }, aria: { label: sort_field } ) end diff --git a/app/controllers/org_admin/plans_controller.rb b/app/controllers/org_admin/plans_controller.rb index bdb5723ffa..d04739b5fc 100644 --- a/app/controllers/org_admin/plans_controller.rb +++ b/app/controllers/org_admin/plans_controller.rb @@ -15,7 +15,7 @@ def index @super_admin = current_user.can_super_admin? @clicked_through = params[:click_through].present? - @plans = @super_admin ? Plan.all.page(1) : current_user.org.plans.page(1) + @plans = @super_admin ? Plan.all.page(1) : current_user.org.org_admin_plans.page(1) end # GET org_admin/plans/:id/feedback_complete @@ -61,7 +61,7 @@ def download_plans plans = CSV.generate do |csv| csv << header_cols - org.plans.includes(template: :org).order(updated_at: :desc).each do |plan| + org.org_admin_plans.includes(template: :org).order(updated_at: :desc).each do |plan| csv << [ plan.title.to_s, plan.template.title.to_s, diff --git a/app/controllers/paginable/plans_controller.rb b/app/controllers/paginable/plans_controller.rb index e143caea2e..e29d26bdf9 100644 --- a/app/controllers/paginable/plans_controller.rb +++ b/app/controllers/paginable/plans_controller.rb @@ -34,12 +34,10 @@ def organisationally_or_publicly_visible # GET /paginable/plans/publicly_visible/:page def publicly_visible - paginable_renderise( - partial: "publicly_visible", - scope: Plan.publicly_visible.includes(:template), - query_params: { sort_field: "plans.updated_at", sort_direction: :desc }, - format: :json - ) + # We want the pagination/sort/search to be retained in the URL so redirect instead + # of processing this as a JSON + paginable_params = params.permit(:page, :search, :sort_field, :sort_direction) + redirect_to public_plans_path(paginable_params.to_h) end # GET /paginable/plans/org_admin/:page @@ -49,7 +47,7 @@ def org_admin # check if current user if super_admin @super_admin = current_user.can_super_admin? @clicked_through = params[:click_through].present? - plans = @super_admin ? Plan.all : current_user.org.plans + plans = @super_admin ? Plan.all : current_user.org.org_admin_plans plans = plans.joins(:template, roles: [user: :org]).where(Role.creator_condition) paginable_renderise( diff --git a/app/controllers/paginable/templates_controller.rb b/app/controllers/paginable/templates_controller.rb index 0457842868..680e7eb8ca 100644 --- a/app/controllers/paginable/templates_controller.rb +++ b/app/controllers/paginable/templates_controller.rb @@ -85,18 +85,10 @@ def customisable # GET /paginable/templates/publicly_visible/:page (AJAX) # ----------------------------------------------------- def publicly_visible - templates = Template.live(Template.families(Org.funder.pluck(:id)).pluck(:family_id)) - .publicly_visible.pluck(:id) << - Template.where(is_default: true).unarchived.published.pluck(:id) - paginable_renderise( - partial: "publicly_visible", - scope: Template.joins(:org) - .includes(:org) - .where(id: templates.uniq.flatten) - .published, - query_params: { sort_field: "templates.title", sort_direction: :asc }, - format: :json - ) + # We want the pagination/sort/search to be retained in the URL so redirect instead + # of processing this as a JSON + paginable_params = params.permit(:page, :search, :sort_field, :sort_direction) + redirect_to public_templates_path(paginable_params.to_h) end # GET /paginable/templates/:id/history/:page (AJAX) diff --git a/app/controllers/plans_controller.rb b/app/controllers/plans_controller.rb index 75c7f5be98..e6061c984b 100644 --- a/app/controllers/plans_controller.rb +++ b/app/controllers/plans_controller.rb @@ -124,7 +124,7 @@ def create elsif !@plan.template.customization_of.nil? # We used a customized version of the the funder template # rubocop:disable Layout/LineLength - msg += " #{_('This plan is based on the')} #{@plan.funder&.name}: '#{@plan.template.title}' #{_('template with customisations by the')} #{plan_params[:org_name]}" + msg += " #{_('This plan is based on the')} #{@plan.funder&.name}: '#{@plan.template.title}' #{_('template with customisations by the')} #{@plan.template.org.name}" # rubocop:enable Layout/LineLength else # We used the specified org's or funder's template @@ -219,7 +219,16 @@ def show # doing this when we refactor the Plan editing UI # GET /plans/:plan_id/phases/:id/edit def edit - plan = Plan.includes({ template: { phases: { sections: :questions } } }, { answers: :notes }) + plan = Plan.includes( + { template: { + phases: { + sections: { + questions: %i[question_format question_options annotations themes] + } + } + } }, + { answers: :notes } + ) .find(params[:id]) authorize plan phase_id = params[:phase_id].to_i @@ -258,7 +267,7 @@ def update if @plan.update(attrs) # _attributes(attrs) format.html do - redirect_to plan_contributors_path(@plan), + redirect_to plan_path(@plan), notice: success_message(@plan, _("saved")) end format.json do @@ -293,7 +302,7 @@ def share @plan = Plan.find(params[:id]) if @plan.present? authorize @plan - @plan_roles = @plan.roles + @plan_roles = @plan.roles.where(active: true) else redirect_to(plans_path) end @@ -306,7 +315,7 @@ def request_feedback @plan = Plan.find(params[:id]) if @plan.present? authorize @plan - @plan_roles = @plan.roles + @plan_roles = @plan.roles.where(active: true) else redirect_to(plans_path) end diff --git a/app/controllers/public_pages_controller.rb b/app/controllers/public_pages_controller.rb index 2ab686e2a8..d6dd911315 100644 --- a/app/controllers/public_pages_controller.rb +++ b/app/controllers/public_pages_controller.rb @@ -4,14 +4,23 @@ class PublicPagesController < ApplicationController # GET template_index # ----------------------------------------------------- + # rubocop:disable Metrics/AbcSize def template_index + @templates_query_params = { + page: paginable_params.fetch(:page, 1), + search: paginable_params.fetch(:search, ""), + sort_field: paginable_params.fetch(:sort_field, "templates.title"), + sort_direction: paginable_params.fetch(:sort_direction, "asc") + } + templates = Template.live(Template.families(Org.funder.pluck(:id)).pluck(:family_id)) .publicly_visible.pluck(:id) << Template.where(is_default: true).unarchived.published.pluck(:id) @templates = Template.includes(:org) .where(id: templates.uniq.flatten) - .unarchived.published.order(title: :asc).page(1) + .unarchived.published end + # rubocop:enable Metrics/AbcSize # GET template_export/:id # ----------------------------------------------------- @@ -79,13 +88,21 @@ def template_export # GET /plans_index # ------------------------------------------------------------------------------------ def plan_index - @plans = Plan.publicly_visible.includes(:template).page(1) + @plans = Plan.publicly_visible.includes(:template) render "plan_index", locals: { query_params: { - sort_field: "plans.updated_at", - sort_direction: "desc" + page: paginable_params.fetch(:page, 1), + search: paginable_params.fetch(:search, ""), + sort_field: paginable_params.fetch(:sort_field, "plans.updated_at"), + sort_direction: paginable_params.fetch(:sort_direction, "desc") } } end + private + + def paginable_params + params.permit(:page, :search, :sort_field, :sort_direction) + end + end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 0773f8b4a5..298525d6b1 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -66,12 +66,12 @@ def create existing_user = User.where_case_insensitive("email", sign_up_params[:email]).first if existing_user.present? if existing_user.invitation_token.present? && !existing_user.accept_terms? - # 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 + # If the user is creating an account but they have an outstanding invitation, remember + # any plans that were shared with the invitee so we can attach them to the new User record + shared_plans = existing_user.roles + .select(&:active?) + .map { |role| { plan_id: role.plan_id, access: role.access } } existing_user.destroy - else redirect_to after_sign_up_error_path_for(resource), alert: _("That email address is already registered.") @@ -88,6 +88,17 @@ def create build_resource(attrs) + # If the user is creating an account but they have an outstanding invitation, attach the shared + # plan(s) to their new User record + if shared_plans.present? && shared_plans.any? + shared_plans.each do |role_hash| + plan = Plan.find_by(id: role_hash[:plan_id]) + next unless plan.present? + + Role.create(plan: plan, user: resource, access: role_hash[:access], active: true) + end + end + # Determine if reCAPTCHA is enabled and if so verify it use_recaptcha = Rails.configuration.x.application.use_recaptcha || false if (!use_recaptcha || verify_recaptcha(model: resource)) && resource.save diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb index 8e6fe7aa57..58e583eb65 100644 --- a/app/controllers/roles_controller.rb +++ b/app/controllers/roles_controller.rb @@ -17,7 +17,10 @@ def create authorize @role message = "" - if role_params[:user].present? && plan.present? + if role_params[:user].present? && + role_params[:user].key?(:email) && + role_params[:user][:email].present? && plan.present? + if @role.plan.owner.present? && @role.plan.owner.email == role_params[:user][:email] # rubocop:disable Layout/LineLength flash[:notice] = _("Cannot share plan with %{email} since that email matches with the owner of the plan.") % { @@ -26,7 +29,11 @@ def create # rubocop:enable Layout/LineLength else user = User.where_case_insensitive("email", role_params[:user][:email]).first - if Role.find_by(plan: @role.plan, user: user) # role already exists + if user.present? && + Role.where(plan: @role.plan, user: user, active: true) + .count + .positive? # role already exists + flash[:notice] = _("Plan is already shared with %{email}.") % { email: role_params[:user][:email] } diff --git a/app/controllers/super_admin/notifications_controller.rb b/app/controllers/super_admin/notifications_controller.rb index e6b9686c31..fba40c042c 100644 --- a/app/controllers/super_admin/notifications_controller.rb +++ b/app/controllers/super_admin/notifications_controller.rb @@ -36,7 +36,7 @@ def create @notification.notification_type = "global" if @notification.save flash.now[:notice] = success_message(@notification, _("created")) - render :edit + redirect_to edit_super_admin_notification_path(@notification) else flash.now[:alert] = failure_message(@notification, _("create")) render :new @@ -49,6 +49,7 @@ def update authorize(Notification) if @notification.update(notification_params) flash.now[:notice] = success_message(@notification, _("updated")) + return redirect_to edit_super_admin_notification_path(@notification) else flash.now[:alert] = failure_message(@notification, _("update")) end diff --git a/app/controllers/super_admin/orgs_controller.rb b/app/controllers/super_admin/orgs_controller.rb index e578acbb41..6cecf152a2 100644 --- a/app/controllers/super_admin/orgs_controller.rb +++ b/app/controllers/super_admin/orgs_controller.rb @@ -99,6 +99,51 @@ def destroy end end + # POST /super_admin/:id/merge_analyze + def merge_analyze + @org = Org.includes(:templates, :tracker, :annotations, + :departments, :token_permission_types, :funded_plans, + identifiers: [:identifier_scheme], + guidance_groups: [guidances: [:themes]], + users: [identifiers: [:identifier_scheme]]) + .find(params[:id]) + authorize @org + + lookup = OrgSelection::HashToOrgService.to_org( + hash: JSON.parse(merge_params[:id]), allow_create: false + ) + @target_org = Org.includes(:templates, :tracker, :annotations, + :departments, :token_permission_types, :funded_plans, + identifiers: [:identifier_scheme], + guidance_groups: [guidances: [:themes]], + users: [identifiers: [:identifier_scheme]]) + .find(lookup.id) + end + + # POST /super_admin/:id/merge_commit + def merge_commit + @org = Org.find(params[:id]) + authorize @org + + @target_org = Org.find_by(id: merge_params[:target_org]) + + if @target_org.present? + if @target_org.merge!(to_be_merged: @org) + msg = "Successfully merged '#{@org.name}' into '#{@target_org.name}'" + redirect_to super_admin_orgs_path, notice: msg + else + msg = _("An error occurred while trying to merge the Organisations.") + redirect_to admin_edit_org_path(@org), alert: msg + end + else + msg = _("Unable to merge the two Organisations at this time.") + redirect_to admin_edit_org_path(@org), alert: msg + end + rescue JSON::ParserError + msg = _("Unable to determine what records need to be merged.") + redirect_to admin_edit_org_path(@org), alert: msg + end + private def org_params @@ -110,6 +155,10 @@ def org_params :org_id, :org_name, :org_crosswalk) end + def merge_params + params.require(:org).permit(:org_name, :org_sources, :org_crosswalk, :id, :target_org) + end + end end diff --git a/app/controllers/super_admin/users_controller.rb b/app/controllers/super_admin/users_controller.rb index 1f3e4f6b5a..933ac6e5c5 100644 --- a/app/controllers/super_admin/users_controller.rb +++ b/app/controllers/super_admin/users_controller.rb @@ -64,16 +64,18 @@ def update def merge @user = User.find(params[:id]) authorize @user - remove = User.find(params[:merge_id]) - if @user.merge(remove) - flash.now[:notice] = success_message(@user, _("merged")) + if params[:id] != params[:merge_id] + merge_accounts else - flash.now[:alert] = failure_message(@user, _("merge")) + flash.now[:alert] = _("You attempted to merge 2 accounts with the same email address. + Please merge with a different email address.") end + # After merge attempt get departments and plans @departments = @user.org.departments.order(:name) @plans = Plan.active(@user).page(1) + render :edit end @@ -121,6 +123,15 @@ def user_params :other_organisation) end + def merge_accounts + remove = User.find(params[:merge_id]) + if @user.merge(remove) + flash.now[:notice] = success_message(@user, _("merged")) + else + flash.now[:alert] = failure_message(@user, _("merge")) + end + end + end end diff --git a/app/helpers/conditions_helper.rb b/app/helpers/conditions_helper.rb index 5e31d66e5b..a674017fdc 100644 --- a/app/helpers/conditions_helper.rb +++ b/app/helpers/conditions_helper.rb @@ -71,9 +71,9 @@ def num_section_answers(plan, section) count = 0 plan_remove_list = remove_list(plan) plan.answers.each do |answer| - next unless answer.question.section.id == section.id && - !plan_remove_list.include?(answer.question.id) && - section.answered_questions(plan).include?(answer) && + next unless answer.question.section_id == section.id && + !plan_remove_list.include?(answer.question_id) && + section.question_ids.include?(answer.question_id) && answer.answered? count += 1 @@ -82,28 +82,27 @@ def num_section_answers(plan, section) end # number of questions in a section after update with conditions - # rubocop:disable Metrics/AbcSize def num_section_questions(plan, section, phase = nil) # when section and phase are a hash in exports if section.is_a?(Hash) && !phase.nil? && plan.is_a?(Plan) - phase_id = plan.phases.select { |ph| ph.number == phase[:number] }.first.id - section = plan.sections - .select { |s| s.phase_id == phase_id && s.title == section[:title] } - .first + + phase = plan.template + .phases + .select { |ph| ph.number == phase[:number] } + .first + section = phase.sections + .select { |s| s.phase_id == phase.id && s.title == section[:title] } + .first end count = 0 plan_remove_list = remove_list(plan) - plan.questions.each do |question| - if question.section.id == section.id && - !plan_remove_list.include?(question.id) - count += 1 - end + section.questions.each do |question| + count += 1 unless plan_remove_list.include?(question.id) end count end - # rubocop:enable Metrics/AbcSize # returns an array of hashes of section_id, number of section questions, and # number of section answers diff --git a/app/helpers/super_admin/orgs/merge_helper.rb b/app/helpers/super_admin/orgs/merge_helper.rb new file mode 100644 index 0000000000..1c2051fdda --- /dev/null +++ b/app/helpers/super_admin/orgs/merge_helper.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module SuperAdmin + + module Orgs + + module MergeHelper + + def org_column_content(attributes:) + return "No mergeable attributes" unless attributes.present? && attributes.keys.any? + + html = "" + end + + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + def column_content(entries:, orcid:) + return _("None") unless entries.present? && entries.any? + + html = "" + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + + def merge_column_content(entries:, orcid:, to_org_name:) + return _("Nothing to merge") unless entries.present? && entries.any? + + html = _("

The following %{object_types} will be moved over to '%{org_name}':

") % { + object_types: entries.first.class.name.pluralize, + org_name: to_org_name + } + html + column_content(entries: entries, orcid: orcid) + end + + end + + end + +end diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 23254c1822..1c83822daa 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -37,6 +37,7 @@ import '../src/utils/tooltipHelper'; // Specific functions from the Utilities files that will be made available to // the js.erb templates in the `window.x` statements below import { renderAlert, renderNotice } from '../src/utils/notificationHelper'; +import toggleSpinner from '../src/utils/spinner'; // View specific JS import '../src/answers/conditions'; @@ -101,3 +102,4 @@ window.jQuery = jQuery; // Allow js.erb files to access the notificationHelper functions window.renderAlert = renderAlert; window.renderNotice = renderNotice; +window.toggleSpinner = toggleSpinner; diff --git a/app/javascript/src/orgs/adminEdit.js b/app/javascript/src/orgs/adminEdit.js index 49e90ed95c..ac27b03499 100644 --- a/app/javascript/src/orgs/adminEdit.js +++ b/app/javascript/src/orgs/adminEdit.js @@ -56,4 +56,7 @@ $(() => { e.preventDefault(); $(e.target).parent('a').tooltip('toggle'); }); + + initAutocomplete('#org-merge-controls .autocomplete'); + scrubOrgSelectionParamsOnSubmit('form.edit_org'); }); diff --git a/app/javascript/src/utils/spinner.js b/app/javascript/src/utils/spinner.js new file mode 100644 index 0000000000..9298e9f009 --- /dev/null +++ b/app/javascript/src/utils/spinner.js @@ -0,0 +1,39 @@ +// Will display a spinner at the start of any UJS/Ajax call and then hide it after the +// controller responds. +const toggleSpinner = (visible) => { + const spinnerBlock = $('.spinner-border'); + + if (spinnerBlock.length > 0) { + if (visible) { + spinnerBlock.removeClass('hidden'); + } else { + spinnerBlock.addClass('hidden'); + } + } +}; + +$(() => { + $('body').on('ajax:beforeSend', () => { + toggleSpinner(true); + }); + + $('body').on('ajax:complete', () => { + toggleSpinner(false); + }); + + $('body').on('ajax:error', () => { + toggleSpinner(false); + }); + + $('body').on('ajax:stopped', () => { + toggleSpinner(false); + }); + + $('body').on('ajax:success', () => { + toggleSpinner(false); + }); + + toggleSpinner(false); +}); + +export default (visible) => toggleSpinner(visible); diff --git a/app/models/concerns/identifiable.rb b/app/models/concerns/identifiable.rb index 73f72d28a4..3593d8a061 100644 --- a/app/models/concerns/identifiable.rb +++ b/app/models/concerns/identifiable.rb @@ -58,7 +58,7 @@ def identifier_for_scheme(scheme:) # Combines the existing identifiers with the new ones def consolidate_identifiers!(array:) - return false unless array.present? && array.is_a?(Array) + return false unless array.present? && array.any? array.each do |id| next unless id.is_a?(Identifier) && id.value.present? diff --git a/app/models/contributor.rb b/app/models/contributor.rb index 0c103c8392..f177935ad9 100644 --- a/app/models/contributor.rb +++ b/app/models/contributor.rb @@ -67,6 +67,7 @@ class Contributor < ApplicationRecord has_flags 1 => :data_curation, 2 => :investigation, 3 => :project_administration, + 4 => :other, column: "roles" # ========== @@ -91,7 +92,7 @@ class << self # returns the default role def default_role - "investigation" + "other" end end diff --git a/app/models/guidance_group.rb b/app/models/guidance_group.rb index da753920dc..c4c2ba04c7 100644 --- a/app/models/guidance_group.rb +++ b/app/models/guidance_group.rb @@ -122,8 +122,7 @@ def self.all_viewable(user) all_viewable_groups = default_org_groups + funder_groups + organisation_groups - all_viewable_groups = all_viewable_groups.flatten.uniq - all_viewable_groups + all_viewable_groups.flatten.uniq end def self.create_org_default(org) @@ -134,4 +133,32 @@ def self.create_org_default(org) ) end + # ==================== + # = Instance methods = + # ==================== + + def merge!(to_be_merged:) + return self unless to_be_merged.is_a?(GuidanceGroup) + + GuidanceGroup.transaction do + # Reassociate any Plan-GuidanceGroup connections + to_be_merged.plans.each do |plan| + plan.guidance_groups << self + plan.save + end + # Reassociate the Guidances + to_be_merged.guidances.update_all(guidance_group_id: id) + to_be_merged.plans.delete_all + + # Terminate the transaction if the resulting Org is not valid + raise ActiveRecord::Rollback unless save + + # Reload and then drop the specified Org. The reload prevents ActiveRecord + # from also destroying associations that we've already reassociated above + raise ActiveRecord::Rollback unless to_be_merged.reload.destroy.present? + + reload + end + end + end diff --git a/app/models/org.rb b/app/models/org.rb index ddf3b384c7..141f0edfd6 100644 --- a/app/models/org.rb +++ b/app/models/org.rb @@ -289,14 +289,10 @@ def org_admins User.joins(:perms).where("users.org_id = ? AND perms.name IN (?)", id, admin_perms) end - def plans - Rails.cache.fetch("org[#{id}].plans", expires_in: 2.seconds) do - plan_ids = Role.administrator - .where(user_id: users.pluck(:id), active: true) - .pluck(:plan_id).uniq - Plan.includes(:template, :phases, :roles, :users) - .where(id: plan_ids) - end + # This replaces the old plans method. We now use the native plans method and this. + def org_admin_plans + combined_plan_ids = (native_plan_ids + affiliated_plan_ids).flatten.uniq + Plan.includes(:template, :phases, :roles, :users).where(id: combined_plan_ids) end def grant_api!(token_permission_type) @@ -304,6 +300,48 @@ def grant_api!(token_permission_type) token_permission_types.include? token_permission_type end + # Merges the specified Org into this Org + # rubocop:disable Metrics/AbcSize + def merge!(to_be_merged:) + return self unless to_be_merged.is_a?(Org) + + transaction do + merge_attributes!(to_be_merged: to_be_merged) + + # Merge all of the associated objects + to_be_merged.annotations.update_all(org_id: id) + merge_departments!(to_be_merged: to_be_merged) + to_be_merged.funded_plans.update_all(funder_id: id) + merge_guidance_groups!(to_be_merged: to_be_merged) + consolidate_identifiers!(array: to_be_merged.identifiers) + + # TODO: Use this commented out version once we've stopped overriding the + # default association method called `plans` on this model + # to_be_merged.plans.update_all(org_id: id) + Plan.where(org_id: to_be_merged.id).update_all(org_id: id) + + to_be_merged.templates.update_all(org_id: id) + merge_token_permission_types!(to_be_merged: to_be_merged) + self.tracker = to_be_merged.tracker unless tracker.present? + to_be_merged.users.update_all(org_id: id) + + # Terminate the transaction if the resulting Org is not valid + raise ActiveRecord::Rollback unless save + + # Remove all of the remaining identifiers and token_permission_types + # that were not merged + to_be_merged.identifiers.delete_all + to_be_merged.token_permission_types.delete_all + + # Reload and then drop the specified Org. The reload prevents ActiveRecord + # from also destroying associations that we've already reassociated above + raise ActiveRecord::Rollback unless to_be_merged.reload.destroy.present? + + reload + end + end + # rubocop:enable Metrics/AbcSize + private ## @@ -315,6 +353,66 @@ def resize_image self.logo = logo.thumb("x100") # resize height and maintain aspect ratio end + # rubocop:disable Metrics/AbcSize + def merge_attributes!(to_be_merged:) + return false unless to_be_merged.is_a?(Org) + + self.target_url = to_be_merged.target_url unless target_url.present? + self.managed = true if !managed? && to_be_merged.managed? + self.links = to_be_merged.links unless links.nil? || links == "{\"org\":[]}" + self.logo_uid = to_be_merged.logo_uid unless logo.present? + self.logo_name = to_be_merged.logo_name unless logo.present? + self.contact_email = to_be_merged.contact_email unless contact_email.present? + self.contact_name = to_be_merged.contact_name unless contact_name.present? + self.feedback_enabled = to_be_merged.feedback_enabled unless feedback_enabled? + self.feedback_email_msg = to_be_merged.feedback_email_msg unless feedback_email_msg.present? + # rubocop:disable Layout/LineLength + self.feedback_email_subject = to_be_merged.feedback_email_subject unless feedback_email_subject.present? + # rubocop:enable Layout/LineLength + end + # rubocop:enable Metrics/AbcSize + + def merge_departments!(to_be_merged:) + return false unless to_be_merged.is_a?(Org) && to_be_merged.departments.any? + + existing = departments.collect { |dpt| dpt.name.downcase.strip } + # Do not attach departments if we already have an existing one + to_be_merged.departments.each do |department| + department.destroy if existing.include?(department.name.downcase) + department.update(org_id: id) unless existing.include?(department.name.downcase) + end + end + + def merge_guidance_groups!(to_be_merged:) + return false unless to_be_merged.is_a?(Org) && to_be_merged.guidance_groups.any? + + to_gg = guidance_groups.first + # Create a new GuidanceGroup if one does not already exist + to_gg = GuidanceGroup.create(org: self, name: abbreviation) unless to_gg.present? + + # Merge the GuidanceGroups + to_be_merged.guidance_groups.each { |from_gg| to_gg.merge!(to_be_merged: from_gg) } + end + + def merge_token_permission_types!(to_be_merged:) + return false unless to_be_merged.is_a?(Org) && to_be_merged.token_permission_types.any? + + to_be_merged.token_permission_types.each do |perm_type| + token_permission_types << perm_type unless token_permission_types.include?(perm_type) + end + end + + def affiliated_plan_ids + Rails.cache.fetch("org[#{id}].plans", expires_in: 2.seconds) do + Role.administrator.where(user_id: users.pluck(:id), active: true) + .pluck(:plan_id).uniq + end + end + + def native_plan_ids + plans.map(&:id) + end + # Ensure that the Org has all of the available token permission types prior to save def ensure_api_access TokenPermissionType.all.each do |perm| diff --git a/app/models/plan.rb b/app/models/plan.rb index 07343e9b58..8ac2ed1a32 100644 --- a/app/models/plan.rb +++ b/app/models/plan.rb @@ -160,7 +160,7 @@ class Plan < ApplicationRecord # Retrieves any plan organisationally or publicly visible for a given org id scope :organisationally_or_publicly_visible, lambda { |user| - plan_ids = user.org.plans.where(complete: true).pluck(:id).uniq + plan_ids = user.org.org_admin_plans.where(complete: true).pluck(:id).uniq includes(:template, roles: :user) .where(id: plan_ids, visibility: [ visibilities[:organisationally_visible], diff --git a/app/models/question.rb b/app/models/question.rb index c7ccfa0187..2deb90a5a1 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -166,9 +166,10 @@ def guidance_for_org(org) # # Returns ActiveRecord::Relation def example_answers(org_ids) - annotations.where(org_id: Array(org_ids), - type: Annotation.types[:example_answer]) - .order(:created_at) + org_ids = Array(org_ids) + annotations.select { |a| org_ids.include?(a.org_id) } + .select(&:example_answer?) + .sort { |a, b| a.created_at <=> b.created_at } end alias get_example_answers example_answers @@ -183,7 +184,9 @@ def example_answers(org_ids) # # Returns Annotation def guidance_annotation(org_id) - annotations.where(org_id: org_id, type: Annotation.types[:guidance]).first + annotations.select { |a| a.org_id == org_id } + .select(&:guidance?) + .first end alias get_guidance_annotation guidance_annotation diff --git a/app/models/section.rb b/app/models/section.rb index eaa2697bc5..4017e7438b 100644 --- a/app/models/section.rb +++ b/app/models/section.rb @@ -113,8 +113,8 @@ def num_answered_questions(plan) def answered_questions(plan) return [] if plan.nil? - plan.answers.includes({ question: :question_format }, :question_options) - .where(question_id: question_ids) + plan.answers + .select { |answer| question_ids.include?(answer.question_id) } .to_a end diff --git a/app/policies/org_policy.rb b/app/policies/org_policy.rb index f133cb2d10..8a927ced14 100644 --- a/app/policies/org_policy.rb +++ b/app/policies/org_policy.rb @@ -51,4 +51,12 @@ def templates? true end + def merge_analyze? + user.can_super_admin? + end + + def merge_commit? + user.can_super_admin? + end + end diff --git a/app/presenters/api/v1/contributor_presenter.rb b/app/presenters/api/v1/contributor_presenter.rb index 0f4c22c61e..6970433bd7 100644 --- a/app/presenters/api/v1/contributor_presenter.rb +++ b/app/presenters/api/v1/contributor_presenter.rb @@ -11,6 +11,7 @@ class << self # Convert the specified role into a CRediT Taxonomy URL def role_as_uri(role:) return nil unless role.present? + return "other" if role.to_s.downcase == "other" "#{Contributor::ONTOLOGY_BASE_URL}/#{role.to_s.downcase.gsub('_', '-')}" end diff --git a/app/presenters/contributor_presenter.rb b/app/presenters/contributor_presenter.rb index 73d721fa04..91ce18bab2 100644 --- a/app/presenters/contributor_presenter.rb +++ b/app/presenters/contributor_presenter.rb @@ -43,8 +43,10 @@ def role_symbol_to_string(symbol:) "Data Manager" when :project_administration "Project Administrator" - else + when :investigation "Principal Investigator" + else + "Other" end end diff --git a/app/presenters/guidance_presenter.rb b/app/presenters/guidance_presenter.rb index e3c128e090..c2acdd6b9a 100644 --- a/app/presenters/guidance_presenter.rb +++ b/app/presenters/guidance_presenter.rb @@ -113,9 +113,9 @@ def guidance_annotations?(org: nil, question: nil) # structure: # { guidance_group: { theme: [guidance, ...], ... }, ... } def guidance_groups_by_theme(org: nil, question: nil) - raise ArgumentError unless question.respond_to?(:themes) + raise ArgumentError unless question.is_a?(Question) + raise ArgumentError unless org.is_a?(Org) - question = Question.includes(:themes).find(question.id) return {} unless hashified_guidance_groups.key?(org) hashified_guidance_groups[org].each_key.each_with_object({}) do |gg, acc| diff --git a/app/presenters/super_admin/orgs/merge_presenter.rb b/app/presenters/super_admin/orgs/merge_presenter.rb new file mode 100644 index 0000000000..e3bc57cf96 --- /dev/null +++ b/app/presenters/super_admin/orgs/merge_presenter.rb @@ -0,0 +1,164 @@ +# frozen_string_literal: true + +module SuperAdmin + + module Orgs + + class MergePresenter + + attr_accessor :from_org, :to_org, :from_org_name, :to_org_name, + :from_org_entries, :to_org_entries, :mergeable_entries, + :categories, :from_org_attributes, :to_org_attributes, + :mergeable_attributes + + def initialize(from_org:, to_org:) + @from_org = from_org + @to_org = to_org + + # Abbreviated Org names for display in tables + @from_org_name = @from_org.name.split(" ")[0..2].join(" ") + @to_org_name = @to_org.name.split(" ")[0..2].join(" ") + + # Association records + @from_org_entries = prepare_org(org: @from_org) + @to_org_entries = prepare_org(org: @to_org) + @mergeable_entries = prepare_mergeables + @categories = @from_org_entries.keys.sort { |a, b| a <=> b } + + # Specific Org columns + @from_org_attributes = org_attributes(org: @from_org) + @to_org_attributes = org_attributes(org: @to_org) + @mergeable_attributes = mergeable_columns + end + + private + + # rubocop:disable Metrics/AbcSize + def prepare_org(org:) + return {} unless org.present? && org.is_a?(Org) + + { + annotations: org.annotations.sort { |a, b| a.text <=> b.text }, + departments: org.departments.sort { |a, b| a.name <=> b.name }, + funded_plans: org.funded_plans.sort { |a, b| a.title <=> b.title }, + guidances: org.guidance_groups.collect(&:guidances).flatten, + identifiers: org.identifiers.sort { |a, b| a.value <=> b.value }, + # TODO: Org.plans is overridden and does not clearly identify Orgs that 'own' + # the plan (i.e. the one the user selected as the 'Research Org') + # Loading them directly here until issue #2724 is resolved + plans: Plan.where(org: org).sort { |a, b| a.title <=> b.title }, + templates: org.templates.sort { |a, b| a.title <=> b.title }, + token_permission_types: org.token_permission_types.sort { |a, b| a.to_s <=> b.to_s }, + tracker: [org.tracker].compact, + users: org.users.sort { |a, b| a.email <=> b.email } + } + end + # rubocop:enable Metrics/AbcSize + + def prepare_mergeables + return {} unless @from_org_entries.any? && @to_org_entries.any? + + { + annotations: diff_from_and_to(category: :annotations), + departments: diff_from_and_to(category: :departments), + funded_plans: diff_from_and_to(category: :funded_plans), + guidances: diff_from_and_to(category: :guidances), + identifiers: diff_from_and_to(category: :identifiers), + plans: diff_from_and_to(category: :plans), + templates: diff_from_and_to(category: :templates), + token_permission_types: diff_from_and_to(category: :token_permission_types), + tracker: @to_org_entries[:tracker].any? ? [] : @from_org_entries[:tracker], + users: diff_from_and_to(category: :users) + } + end + + # rubocop:disable Metrics/AbcSize + def diff_from_and_to(category:) + return [] unless category.present? && @from_org_entries.fetch(category, []).any? + + case category + when :departments + # Merge only the unique departments + existing = @to_org_entries[category].map { |e| e.name.downcase } + @from_org_entries[category].reject { |e| existing.include?(e.name.downcase) } + when :identifiers + # Merge identifiers with no identifier_scheme + # Retain the to_org's identifiers for specific identifier_schemes + schemes = @to_org_entries[category].collect(&:identifier_scheme) + @from_org_entries[category].reject do |entry| + entry.identifier_scheme.present? && schemes.include?(entry.identifier_scheme) + end + when :token_permission_types + # Merge only the unique token_permission_types + existing = @to_org_entries[category].map { |e| e.token_type.downcase } + @from_org_entries[category].reject { |e| existing.include?(e.token_type.downcase) } + when :users + # Merge only the unique users + existing = @to_org_entries[category].map { |e| e.email.downcase } + @from_org_entries[category].reject { |e| existing.include?(e.email.downcase) } + else + @from_org_entries[category] + end + end + # rubocop:enable Metrics/AbcSize + + def org_attributes(org:) + return {} unless org.is_a?(Org) + + { + contact_email: org.contact_email, + contact_name: org.contact_name, + feedback_email_msg: org.feedback_email_msg, + feedback_email_subject: org.feedback_email_subject, + feedback_enabled: org.feedback_enabled, + managed: org.managed, + links: org.links, + logo_name: org.logo_name, + logo_uid: org.logo_uid, + target_url: org.target_url + } + end + + def mergeable_columns + out = {} + out[:target_url] = @from_org.target_url if mergeable_column?(column: :target_url) + out[:managed] = @from_org.managed if mergeable_column?(column: :managed) + out[:links] = @from_org.links if mergeable_column?(column: :links) + + if mergeable_column?(column: :logo) + out[:logo_uid] = @from_org.logo_uid + out[:logo_name] = @from_org.logo_name + end + if mergeable_column?(column: :contact_email) + out[:contact_email] = @from_org.contact_email + out[:contact_name] = @from_org.contact_name + end + if mergeable_column?(column: :feedback_enabled) + out[:feedback_enabled] = @from_org.feedback_enabled + out[:feedback_email_subject] = @from_org.feedback_email_subject + out[:feedback_email_msg] = @from_org.feedback_email_msg + end + out + end + + def mergeable_column?(column:) + case column + when :links + (@to_org.links.nil? || @to_org.links.fetch("org", []).empty?) && + (@from_org.links.present? || @from_org.links.fetch("org", [].any?)) + when :managed + !@to_org.managed? && @from_org.managed? + when :feedback_enabled + !@to_org.feedback_enabled? && @from_org.feedback_enabled? + else + @to_org.send(column).nil? && + @from_org.send(column).present? && + @to_org != @from_org + end + end + + end + + end + +end diff --git a/app/views/api/v0/plans/index.json.jbuilder b/app/views/api/v0/plans/index.json.jbuilder index 8e5ec1d94a..a2f2aec735 100644 --- a/app/views/api/v0/plans/index.json.jbuilder +++ b/app/views/api/v0/plans/index.json.jbuilder @@ -30,11 +30,14 @@ json.array! @plans.each do |plan| end data_contact = plan.contributors.data_curation.first || plan.owner - json.data_contact do - json.name data_contact.is_a?(Contributor) ? data_contact.name : data_contact.name(false) - json.email data_contact.email - json.phone data_contact.phone if data_contact.is_a?(Contributor) + if data_contact.present? + json.data_contact do + json.name data_contact.is_a?(Contributor) ? data_contact.name : data_contact.name(false) + json.email data_contact.email + json.phone data_contact.phone if data_contact.is_a?(Contributor) + end end + json.users plan.roles.each do |role| json.email role.user.email end diff --git a/app/views/contributors/_form.html.erb b/app/views/contributors/_form.html.erb index 58269ebedb..f199b2c90e 100644 --- a/app/views/contributors/_form.html.erb +++ b/app/views/contributors/_form.html.erb @@ -42,7 +42,7 @@ roles_tooltip = _("Select each role that applies to the contributor.")
<%= form.label(:phone, _("Phone number"), class: "control-label") %>
-
+
<%= phone_tooltip %> <%= form.phone_field :phone, class: "form-control", title: phone_tooltip, @@ -73,7 +73,7 @@ roles_tooltip = _("Select each role that applies to the contributor.") <% roles = ContributorPresenter.roles_for_radio(contributor: contributor) %> <% roles.each do |hash| %> -
+
<%= form.check_box hash.keys.first.to_sym, value: hash.values.first, diff --git a/app/views/contributors/index.html.erb b/app/views/contributors/index.html.erb index c41becec56..ddd3911313 100644 --- a/app/views/contributors/index.html.erb +++ b/app/views/contributors/index.html.erb @@ -19,6 +19,7 @@ <%= paginable_renderise partial: "/paginable/contributors/index", controller: "paginable/contributors", action: "index", + remote: true, scope: @contributors, locals: { plan: @plan }, query_params: { diff --git a/app/views/guidances/admin_index.html.erb b/app/views/guidances/admin_index.html.erb index 15a7eb352d..eb52bb0b74 100644 --- a/app/views/guidances/admin_index.html.erb +++ b/app/views/guidances/admin_index.html.erb @@ -19,6 +19,7 @@ partial: '/paginable/guidance_groups/index', controller: 'paginable/guidance_groups', action: 'index', + remote: true, scope: @guidance_groups, query_params: { sort_field: 'guidance_groups.name', sort_direction: :asc }) %>
@@ -39,6 +40,7 @@ partial: '/paginable/guidances/index', controller: 'paginable/guidances', action: 'index', + remote: true, scope: @guidances, query_params: { sort_field: 'guidances.text', sort_direction: :asc }) %> diff --git a/app/views/layouts/_paginable.html.erb b/app/views/layouts/_paginable.html.erb index 7b30ca0074..5afb3f7c56 100644 --- a/app/views/layouts/_paginable.html.erb +++ b/app/views/layouts/_paginable.html.erb @@ -1,6 +1,6 @@ <% # Custom layout to be included on any view that needs pagination - # locals: { scope, search_term, checkbox_status } + # locals: { remote, scope, search_term, checkbox_status } %> <% total = paginable? ? scope.total_count : scope.length %>
@@ -8,7 +8,7 @@
<%= render(partial: '/shared/search', - locals: { search_term: search_term }) if searchable? || @filter_admin || total > Kaminari.config.default_per_page %> + locals: { remote: remote, search_term: search_term }) if searchable? || @filter_admin || total > Kaminari.config.default_per_page %>
@@ -31,28 +31,28 @@ <% if searchable? %>
    <% if paginable? %> -
  • <%= link_to(_('View all search results'), paginable_base_url('ALL'), { 'data-remote': true, class: 'paginable-action' }) %>
  • +
  • <%= link_to(_('View all search results'), paginable_base_url('ALL'), { 'data-remote': remote, class: 'paginable-action' }) %>
  • <% else %> - <%= link_to(_('View less search results'), paginable_base_url(1), { 'data-remote': true, class: 'paginable-action' }) %> + <%= link_to(_('View less search results'), paginable_base_url(1), { 'data-remote': remote, class: 'paginable-action' }) %> <% end %> -
  • <%= link_to(_('Clear search results'), paginable_base_url(1), { 'data-remote': true, class: 'paginable-action' }) %>
  • +
  • <%= link_to(_('Clear search results'), paginable_base_url(1), { 'data-remote': remote, class: 'paginable-action' }) %>
<% else %> <% if paginable? %> - <%= link_to(_('View all'), paginable_base_url('ALL'), { 'data-remote': true, class: 'paginable-action' }) if @paginable_options[:view_all] %> + <%= link_to(_('View all'), paginable_base_url('ALL'), { 'data-remote': remote, class: 'paginable-action' }) if @paginable_options[:view_all] %> <% else %> - <%= link_to(_('View less'), paginable_base_url(1), { 'data-remote': true, class: 'paginable-action' }) %> + <%= link_to(_('View less'), paginable_base_url(1), { 'data-remote': remote, class: 'paginable-action' }) %> <% end %> <% end %> <% else %> <% if searchable? || @filter_admin %> - <%= link_to(_('Clear search results'), paginable_base_url(1), { 'data-remote': true, class: 'paginable-action' }) %> + <%= link_to(_('Clear search results'), paginable_base_url(1), { 'data-remote': remote, class: 'paginable-action' }) %> <% end %> <% end %>
<% if paginable? %> - <%= paginate(scope, params: paginable_params, remote: true) %> + <%= paginate(scope, params: paginable_params, remote: remote) %> <% end %>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 4ec9fe8834..079d6e3b34 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -94,6 +94,16 @@
<%= render "layouts/notifications", notifications: Notification.active_per_user(current_user) %> <%= yield %> + + + <%# Generic UJS/Ajax spinner. Bootstrap 5+ has built in spinner for this class, so update when + we upgrade to remove the img tag %> + + diff --git a/app/views/org_admin/plans/index.html.erb b/app/views/org_admin/plans/index.html.erb index db63d6505b..2b16861c5d 100644 --- a/app/views/org_admin/plans/index.html.erb +++ b/app/views/org_admin/plans/index.html.erb @@ -48,6 +48,7 @@ partial: '/paginable/plans/org_admin', controller: 'paginable/plans', action: 'org_admin', + remote: true, scope: @plans, view_all: !current_user.can_super_admin?, query_params: { sort_field: 'plans.updated_at', sort_direction: :desc }) %> diff --git a/app/views/org_admin/templates/history.html.erb b/app/views/org_admin/templates/history.html.erb index 9e42f43290..da311ea07b 100644 --- a/app/views/org_admin/templates/history.html.erb +++ b/app/views/org_admin/templates/history.html.erb @@ -20,6 +20,7 @@ partial: '/paginable/templates/history', controller: 'paginable/templates', action: 'history', + remote: true, query_params: query_params, scope: templates, locals: local_assigns ) %> diff --git a/app/views/org_admin/templates/index.html.erb b/app/views/org_admin/templates/index.html.erb index 6ca181f3d3..9de826dfe5 100644 --- a/app/views/org_admin/templates/index.html.erb +++ b/app/views/org_admin/templates/index.html.erb @@ -105,6 +105,7 @@ partial: "paginable/templates/#{action_name}", controller: 'paginable/templates', action: action_name, + remote: true, scope: @templates, query_params: @query_params, locals: { customizations: @customizations }) %> diff --git a/app/views/org_admin/users/plans.html.erb b/app/views/org_admin/users/plans.html.erb index 93ce5d5f6b..25f05671a3 100644 --- a/app/views/org_admin/users/plans.html.erb +++ b/app/views/org_admin/users/plans.html.erb @@ -10,6 +10,7 @@ partial: '/paginable/plans/org_admin_other_user', controller: 'paginable/plans', action: 'org_admin_other_user', + remote: true, scope: @plans, query_params: { sort_field: 'plans.updated_at', sort_direction: 'desc' }) %>
diff --git a/app/views/orgs/_merge_form.html.erb b/app/views/orgs/_merge_form.html.erb new file mode 100644 index 0000000000..bd04cfbefe --- /dev/null +++ b/app/views/orgs/_merge_form.html.erb @@ -0,0 +1,26 @@ +<%# locals: org, analysis %> + +

<%= _("Merge Organisations") %>

+ +
+
+

<%= sanitize(_("Please select the Organisation that will replace the current organisation: '%{org_name}'.") % { org_name: org.name }) %>

+

<%= _(" Then click the 'analyze' button to review the proposed changes. Note that no changes will take place until you have reviewed and approve of the changes.") %>

+ <%= form_for org, url: merge_analyze_super_admin_org_path(org), method: :post, remote: true do |form| %> + <%= render partial: "shared/org_selectors/local_only", + locals: { + form: form, + id_field: :id, + default_org: nil, + orgs: Org.includes(identifiers: :identifier_scheme).all.reject { |o| o == org }, + required: true + } %> + <%= form.button(_('Analyze'), class: "btn btn-primary", type: "submit") %> + <% end %> +
+
+ +
+
+
+
diff --git a/app/views/orgs/admin_edit.html.erb b/app/views/orgs/admin_edit.html.erb index 2d37230ce4..df5f67f0c8 100644 --- a/app/views/orgs/admin_edit.html.erb +++ b/app/views/orgs/admin_edit.html.erb @@ -25,6 +25,12 @@
  • <%= _('Schools/Departments') %>
  • + + <% if current_user.can_super_admin? %> +
  • + <%= _('Merge') %> +
  • + <% end %> @@ -61,6 +67,18 @@
    + + <% if current_user.can_super_admin? %> +
    +
    +
    +
    + <%= render partial: 'orgs/merge_form', locals: { org: org, analysis: nil } %> +
    +
    +
    +
    + <% end %> diff --git a/app/views/plans/_share_form.html.erb b/app/views/plans/_share_form.html.erb index 6fdfb2c748..f9ec141049 100644 --- a/app/views/plans/_share_form.html.erb +++ b/app/views/plans/_share_form.html.erb @@ -41,7 +41,7 @@

    <%= _('Manage collaborators')%>

    <%= _('Invite specific people to read, edit, or administer your plan. Invitees will receive an email notification that they have access to this plan.') %>

    -<% if @plan.roles.any? then %> +<% if @plan_roles.any? then %> @@ -108,6 +108,7 @@ <%= email_tooltip %> <%= user.email_field :email, class: "form-control", title: email_tooltip, aria: { required: true }, + required: true, date: { toggle: "tooltip", html: true } %> <% end %> @@ -122,7 +123,7 @@
    <%= f.label :administrator_access do %> - <%= f.radio_button :access, administrator.access, id: "role_administrator_access", "aria-required": true %> + <%= f.radio_button :access, administrator.access, id: "role_administrator_access", "aria-required": true, required: true %> <%= _('Co-owner') %> <% end %>
    diff --git a/app/views/public_pages/plan_index.html.erb b/app/views/public_pages/plan_index.html.erb index 6d91a3c698..5d5af35f21 100644 --- a/app/views/public_pages/plan_index.html.erb +++ b/app/views/public_pages/plan_index.html.erb @@ -22,6 +22,8 @@ partial: '/paginable/plans/publicly_visible', controller: 'paginable/plans', action: 'publicly_visible', + remote: false, + path_params: @args || {}, query_params: query_params, scope: @plans) %> <% end %> diff --git a/app/views/public_pages/template_index.html.erb b/app/views/public_pages/template_index.html.erb index 03aeb6f820..014d5db364 100644 --- a/app/views/public_pages/template_index.html.erb +++ b/app/views/public_pages/template_index.html.erb @@ -16,8 +16,9 @@ partial: '/paginable/templates/publicly_visible', controller: 'paginable/templates', action: 'publicly_visible', + remote: false, scope: @templates, - query_params: { sort_field: 'templates.title', sort_direction: :asc }) %> + query_params: @templates_query_params) %> <% end %>
    \ No newline at end of file diff --git a/app/views/shared/_search.html.erb b/app/views/shared/_search.html.erb index 01972d4fc2..7fb8c234c2 100644 --- a/app/views/shared/_search.html.erb +++ b/app/views/shared/_search.html.erb @@ -1,6 +1,6 @@ <%# locals: { search_term } %> -<%= form_tag(paginable_base_url(1), method: :get, remote: true, class: 'form-inline paginable-action') do %> +<%= form_tag(paginable_base_url(1), method: :get, remote: remote, class: 'form-inline paginable-action') do %>
    diff --git a/app/views/shared/export/_plan_coversheet.erb b/app/views/shared/export/_plan_coversheet.erb index e8076a31a8..3a50c75dcd 100644 --- a/app/views/shared/export/_plan_coversheet.erb +++ b/app/views/shared/export/_plan_coversheet.erb @@ -22,17 +22,29 @@

    <%= _("ORCID iD: ") %><%= orcid.value_without_scheme_prefix %>


    <% end %> - <% if @plan.grant_number.present? %> -

    <%= _("Grant number: ") %><%= @plan.grant_number %>


    - <% end %> - <% if @plan.description.present? %>

    <%= _("Project abstract: ") %>

    <%= sanitize(@plan.description) %>

    <% end %> + <% if @plan.id.present? %> +

    <%= _("ID: ") %><%= @plan.id %>


    + <% end %> + + <% if @plan.start_date.present? %> +

    <%= _("Start date: ") %><%= l(@plan.start_date.to_date, formats: :short) %>


    + <% end %> + + <% if @plan.end_date.present? %> +

    <%= _("End date: ") %><%= l(@plan.end_date.to_date, formats: :short) %>


    + <% end %> +

    <%= _("Last modified: ") %><%= l(@plan.updated_at.to_date, formats: :short) %>


    + <% if @plan.grant_number.present? %> +

    <%= _("Grant number / URL: ") %><%= @plan.grant_number %>


    + <% end %> + <% if @public_plan %>

    <%= _("Copyright information:") %>

    diff --git a/app/views/super_admin/api_clients/email_credentials.js.erb b/app/views/super_admin/api_clients/email_credentials.js.erb index ef512944f9..3b2bc22a3b 100644 --- a/app/views/super_admin/api_clients/email_credentials.js.erb +++ b/app/views/super_admin/api_clients/email_credentials.js.erb @@ -1,2 +1,3 @@ var msg = '<%= _("The credentials have been sent to %{email}.") % { email: @api_client.contact_email } %>'; renderNotice(msg); +toggleSpinner(false); diff --git a/app/views/super_admin/api_clients/index.html.erb b/app/views/super_admin/api_clients/index.html.erb index ed11626bf8..01801c75d0 100644 --- a/app/views/super_admin/api_clients/index.html.erb +++ b/app/views/super_admin/api_clients/index.html.erb @@ -17,6 +17,7 @@ partial: '/paginable/api_clients/index', controller: 'paginable/api_clients', action: 'index', + remote: true, scope: @api_clients, query_params: { sort_field: 'api_clients.name', sort_direction: :asc }) %>
    diff --git a/app/views/super_admin/api_clients/refresh_credentials.js.erb b/app/views/super_admin/api_clients/refresh_credentials.js.erb index 931e2c9750..07d0057d6e 100644 --- a/app/views/super_admin/api_clients/refresh_credentials.js.erb +++ b/app/views/super_admin/api_clients/refresh_credentials.js.erb @@ -3,3 +3,4 @@ var msg = '<%= @success ? _("Successfully regenerated the client credentials.") var context = $('#edit_api_client_<%= @api_client.id %>'); context.html('<%= escape_javascript(render partial: "/super_admin/api_clients/form") %>'); renderNotice(msg); +toggleSpinner(false); diff --git a/app/views/super_admin/notifications/index.html.erb b/app/views/super_admin/notifications/index.html.erb index f55c619940..f0b174a052 100644 --- a/app/views/super_admin/notifications/index.html.erb +++ b/app/views/super_admin/notifications/index.html.erb @@ -14,6 +14,7 @@ partial: '/paginable/notifications/index', controller: 'paginable/notifications', action: 'index', + remote: true, scope: notifications, query_params: { sort_field: 'notifications.starts_at', sort_direction: 'desc' }) %> diff --git a/app/views/super_admin/orgs/_analysis.html.erb b/app/views/super_admin/orgs/_analysis.html.erb new file mode 100644 index 0000000000..42bad41c4a --- /dev/null +++ b/app/views/super_admin/orgs/_analysis.html.erb @@ -0,0 +1,98 @@ +<%# locals: org, target_org %> + +<% +presenter = SuperAdmin::Orgs::MergePresenter.new(from_org: org, to_org: target_org) +orcid = IdentifierScheme.find_by(name: 'orcid') +%> +<%= form_for org, url: merge_commit_super_admin_org_path(org), method: :post, remote: false do |form| %> + +

    <%= _("Merging '%{from_org_name}' into '%{to_org_name}'") % { from_org_name: org.name, to_org_name: target_org.name } %>

    + +

    <%= _("Summary:") %>

    +

    <%= _("Counts of '%{to_org_name}' records before and after a merge with %{from_org_name}.") % { to_org_name: target_org.name, from_org_name: org.name } %>

    +
    +
    + + + + + + + + + <% presenter.categories.each do |category| %> + <% + to_entries = presenter.to_org_entries[category] + mergeable_entries = presenter.mergeable_entries[category] + %> + + + + + + <% end %> + +
    <%= _("Record Type") %><%= _("'%{to_org_name}' Count - before merge") % { to_org_name: presenter.to_org_name } %><%= _("'%{to_org_name}' Count - after merge") % { to_org_name: presenter.to_org_name } %>
    <%= category.capitalize %><%= to_entries.length %><%= to_entries.length + mergeable_entries.length %>
    + + +

    <%= _("Details:") %>

    +

    <%= _("The table below shows the specific changes that will occur during the merge process. Once the merge is complete, '%{from_org_name}' will be deleted. You can then consolidate/correct any merge issues (e.g. remove any departments that were duplicated)") % { from_org_name: org.name } %>

    +
    + + + + + + + + + + + + <% + org_merge_cols = presenter.mergeable_attributes + org_from_cols = presenter.from_org_attributes.select { |col| org_merge_cols.include?(col) } + org_to_cols = presenter.to_org_attributes.select { |col| org_merge_cols.include?(col) } + %> + + + + + + + <% presenter.categories.each do |category| %> + <% + from_entries = presenter.from_org_entries[category] + to_entries = presenter.to_org_entries[category] + mergeable_entries = presenter.mergeable_entries[category] + %> + <% next unless mergeable_entries.length > 0 %> + + + + + + + + <% end %> + +
    <%= _('Record Type') %><%= _("%{from_org_name} - Before") % { from_org_name: presenter.from_org_name } %><%= _("%{to_org_name} - Before") % { to_org_name: presenter.to_org_name } %><%= _("Action") %>
    <%= _("Org") %><%= sanitize(org_column_content(attributes: org_from_cols)) %><%= sanitize(org_column_content(attributes: org_to_cols)) %> +

    <%= _("The following attributes will be updated") if org_merge_cols.any? %>

    + <%= sanitize(org_column_content(attributes: org_merge_cols)) %> +
    <%= category.capitalize %><%= sanitize(column_content(entries: from_entries, orcid: orcid)) %><%= sanitize(column_content(entries: to_entries, orcid: orcid)) %> + <%= sanitize(merge_column_content( + entries: mergeable_entries, orcid: orcid, to_org_name: presenter.to_org_name) + ) %> +
    +
    + + <%= form.hidden_field :target_org, value: target_org.id %> + +

    <%= _("Warning: The changes proposed above will be committed to the database. '%{org_name}' will then be deleted.") % { org_name: org.name } %>

    +

    <%= _("These changes cannot be undone! Do not proceed unless you are certain.") %>

    +

    <%= _("You may find it helpful to make a note of the expected number of records for each category that will be merged before continuing.") %>

    + <% if org.plans.select(&:feedback_requested?).any? %> +

    <%= _("Warning: '%{from_org_name}' has Plans that are currently in the review process. The Org admins may not be able to see these plans after the merge!") % { from_org_name: org.name } %>

    + <% end %> + <%= form.button(_('Merge records'), class: "btn btn-primary", type: "submit") %> +<% end %> diff --git a/app/views/super_admin/orgs/index.html.erb b/app/views/super_admin/orgs/index.html.erb index ef42981c60..dd6d521cbc 100644 --- a/app/views/super_admin/orgs/index.html.erb +++ b/app/views/super_admin/orgs/index.html.erb @@ -4,7 +4,7 @@

    <%= _('Organisations') %> - <%= _('Create Organisation') %>

    @@ -16,6 +16,7 @@ partial: '/paginable/orgs/index', controller: 'paginable/orgs', action: 'index', + remote: true, scope: orgs, query_params: { sort_field: 'orgs.name', sort_direction: :asc }) %>
    diff --git a/app/views/super_admin/orgs/merge_analyze.js.erb b/app/views/super_admin/orgs/merge_analyze.js.erb new file mode 100644 index 0000000000..a3d0dfd27b --- /dev/null +++ b/app/views/super_admin/orgs/merge_analyze.js.erb @@ -0,0 +1,10 @@ +var target = $('#merge-analysis'); + +if (target != undefined) { + <% if @org.present? && @target_org.present? %> + target.html(`<%= escape_javascript(render partial: "super_admin/orgs/analysis", locals: { org: @org, target_org: @target_org }) %>`); + <% else %> + target.html(`<%= _("Something went wrong and we were unable to analyze a merge between these two organisations.") %>`); + <% end %> +} +toggleSpinner(false); diff --git a/app/views/super_admin/themes/index.html.erb b/app/views/super_admin/themes/index.html.erb index ebc567d861..77557e6013 100644 --- a/app/views/super_admin/themes/index.html.erb +++ b/app/views/super_admin/themes/index.html.erb @@ -14,6 +14,7 @@ partial: '/paginable/themes/index', controller: 'paginable/themes', action: 'index', + remote: true, scope: themes, query_params: { sort_field: 'themes.title', sort_direction: :asc }) %> diff --git a/app/views/usage/plans_by_template.js.erb b/app/views/usage/plans_by_template.js.erb index f5e2b56519..ae720a8e39 100644 --- a/app/views/usage/plans_by_template.js.erb +++ b/app/views/usage/plans_by_template.js.erb @@ -8,3 +8,4 @@ window.templatePlansChart.dispatchEvent( detail: JSON.parse('<%= prep_data_for_template_plans_chart(data: @plans_per_month).html_safe %>') }) ); +toggleSpinner(false); diff --git a/app/views/users/admin_index.html.erb b/app/views/users/admin_index.html.erb index f14fc59f84..698f261eb6 100644 --- a/app/views/users/admin_index.html.erb +++ b/app/views/users/admin_index.html.erb @@ -26,6 +26,7 @@ partial: '/paginable/users/index', controller: 'paginable/users', action: 'index', + remote: true, scope: @users, view_all: !current_user.can_super_admin?) %>
    diff --git a/app/views/users/refresh_token.js.erb b/app/views/users/refresh_token.js.erb index bd447a5e45..1c7f52e44a 100644 --- a/app/views/users/refresh_token.js.erb +++ b/app/views/users/refresh_token.js.erb @@ -3,3 +3,4 @@ var msg = '<%= @success ? _("Successfully regenerate your API token.") : _("Unab var context = $('#api-token'); context.html('<%= escape_javascript(render partial: "/devise/registrations/api_token", locals: { user: current_user }) %>'); renderNotice(msg); +toggleSpinner(false); diff --git a/config.ru b/config.ru index 1315d135c3..08dee95cd3 100644 --- a/config.ru +++ b/config.ru @@ -3,4 +3,6 @@ # This file is used by Rack-based servers to start the application. require File.expand_path(File.dirname(__FILE__) + "/config/environment") -run DMPRoadmap::Application +map ENV["RAILS_RELATIVE_URL_ROOT"] || "/" do + run DMPRoadmap::Application +end diff --git a/config/locale/app.pot b/config/locale/app.pot index d9c1d9b762..82d2a92974 100644 --- a/config/locale/app.pot +++ b/config/locale/app.pot @@ -5296,7 +5296,8 @@ msgstr "" #: ../../app/views/user_mailer/sharing_notification.html.erb:5 msgid "" -"Your colleague %{inviter_name} has invited you to contribute to \n" +"Your colleague %{inviter_name} has invited you to contribute to +\n" " their Data Management Plan in %{tool_name}" msgstr "" diff --git a/config/locale/de/app.po b/config/locale/de/app.po index 8aeac2e9ad..31065348a8 100644 --- a/config/locale/de/app.po +++ b/config/locale/de/app.po @@ -5876,10 +5876,12 @@ msgstr " in einem Plan namens" #: ../../app/views/user_mailer/sharing_notification.html.erb:5 msgid "" -"Your colleague %{inviter_name} has invited you to contribute to \n" +"Your colleague %{inviter_name} has invited you to contribute to +\n" " their Data Management Plan in %{tool_name}" msgstr "" -"Ihr Kollege %{inviter_name} hat Sie eingeladen, einen Beitrag zu leisten \n" +"Ihr Kollege %{inviter_name} hat Sie eingeladen, einen Beitrag zu leisten +\n" " ihren Datenverwaltungsplan in %{tool_name}" #: ../../app/views/user_mailer/welcome_notification.html.erb:2 diff --git a/config/locale/en_CA/app.po b/config/locale/en_CA/app.po index 9c3ce7dd9e..80c27ea12b 100644 --- a/config/locale/en_CA/app.po +++ b/config/locale/en_CA/app.po @@ -5296,7 +5296,8 @@ msgstr "" #: ../../app/views/user_mailer/sharing_notification.html.erb:5 msgid "" -"Your colleague %{inviter_name} has invited you to contribute to \n" +"Your colleague %{inviter_name} has invited you to contribute to +\n" " their Data Management Plan in %{tool_name}" msgstr "" diff --git a/config/locale/en_GB/app.po b/config/locale/en_GB/app.po index 74730b9c6f..5fb8b7bb63 100644 --- a/config/locale/en_GB/app.po +++ b/config/locale/en_GB/app.po @@ -5298,7 +5298,8 @@ msgstr "" #: ../../app/views/user_mailer/sharing_notification.html.erb:5 msgid "" -"Your colleague %{inviter_name} has invited you to contribute to \n" +"Your colleague %{inviter_name} has invited you to contribute to +\n" " their Data Management Plan in %{tool_name}" msgstr "" diff --git a/config/locale/en_US/app.po b/config/locale/en_US/app.po index 1437dd82d2..9f23272835 100644 --- a/config/locale/en_US/app.po +++ b/config/locale/en_US/app.po @@ -5387,7 +5387,8 @@ msgstr "" #: ../../app/views/user_mailer/sharing_notification.html.erb:5 msgid "" -"Your colleague %{inviter_name} has invited you to contribute to \n" +"Your colleague %{inviter_name} has invited you to contribute to +\n" " their Data Management Plan in %{tool_name}" msgstr "" diff --git a/config/locale/es/app.po b/config/locale/es/app.po index d431299a98..e221283103 100644 --- a/config/locale/es/app.po +++ b/config/locale/es/app.po @@ -5822,10 +5822,12 @@ msgstr " en un plan llamado" #: ../../app/views/user_mailer/sharing_notification.html.erb:5 msgid "" -"Your colleague %{inviter_name} has invited you to contribute to \n" +"Your colleague %{inviter_name} has invited you to contribute to +\n" " their Data Management Plan in %{tool_name}" msgstr "" -"Tu colega %{inviter_name} te ha invitado a contribuir a \n" +"Tu colega %{inviter_name} te ha invitado a contribuir a +\n" " su plan de gestión de datos en %{tool_name}" #: ../../app/views/user_mailer/welcome_notification.html.erb:2 diff --git a/config/locale/fi/app.po b/config/locale/fi/app.po index 8118a1d865..6fe737c16a 100644 --- a/config/locale/fi/app.po +++ b/config/locale/fi/app.po @@ -5771,10 +5771,12 @@ msgstr " suunnitelmassa nimeltään" #: ../../app/views/user_mailer/sharing_notification.html.erb:5 msgid "" -"Your colleague %{inviter_name} has invited you to contribute to \n" +"Your colleague %{inviter_name} has invited you to contribute to +\n" " their Data Management Plan in %{tool_name}" msgstr "" -"Kollegasi %{inviter_name} on kutsunut sinut osallistumaan \n" +"Kollegasi %{inviter_name} on kutsunut sinut osallistumaan +\n" " heidän tiedonhallintasuunnitelmansa %{tool_name}" #: ../../app/views/user_mailer/welcome_notification.html.erb:2 diff --git a/config/locale/fr_CA/app.po b/config/locale/fr_CA/app.po index fba1233f5f..9d1e3561a6 100644 --- a/config/locale/fr_CA/app.po +++ b/config/locale/fr_CA/app.po @@ -5914,7 +5914,8 @@ msgstr " dans un plan appelé" #: ../../app/views/user_mailer/sharing_notification.html.erb:5 msgid "" -"Your colleague %{inviter_name} has invited you to contribute to \n" +"Your colleague %{inviter_name} has invited you to contribute to +\n" " their Data Management Plan in %{tool_name}" msgstr "" "Votre collègue %{inviter_name} vous a invité(e) à contribuer à son plan de ges" diff --git a/config/locale/fr_FR/app.po b/config/locale/fr_FR/app.po index a381c8efa2..4f06e8c7ca 100644 --- a/config/locale/fr_FR/app.po +++ b/config/locale/fr_FR/app.po @@ -5861,10 +5861,12 @@ msgstr " dans un plan appelé" #: ../../app/views/user_mailer/sharing_notification.html.erb:5 msgid "" -"Your colleague %{inviter_name} has invited you to contribute to \n" +"Your colleague %{inviter_name} has invited you to contribute to +\n" " their Data Management Plan in %{tool_name}" msgstr "" -"Votre collègue %{inviter_name} vous a invité à contribuer à \n" +"Votre collègue %{inviter_name} vous a invité à contribuer à +\n" " leur plan de gestion des données dans %{tool_name}" #: ../../app/views/user_mailer/welcome_notification.html.erb:2 diff --git a/config/locale/pt_BR/app.po b/config/locale/pt_BR/app.po index 2cc6d858e5..4dc90d2393 100644 --- a/config/locale/pt_BR/app.po +++ b/config/locale/pt_BR/app.po @@ -5796,10 +5796,12 @@ msgstr " em um plano chamado" #: ../../app/views/user_mailer/sharing_notification.html.erb:5 msgid "" -"Your colleague %{inviter_name} has invited you to contribute to \n" +"Your colleague %{inviter_name} has invited you to contribute to +\n" " their Data Management Plan in %{tool_name}" msgstr "" -"Seu colega %{inviter_name} convidou você para contribuir com \n" +"Seu colega %{inviter_name} convidou você para contribuir com +\n" " seu plano de gerenciamento de dados em %{tool_name}" #: ../../app/views/user_mailer/welcome_notification.html.erb:2 diff --git a/config/locale/sv_FI/app.po b/config/locale/sv_FI/app.po index 61c51850df..c3cb929b63 100644 --- a/config/locale/sv_FI/app.po +++ b/config/locale/sv_FI/app.po @@ -5747,10 +5747,12 @@ msgstr " i en plan som heter" #: ../../app/views/user_mailer/sharing_notification.html.erb:5 msgid "" -"Your colleague %{inviter_name} has invited you to contribute to \n" +"Your colleague %{inviter_name} has invited you to contribute to +\n" " their Data Management Plan in %{tool_name}" msgstr "" -"Din kollega %{inviter_name} har bjudit in dig att bidra till \n" +"Din kollega %{inviter_name} har bjudit in dig att bidra till +\n" " sin datahanteringsplan i %{tool_name}" #: ../../app/views/user_mailer/welcome_notification.html.erb:2 diff --git a/config/locale/tr_TR/app.po b/config/locale/tr_TR/app.po index 1d5fb3d9c6..8917c5ae50 100644 --- a/config/locale/tr_TR/app.po +++ b/config/locale/tr_TR/app.po @@ -5296,7 +5296,8 @@ msgstr "" #: ../../app/views/user_mailer/sharing_notification.html.erb:5 msgid "" -"Your colleague %{inviter_name} has invited you to contribute to \n" +"Your colleague %{inviter_name} has invited you to contribute to +\n" " their Data Management Plan in %{tool_name}" msgstr "" diff --git a/config/locales/translation.tr-TR.yml b/config/locales/translation.tr-TR.yml index 9f2cb1783d..29c2545059 100644 --- a/config/locales/translation.tr-TR.yml +++ b/config/locales/translation.tr-TR.yml @@ -72,8 +72,8 @@ tr-TR: mb: MB gb: GB tb: TB - pb: - eb: + pb: PB + eb: EB decimal_units: units: thousand: Bin @@ -121,8 +121,8 @@ tr-TR: not_found: bulunamadı not_locked: kilitlenmemiş not_saved: - one: - other: + one: '1 hata bu %{resource} dosyasının kaydedilmesini engelledi:' + other: "%{count} hata bu %{resource} dosyasının kaydedilmesini engelledi:" template: body: 'Lütfen aşağıdaki hataları düzeltiniz:' header: @@ -193,30 +193,31 @@ tr-TR: submit: "%{model} Kaydet" page_entries_info: entry: - zero: - one: - other: + zero: girdiler + one: giriş + other: girdiler one_page: display_entries: - zero: - one: - other: + zero: "%{entry_name} bulunamadı" + one: "1 %{entry_name} görüntüleniyor" + other: "Tüm %{count} %{entry_name} görüntüleniyor" more_pages: display_entries: Toplam %{total} %{entry_name}; %{first} - %{last} - arası gösteriliyor. + arası görüntüleniyor. flash: actions: create: - notice: + notice: "%{resource_name} başarıyla oluşturuldu." update: - notice: + notice: "%{resource_name} başarıyla güncellendi." destroy: - notice: - alert: + notice: "%{resource_name} başarıyla yok edildi." + alert: "%{resource_name} yok edilemedi." recaptcha: errors: - verification_failed: - recaptcha_unreachable: + verification_failed: reCAPTCHA doğrulaması başarısız oldu, lütfen tekrar deneyin. + recaptcha_unreachable: Hata! reCAPTCHA yanıtınızı doğrulayamadık. Lütfen tekrar + deneyin. devise: confirmations: confirmed: Hesabınız başarıyla onaylandı. Şu an giriş yapmış bulunuyorsunuz. @@ -235,39 +236,43 @@ tr-TR: timeout: Oturumunuz sonlandı. Devam etmek için, lütfen yeniden giriş yapınız. unauthenticated: Devam etmeden önce giriş yapmanız veya kayıt olmanız gereklidir. unconfirmed: Devam etmeden önce hesabınızı onaylamalısınız. - invited: + invited: Bekleyen bir davetiniz var, hesabınızın oluşturulmasını tamamlamak + için daveti kabul ediniz. mailer: confirmation_instructions: subject: Onaylama talimatları reset_password_instructions: - subject: Parola sıfırlama talimatları + subject: Şifre sıfırlama talimatları unlock_instructions: - subject: Kilit açma talimatları + subject: Kilit kaldırma talimatları email_changed: subject: E-posta Değiştirildi password_change: - subject: Parola Değişti + subject: Şifre Değişti invitation_instructions: - subject: - hello: - someone_invited_you: - accept: - accept_until: - ignore: + subject: Davetiye yönergeleri + hello: Merhaba %{email} + someone_invited_you: Birisi sizi %{url} adresine davet etti, aşağıdaki bağlantıya + tıklayarak daveti kabul edebilirsiniz. + accept: Daveti kabul et + accept_until: Bu davetiye %{due_date} tarihine kadar geçerli olacaktır. + ignore: Daveti kabul etmek istemiyorsanız, lütfen bu e-postayı dikkate almayın. + Hesabınızın oluşturulması için yukarıdaki bağlantıya tıklayıp şifrenizi + belirlemeniz gerekmektedir. omniauth_callbacks: failure: Sizi %{kind} aracılığıyla tanıyamadık çünkü "%{reason}". success: "%{kind} aracılığıyla onaylandınız." passwords: - no_token: Bu sayfaya parola sıfırlama e-postasındaki bağlantıya tıklayarak gelmeniz + no_token: Bu sayfaya şifre sıfırlama e-postasındaki bağlantıya tıklayarak gelmeniz gerekmektedir. Bağlantıya tıklamak yerine kopyaladıysanız, hata yapmış olabilirsiniz. - Lütfen parola sıfırlama e-postasındaki bağlantıya tıklayın. - send_instructions: Birkaç dakika içinde parolanızı nasıl sıfırlayacağınız ile + Lütfen şifre sıfırlama e-postasındaki bağlantıya tıklayın. + send_instructions: Birkaç dakika içinde şifrenizi nasıl sıfırlayacağınız ile ilgili bir e-posta alacaksınız. send_paranoid_instructions: Eğer e-posta adresiniz sistemimizde kayıtlı ise, - birkaç dakika içinde parolanızı nasıl sıfırlayacağınız ile ilgili bir e-posta + birkaç dakika içinde şifrenizi nasıl sıfırlayacağınız ile ilgili bir e-posta alacaksınız. - updated: Parolanız başarıyla güncellendi. Şu an giriş yapmış bulunuyorsunuz. - updated_not_active: Parolanız başarıyla güncellendi. + updated: Şifreniz başarıyla değiştirildi. Şu an giriş yapmış bulunuyorsunuz. + updated_not_active: Şifreniz başarıyla değiştirildi. registrations: destroyed: Hesabınız başarıyla kapatıldı. En kısa sürede görüşmek üzere! signed_up: Başarıyla giriş yaptınız. Hoşgeldiniz! @@ -282,8 +287,8 @@ tr-TR: adresinizin onaylanması gerek. Yeni e-posta hesabınızın onaylanması için lütfen size gönderdiğimiz e-postada bulunan onay bağlantısını açın. updated: Hesabınız başarıyla güncellendi. - updated_but_not_signed_in: Hesabınız başarıyla güncellendi, ancak parola değiştirildiğinde - yeniden giriş yapmalısınız + updated_but_not_signed_in: Hesabınız başarıyla güncellendi, ancak şifreniz değiştirildiği + için tekrar giriş yapmanız gerekmekte. sessions: signed_in: Başarıyla giriş yapıldı. signed_out: Başarıyla çıkış yapıldı. @@ -293,49 +298,49 @@ tr-TR: içeren bir e-posta alacaksınız. send_paranoid_instructions: Eğer hesabınız varsa, birkaç dakika içerisinde hesap kilidi açma talimatlarını içeren bir e-posta alacaksınız. - unlocked: Hesabınızın kilidi başarıyla açıldı. Devam etmek için lütfen giriş + unlocked: Hesabınızın kilidi başarıyla kaldırıldı. Devam etmek için lütfen giriş yapın. invitations: - send_instructions: - invitation_token_invalid: - updated: - updated_not_active: - no_invitations_remaining: - invitation_removed: + send_instructions: "%{email} adresine bir davet e-postası gönderildi." + invitation_token_invalid: Sağlanan davetiye tokenı geçerli değil! + updated: Şifreniz başarıyla ayarlandı. Başarılı bir şekilde oturum açtınız. + updated_not_active: Şifreniz başarıyla ayarlandı. + no_invitations_remaining: Davetiye kalmadı + invitation_removed: Davetiniz kaldırıldı. new: - header: - submit_button: + header: Davetiye gönder + submit_button: Bir davet yolla edit: - header: - submit_button: + header: Şifrenizi ayarlayın + submit_button: Şifremi ayarla contact_us: contact_mailer: contact_email: - sent_by_contact_form: - sent_by_name: - subject: + sent_by_contact_form: "%{email} e-posta adresinden iletişim formu ile gönderilmiştir" + sent_by_name: "%{email} e-posta adresinden %{name} tarafından gönderilmiştir" + subject: "%{email} e-posta adresinden gelen Bize Ulaşın mesajı" contacts: new: - contact_us: - email: - message: - name: - subject: - submit: + contact_us: Bize Ulaşın + email: E-posta + message: İleti + name: isim + subject: Konu + submit: Gönder new_formtastic: - contact_us: - email: - message: - name: - subject: - submit: + contact_us: Bize Ulaşın + email: E-posta + message: İleti + name: İsim + subject: Konu + submit: Gönder new_simple_form: - contact_us: - email: - message: - name: - subject: - submit: + contact_us: Bize Ulaşın + email: E-posta + message: İleti + name: İsim + subject: Konu + submit: Gönder notices: - error: - success: + error: Her iki alanı da doldurmalısınız. + success: İletişim e-postası başarıyla gönderildi. diff --git a/config/routes.rb b/config/routes.rb index fb0a52c57c..1960a85ec2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -323,7 +323,13 @@ end namespace :super_admin do - resources :orgs, only: %i[index new create destroy] + resources :orgs, only: %i[index new create destroy] do + member do + post "merge_analyze" + post "merge_commit" + end + end + resources :themes, only: %i[index new create edit update destroy] resources :users, only: %i[edit update] do member do diff --git a/db/migrate/20201208192403_drop_org_identifiers.rb b/db/migrate/20201208192403_drop_org_identifiers.rb new file mode 100644 index 0000000000..de935890af --- /dev/null +++ b/db/migrate/20201208192403_drop_org_identifiers.rb @@ -0,0 +1,5 @@ +class DropOrgIdentifiers < ActiveRecord::Migration[5.2] + def change + drop_table :org_identifiers + end +end diff --git a/db/schema.rb b/db/schema.rb index c1624f0d8d..afeebf74be 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -200,17 +200,6 @@ t.boolean "enabled", default: true end - create_table "org_identifiers", id: :integer, force: :cascade do |t| - t.string "identifier" - t.integer "identifier_scheme_id" - t.string "attrs" - t.datetime "created_at" - t.datetime "updated_at" - t.integer "org_id" - t.index ["identifier_scheme_id"], name: "fk_rails_189ad2e573" - t.index ["org_id"], name: "fk_rails_36323c0674" - end - create_table "org_token_permissions", id: :integer, force: :cascade do |t| t.integer "org_id" t.integer "token_permission_type_id" @@ -553,8 +542,6 @@ add_foreign_key "notes", "users" add_foreign_key "notification_acknowledgements", "notifications" add_foreign_key "notification_acknowledgements", "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" diff --git a/spec/controllers/super_admin/orgs_controller_spec.rb b/spec/controllers/super_admin/orgs_controller_spec.rb new file mode 100644 index 0000000000..a463355a86 --- /dev/null +++ b/spec/controllers/super_admin/orgs_controller_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe SuperAdmin::OrgsController, type: :controller do + + before(:each) do + @scheme = create(:identifier_scheme) + tpt = create(:token_permission_type) + @from_org = create(:org, :funder, templates: 1, plans: 2, managed: true, + feedback_enabled: true, + token_permission_types: [tpt]) + create(:annotation, org: @from_org) + create(:department, org: @from_org) + gg = @from_org.guidance_groups.first if @from_org.guidance_groups.any? + gg = create(:guidance_group, org: @from_org) unless gg.present? + create(:guidance, guidance_group: gg) + create(:identifier, identifiable: @from_org, identifier_scheme: nil) + create(:identifier, identifiable: @from_org, identifier_scheme: @scheme) + create(:plan, funder: @from_org) + create(:tracker, org: @from_org) + create(:user, org: @from_org) + + @from_org.reload + @to_org = create(:org, :institution, plans: 2, managed: false) + @user = create(:user, :super_admin) + + @controller = described_class.new + sign_in(@user) + end + + describe "POST /super_admin/:id/merge_analyze", js: true do + before(:each) do + @params = { + "id": @from_org.id, + # Send over the Org typehead json in the org.id field so the service can unpackage it + "org": { "id": { "id": @to_org.id, "name": @to_org.name }.to_json } + } + end + + it "fails if user is not a super admin" do + sign_in(create(:user)) + post :merge_analyze, params: @params + expect(response.code).to eql("302") + expect(response).to redirect_to(plans_path) + expect(flash[:alert].present?).to eql(true) + end + it "succeeds in analyzing the Orgs" do + post :merge_analyze, params: @params, format: :js + expect(response.code).to eql("200") + expect(assigns(:org)).to eql(@from_org) + expect(assigns(:target_org)).to eql(@to_org) + expect(response).to render_template(:merge_analyze) + end + end + + describe "POST /super_admin/:id/merge_commit", js: true do + context "standard question type (no question_options and not RDA metadata)" do + before(:each) do + @params = { "id": @from_org.id, "org": { "target_org": @to_org.id } } + end + + it "fails if user is not a super admin" do + sign_in(create(:user)) + post :merge_commit, params: @params, format: :js + expect(response.code).to eql("302") + expect(response).to redirect_to(plans_path) + expect(flash[:alert].present?).to eql(true) + end + it "fails if :target_org is not found" do + @params[:org][:target_org] = 9999 + post :merge_commit, params: @params, format: :js + expect(response.code).to eql("302") + expect(response).to redirect_to(admin_edit_org_path(@from_org)) + expect(flash[:alert].present?).to eql(true) + end + it "succeeds and redirects properly" do + post :merge_commit, params: @params, format: :js + expect(response.code).to eql("302") + expect(response).to redirect_to(super_admin_orgs_path) + end + end + end + +end diff --git a/spec/factories/trackers.rb b/spec/factories/trackers.rb index 09a1926078..875480d4e1 100644 --- a/spec/factories/trackers.rb +++ b/spec/factories/trackers.rb @@ -3,6 +3,6 @@ FactoryBot.define do factory :tracker do org { nil } - code { "MyString" } + code { "UA-#{Faker::Number.number(digits: 5)}-#{Faker::Number.number(digits: 2)}" } end end diff --git a/spec/features/super_admins/merge_org_spec.rb b/spec/features/super_admins/merge_org_spec.rb new file mode 100644 index 0000000000..3f2558b88a --- /dev/null +++ b/spec/features/super_admins/merge_org_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "SuperAdmins Merge Orgs", type: :feature, js: true do + + before do + Org.destroy_all + @scheme = create(:identifier_scheme) + tpt = create(:token_permission_type) + @from_org = create(:org, :organisation, templates: 1, plans: 2, managed: true, + feedback_enabled: true, + token_permission_types: [tpt]) + create(:annotation, org: @from_org) + create(:department, org: @from_org) + gg = @from_org.guidance_groups.first if @from_org.guidance_groups.any? + gg = create(:guidance_group, org: @from_org) unless gg.present? + create(:guidance, guidance_group: gg) + create(:identifier, identifiable: @from_org, identifier_scheme: nil) + create(:identifier, identifiable: @from_org, identifier_scheme: @scheme) + create(:plan, funder: @from_org) + create(:tracker, org: @from_org) + create(:user, org: @from_org) + + @to_org = create(:org, :institution, plans: 2, managed: false) + + @user = create(:user, :super_admin, org: create(:org)) + sign_in(@user) + end + + scenario "Super admin merges an Org into another Org" do + org_name = @from_org.name + click_link "Admin" + sleep(0.5) + click_link "Organisations" + + fill_in(:search, with: @from_org.name) + click_button "Search" + sleep(0.5) + + first("#org-#{@from_org.id}-actions").click + first("a[href=\"/org/admin/#{@from_org.id}/admin_edit\"]").click + + click_link "Merge" + sleep(0.3) + expect(page).to have_text("Merge Organisations") + find("#org_org_name").click + fill_in(:org_org_name, with: @to_org.name[0..6]) + sleep(0.5) + choose_suggestion(@to_org.name) + + click_button "Analyze" + # Wait for response + sleep(0.3) + expect(page).to have_text("Summary:") + + click_button "Merge records" + # Wait for redirect + sleep(0.3) + expect(page).to have_text("Organisations") + expect(page).to have_text("Successfully merged") + + # Make sure that the correct org was deleted + expect(Org.where(name: org_name).any?).to eql(false) + expect(Org.where(name: @to_org.name).any?).to eql(true) + + # Make sure the Org we merged is no longer findable + find("#search").click + fill_in(:search, with: org_name) + click_button "Search" + sleep(0.3) + expect(page).to have_text("There are no records associated") + + # Make sure the Org we merged into is findable + find("#search").click + fill_in(:search, with: @to_org.name) + click_button "Search" + sleep(0.3) + expect(page).to have_text(@to_org.name) + end + +end diff --git a/spec/models/guidance_group_spec.rb b/spec/models/guidance_group_spec.rb index 4a7123e370..2d2dbde30a 100644 --- a/spec/models/guidance_group_spec.rb +++ b/spec/models/guidance_group_spec.rb @@ -210,5 +210,45 @@ end end + + context ":merge!(to_be_merged:)" do + before(:each) do + org = create(:org) + @guidance_group = create(:guidance_group, org: org) + @to_be_merged = create(:guidance_group, org: org, plans: [create(:plan)], + guidances: [create(:guidance)]) + end + + it "returns false if to_be_merged is not a GuidanceGroup" do + result = @guidance_group.merge!(to_be_merged: build(:user)) + expect(result).to eql(@guidance_group) + end + it "occurs inside a transaction" do + GuidanceGroup.any_instance.stubs(:save).returns(false) + result = @guidance_group.merge!(to_be_merged: @to_be_merged) + expect(result).to eql(nil) + # Since the save will fail and we reload the Object it should be valid + expect(@guidance_group.valid?).to eql(true) + expect(@to_be_merged.reload.new_record?).to eql(false) + expect(@to_be_merged.guidances.length).not_to eql(0) + end + it "merges associated :plans" do + expected = @guidance_group.plans.length + @to_be_merged.plans.length + @guidance_group.merge!(to_be_merged: @to_be_merged) + expect(@guidance_group.plans.length).to eql(expected) + end + it "merges associated :guidances" do + expected = @guidance_group.guidances.length + @to_be_merged.guidances.length + @guidance_group.merge!(to_be_merged: @to_be_merged) + expect(@guidance_group.guidances.length).to eql(expected) + end + it "removes the :to_be_merged GuidanceGroup" do + original_id = @to_be_merged.id + expect(@guidance_group.merge!(to_be_merged: @to_be_merged)).to eql(@guidance_group) + expect(Guidance.where(guidance_group_id: original_id).any?).to eql(false) + expect(GuidanceGroup.find_by(id: original_id).present?).to eql(false) + end + end + end end diff --git a/spec/models/org_spec.rb b/spec/models/org_spec.rb index 48b12ee800..565caa9c8e 100644 --- a/spec/models/org_spec.rb +++ b/spec/models/org_spec.rb @@ -392,7 +392,7 @@ plan.add_user!(user.id, :editor) end - it { is_expected.not_to include(plan) } + it { is_expected.to include(plan) } end @@ -402,7 +402,7 @@ plan.add_user!(user.id, :commenter) end - it { is_expected.not_to include(plan) } + it { is_expected.to include(plan) } end @@ -412,7 +412,70 @@ plan.add_user!(user.id, :reviewer) end - it { is_expected.not_to include(plan) } + it { is_expected.to include(plan) } + + end + + end + + describe "#org_admin_plans" do + + let!(:org) { create(:org) } + let!(:plan) { create(:plan, org: org) } + let!(:user) { create(:user, org: org) } + + subject { org.org_admin_plans } + + context "when user belongs to Org and plan owner with role :creator" do + + before do + create(:role, :creator, user: user, plan: plan) + plan.add_user!(user.id, :creator) + end + + it { is_expected.to include(plan) } + + end + + context "when user belongs to Org and plan user with role :administrator" do + + before do + plan.add_user!(user.id, :administrator) + end + + it { + is_expected.to include(plan) + } + + end + + context "user belongs to Org and plan user with role :editor, but not :creator and :admin" do + + before do + plan.add_user!(user.id, :editor) + end + + it { is_expected.to include(plan) } + + end + + context "user belongs to Org and plan user with role :commenter, but not :creator and :admin" do + + before do + plan.add_user!(user.id, :commenter) + end + + it { is_expected.to include(plan) } + + end + + context "user belongs to Org and plan user with role :reviewer, but not :creator and :admin" do + + before do + plan.add_user!(user.id, :reviewer) + end + + it { is_expected.to include(plan) } end @@ -463,4 +526,232 @@ end end + context ":merge!(to_be_merged:)" do + before(:each) do + @scheme = create(:identifier_scheme) + tpt = create(:token_permission_type) + @org = create(:org, :organisation) + + @to_be_merged = create(:org, :funder, templates: 1, plans: 2, managed: true, + token_permission_types: [tpt]) + create(:annotation, org: @to_be_merged) + create(:department, org: @to_be_merged) + gg = @to_be_merged.guidance_groups.first if @to_be_merged.guidance_groups.any? + gg = create(:guidance_group, org: @to_be_merged) unless gg.present? + create(:guidance, guidance_group: gg) + create(:identifier, identifiable: @to_be_merged, identifier_scheme: nil) + create(:identifier, identifiable: @to_be_merged, identifier_scheme: @scheme) + create(:plan, funder_id: @to_be_merged.id) + create(:tracker, org: @to_be_merged) + create(:user, org: @to_be_merged) + @to_be_merged.reload + end + + it "returns false if to_be_merged is not an Org" do + result = @org.merge!(to_be_merged: build(:user)) + expect(result).to eql(@org) + end + it "occurs inside a transaction" do + Org.any_instance.stubs(:save).returns(false) + result = @org.merge!(to_be_merged: @to_be_merged) + expect(result).to eql(nil) + # Since the save will fail and we reload the Object it should be valid + expect(@org.valid?).to eql(true) + expect(@to_be_merged.reload.new_record?).to eql(false) + expect(@to_be_merged.annotations.length).not_to eql(0) + end + it "merges attributes" do + original = @to_be_merged.dup + org = @org.merge!(to_be_merged: @to_be_merged) + expect(org.links).to eql(original.links) + end + it "merges associated :annotations" do + expected = @org.annotations.length + @to_be_merged.annotations.length + @org.merge!(to_be_merged: @to_be_merged) + expect(@org.annotations.length).to eql(expected) + end + it "merges associated :departments" do + expected = @org.departments.length + @to_be_merged.departments.length + @org.merge!(to_be_merged: @to_be_merged) + expect(@org.departments.length).to eql(expected) + end + it "merges associated :funded_plans" do + expected = @org.funded_plans.length + @to_be_merged.funded_plans.length + @org.merge!(to_be_merged: @to_be_merged) + expect(@org.funded_plans.length).to eql(expected) + end + it "merges associated :guidances" do + expected = (@org.guidance_groups.first&.guidances&.length || 0) + + @to_be_merged.guidance_groups.first.guidances.length + @org.merge!(to_be_merged: @to_be_merged) + expect(@org.guidance_groups.first.guidances.length).to eql(expected) + end + it "merges associated :identifiers" do + expected = @org.identifiers.length + @to_be_merged.identifiers.length + @org.merge!(to_be_merged: @to_be_merged) + expect(@org.identifiers.length).to eql(expected) + end + it "merges associated :plans" do + expected = @org.plans.length + @to_be_merged.plans.length + @org.merge!(to_be_merged: @to_be_merged) + expect(@org.plans.length).to eql(expected) + end + it "merges associated :templates" do + expected = @org.templates.length + @to_be_merged.templates.length + @org.merge!(to_be_merged: @to_be_merged) + expect(@org.templates.length).to eql(expected) + end + it "merges associated :token_permission_types" do + expected = (@org.token_permission_types | @to_be_merged.token_permission_types).length + @org.merge!(to_be_merged: @to_be_merged) + expect(@org.token_permission_types.length).to eql(expected) + end + it "merges associated :tracker" do + expected = @to_be_merged.tracker.code + @org.merge!(to_be_merged: @to_be_merged) + expect(@org.tracker.code).to eql(expected) + end + it "merges associated :users" do + expected = @org.users.length + @to_be_merged.users.length + @org.merge!(to_be_merged: @to_be_merged) + expect(@org.users.length).to eql(expected) + end + it "removes the :to_be_merged Org" do + original_id = @to_be_merged.id + expect(@org.merge!(to_be_merged: @to_be_merged)).to eql(@org) + expect(Org.find_by(id: original_id).present?).to eql(false) + end + end + + context "private methods" do + describe ":merge_attributes!(to_be_merged:)" do + before(:each) do + @org = create(:org, :organisation, is_other: false, managed: false, + feedback_enabled: false, contact_email: nil, + contact_name: nil, feedback_email_msg: nil, + feedback_email_subject: nil) + + @to_be_merged = create(:org, :funder, templates: 1, plans: 2, managed: true, + feedback_enabled: true, is_other: true, + sort_name: Faker::Movies::StarWars.planet, + region: create(:region), + language: create(:language)) + end + + it "returns false unless Org is an Org" do + expect(@org.send(:merge_attributes!, to_be_merged: create(:user))).to eql(false) + end + it "merges the correct attributes" do + original = @to_be_merged.dup + org = @org.merge!(to_be_merged: @to_be_merged) + expect(org.managed?).to eql(original.managed?) + expect(org.links).to eql(original.links) + expect(org.target_url).to eql(original.target_url) + expect(org.logo).to eql(original.logo) + expect(org.contact_email).to eql(original.contact_email) + expect(org.contact_name).to eql(original.contact_name) + expect(org.feedback_enabled).to eql(original.feedback_enabled) + expect(org.feedback_email_msg).to eql(original.feedback_email_msg) + expect(org.feedback_email_subject).to eql(original.feedback_email_subject) + end + it "does not merge the attributes it should not merge" do + original = @to_be_merged.dup + org = @org.merge!(to_be_merged: @to_be_merged) + expect(org.abbreviation).not_to eql(original.abbreviation) + expect(org.is_other).not_to eql(original.is_other) + expect(org.name).not_to eql(original.name) + expect(org.organisation?).to eql(true) + expect(org.funder?).to eql(false) + expect(org.sort_name).not_to eql(original.sort_name) + expect(org.region).not_to eql(original.region) + expect(org.language).not_to eql(original.language) + end + end + + describe ":merge_departments!(to_be_merged:)" do + before(:each) do + @org = create(:org) + @to_be_merged = create(:org) + @department = create(:department, org: @to_be_merged) + @to_be_merged.reload + end + + it "returns false unless the specified Org is an Org" do + expect(@org.send(:merge_departments!, to_be_merged: create(:user))).to eql(false) + end + it "returns false unless the specified Org has department" do + expect(@org.send(:merge_departments!, to_be_merged: create(:org))).to eql(false) + end + it "merges :departments that are not already associated" do + @org.send(:merge_departments!, to_be_merged: @to_be_merged) + @org.reload + expect(@org.departments.map(&:name).include?(@department.name)).to eql(true) + end + it "does not merge :departments that have the same name" do + create(:department, name: @department.name.downcase, org: @org) + @org.reload + @org.send(:merge_departments!, to_be_merged: @to_be_merged) + expect(@org.departments.length).to eql(1) + end + end + + describe ":merge_guidance_groups!(to_be_merged:)" do + before(:each) do + @guidance = create(:guidance) + @gg = create(:guidance_group, guidances: [@guidance]) + @org = create(:org, guidance_groups: []) + @to_be_merged = create(:org, guidance_groups: [@gg]) + end + + it "returns false unless the specified Org is an Org" do + expect(@org.send(:merge_guidance_groups!, to_be_merged: create(:user))).to eql(false) + end + it "returns false unless the specified Org has :guidance_groups" do + expect(@org.send(:merge_guidance_groups!, to_be_merged: create(:org))).to eql(false) + end + it "merges into the Org's existing :guidance_group" do + @org.update(guidance_groups: [create(:guidance_group, guidances: [])]) + @org.send(:merge_guidance_groups!, to_be_merged: @to_be_merged) + @org = @org.reload + expect(@org.guidance_groups.include?(@gg)).to eql(false) + expect(@org.guidance_groups.length).to eql(1) + expect(@org.guidance_groups.first.guidances.include?(@guidance)).to eql(true) + end + it "creates a new :guidance_group if the Org does not have one" do + @org.send(:merge_guidance_groups!, to_be_merged: @to_be_merged) + @org = @org.reload + expect(@org.guidance_groups.include?(@gg)).to eql(false) + expect(@org.guidance_groups.length).to eql(1) + expect(@org.guidance_groups.first.guidances.include?(@guidance)).to eql(true) + end + end + + describe ":merge_token_permission_types!(to_be_merged:)" do + before(:each) do + @tpt = create(:token_permission_type) + @org = create(:org) + @to_be_merged = create(:org, token_permission_types: [@tpt]) + end + + it "returns false unless the specified Org is an Org" do + expect(@org.send(:merge_token_permission_types!, to_be_merged: create(:user))).to eql(false) + end + it "returns false unless the specified Org has token_permission_types" do + o = create(:org) + # when org is created tpt gets assigned by default so need to scrub for this test + o.token_permission_types = [] + expect(@org.send(:merge_token_permission_types!, to_be_merged: o)).to eql(false) + end + it "merges :token_permission_types that are not already associated" do + @org.send(:merge_token_permission_types!, to_be_merged: @to_be_merged) + expect(@org.token_permission_types.include?(@tpt)).to eql(true) + end + it "does not merge :token_permission_types that are already associated" do + @org.update(token_permission_types: [@tpt]) + @org.send(:merge_token_permission_types!, to_be_merged: @to_be_merged) + expect(@org.token_permission_types.length).to eql(1) + end + end + end + end diff --git a/spec/presenters/super_admin/orgs/merge_presenter_spec.rb b/spec/presenters/super_admin/orgs/merge_presenter_spec.rb new file mode 100644 index 0000000000..ebc6491aa7 --- /dev/null +++ b/spec/presenters/super_admin/orgs/merge_presenter_spec.rb @@ -0,0 +1,289 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe SuperAdmin::Orgs::MergePresenter do + + before(:each) do + @to_org = create(:org, :organisation, is_other: false, managed: false, + feedback_enabled: false, contact_email: nil, + contact_name: nil, feedback_email_msg: nil, + links: { org: [] }, feedback_email_subject: nil) + + @tpt = create(:token_permission_type) + @from_org = create(:org, :funder, templates: 1, plans: 0, managed: true, + feedback_enabled: true, is_other: true, + sort_name: Faker::Movies::StarWars.planet, + target_url: Faker::Internet.url, + contact_name: Faker::Music::PearlJam.musician, + contact_email: Faker::Internet.email, + links: { org: { foo: "bar" } }, + region: create(:region, name: Faker::Music::PearlJam.song), + language: create(:language), + token_permission_types: [@tpt]) + create(:annotation, org: @from_org) + create(:department, org: @from_org) + gg = @to_be_merged.guidance_groups.first if @from_org.guidance_groups.any? + gg = create(:guidance_group, org: @from_org) unless gg.present? + create(:guidance, guidance_group: gg) + @id_no_scheme = create(:identifier, identifiable: @from_org, identifier_scheme: nil) + @scheme = create(:identifier_scheme) + @id_scheme = create(:identifier, identifiable: @from_org, identifier_scheme: @scheme) + create(:plan, funder_id: @from_org.id) + create(:plan, org: @from_org) + create(:tracker, org: @from_org) + @user = create(:user, org: @from_org) + @from_org.reload + @presenter = SuperAdmin::Orgs::MergePresenter.new(from_org: @from_org, to_org: @to_org) + end + + describe ":initialize(from_org:, to_org:)" do + it "sets the cropped :to_org and :from_org" do + expect(@presenter.to_org).to eql(@to_org) + expect(@presenter.from_org).to eql(@from_org) + end + it "sets the cropped :to_org_name and :from_org_name" do + expect(@presenter.to_org_name.present? && @presenter.to_org_name.is_a?(String)).to eql(true) + expect(@presenter.from_org_name.present? && @presenter.to_org_name.is_a?(String)).to eql(true) + end + it "sets the :categories" do + expect(@presenter.categories.any?).to eql(true) + expect(@presenter.categories.first).to eql(:annotations) + expect(@presenter.categories.last).to eql(:users) + end + it "sets the :to_org_entries, :from_org_entries and mergeable_entries" do + expect(@presenter.to_org_entries.is_a?(Hash)).to eql(true) + expect(@presenter.from_org_entries.is_a?(Hash)).to eql(true) + expect(@presenter.from_org_entries[:annotations].any?).to eql(true) + expect(@presenter.mergeable_entries.is_a?(Hash)).to eql(true) + expect(@presenter.mergeable_entries[:annotations].any?).to eql(true) + end + it "sets the :from_org_attributes, :to_org_attributes and :mergeable_attributes" do + expect(@presenter.to_org_attributes.is_a?(Hash)).to eql(true) + expect(@presenter.from_org_attributes.is_a?(Hash)).to eql(true) + expect(@presenter.from_org_attributes[:contact_email].present?).to eql(true) + expect(@presenter.mergeable_attributes.is_a?(Hash)).to eql(true) + expect(@presenter.mergeable_attributes[:target_url].present?).to eql(true) + end + end + + context "private methods" do + describe ":prepare_org(org:)" do + it "returns the expected categories for from_org" do + results = @presenter.send(:prepare_org, org: @from_org) + expect(results[:annotations].any?).to eql(true) + expect(results[:departments].any?).to eql(true) + expect(results[:funded_plans].any?).to eql(true) + expect(results[:guidances].any?).to eql(true) + expect(results[:identifiers].any?).to eql(true) + expect(results[:plans].any?).to eql(true) + expect(results[:templates].any?).to eql(true) + expect(results[:token_permission_types].any?).to eql(true) + expect(results[:tracker].any?).to eql(true) + expect(results[:users].any?).to eql(true) + end + it "returns the expected categories for to_org" do + results = @presenter.send(:prepare_org, org: @to_org) + expect(results[:annotations].any?).to eql(false) + expect(results[:departments].any?).to eql(false) + expect(results[:funded_plans].any?).to eql(false) + expect(results[:guidances].any?).to eql(false) + expect(results[:identifiers].any?).to eql(false) + expect(results[:plans].any?).to eql(false) + expect(results[:templates].any?).to eql(false) + expect(results[:token_permission_types].any?).to eql(false) + expect(results[:tracker].any?).to eql(false) + expect(results[:users].any?).to eql(false) + end + end + + describe ":prepare_mergeables" do + it "returns the expected categories" do + results = @presenter.send(:prepare_mergeables) + expect(results[:annotations].any?).to eql(true) + expect(results[:departments].any?).to eql(true) + expect(results[:funded_plans].any?).to eql(true) + expect(results[:guidances].any?).to eql(true) + expect(results[:identifiers].any?).to eql(true) + expect(results[:plans].any?).to eql(true) + expect(results[:templates].any?).to eql(true) + expect(results[:token_permission_types].any?).to eql(true) + expect(results[:tracker].any?).to eql(true) + expect(results[:users].any?).to eql(true) + end + it "does not return :tracker if one is already defined on to_org" do + create(:tracker, org: @to_org) + @to_org.reload + @presenter = SuperAdmin::Orgs::MergePresenter.new(from_org: @from_org, to_org: @to_org) + results = @presenter.send(:prepare_mergeables) + expect(results[:tracker]).to eql([]) + end + end + + describe ":diff_from_and_to(category:)" do + before(:each) do + @entries = %i[annotations departments funded_plans guidances identifiers + plans templates token_permission_types tracker users] + end + it "returns an empty array if category is not present" do + expect(@presenter.send(:diff_from_and_to, category: nil)).to eql([]) + end + it "returns an empty array if category is not an org entry" do + expect(@presenter.send(:diff_from_and_to, category: :foo)).to eql([]) + end + it "returns the from_org entries for :annotations" do + results = @presenter.send(:diff_from_and_to, category: :annotations) + expect(results).to eql(@from_org.annotations.to_a) + end + it "returns the uniquie entries for :departments" do + dup_department = build(:department) + @from_org.departments << dup_department + @to_org.departments << dup_department + @presenter = SuperAdmin::Orgs::MergePresenter.new(from_org: @from_org, to_org: @to_org) + results = @presenter.send(:diff_from_and_to, category: :departments) + expect(results.include?(dup_department)).to eql(false) + expect(results).to eql(@from_org.departments.reject { |dpt| dpt == dup_department }) + end + it "returns the from_org entries for :funded_plans" do + results = @presenter.send(:diff_from_and_to, category: :funded_plans) + expect(results).to eql(@from_org.funded_plans.to_a) + end + it "returns the all entries for :guidances" do + results = @presenter.send(:diff_from_and_to, category: :guidances) + expect(results).to eql(@from_org.guidance_groups.map(&:guidances).flatten.to_a) + end + it "returns the :identifiers that have no :identifier_scheme" do + to_id = build(:identifier, identifier_scheme: nil) + @to_org.identifiers << to_id + @presenter = SuperAdmin::Orgs::MergePresenter.new(from_org: @from_org, to_org: @to_org) + results = @presenter.send(:diff_from_and_to, category: :identifiers) + expect(results.include?(@id_no_scheme)).to eql(true) + end + it "returns the :identifiers that are not defined in to_org for the :identifier_scheme" do + to_id = build(:identifier, identifier_scheme: build(:identifier_scheme)) + @to_org.identifiers << to_id + @presenter = SuperAdmin::Orgs::MergePresenter.new(from_org: @from_org, to_org: @to_org) + results = @presenter.send(:diff_from_and_to, category: :identifiers) + expect(results.include?(@id_scheme)).to eql(true) + end + it "does not return the :identifiers that are defined in to_org for the :identifier_scheme" do + to_id = build(:identifier, identifier_scheme: @scheme) + @to_org.identifiers << to_id + @presenter = SuperAdmin::Orgs::MergePresenter.new(from_org: @from_org, to_org: @to_org) + results = @presenter.send(:diff_from_and_to, category: :identifiers) + expect(results.include?(@id_scheme)).to eql(false) + end + it "returns the from_org entries for :plans" do + results = @presenter.send(:diff_from_and_to, category: :plans) + expect(results).to eql(Plan.where(org: @from_org).to_a) + end + it "returns the from_org entries for :templates" do + results = @presenter.send(:diff_from_and_to, category: :templates) + expect(results).to eql(@from_org.templates.to_a) + end + it "returns the :token_permission_types that are not defined on to_org" do + results = @presenter.send(:diff_from_and_to, category: :token_permission_types) + expect(results).to eql(@from_org.token_permission_types.to_a) + end + it "does not return the :token_permission_types that are defined on to_org" do + @to_org.token_permission_types << @tpt + @presenter = SuperAdmin::Orgs::MergePresenter.new(from_org: @from_org, to_org: @to_org) + results = @presenter.send(:diff_from_and_to, category: :token_permission_types) + expect(results.include?(@tpt)).to eql(false) + end + it "returns the from_org entries for :users not defined on to_org" do + results = @presenter.send(:diff_from_and_to, category: :users) + expect(results).to eql(@from_org.users.to_a) + end + it "does not return the :users that are defined on to_org" do + @to_org.users << @user + @presenter = SuperAdmin::Orgs::MergePresenter.new(from_org: @from_org, to_org: @to_org) + results = @presenter.send(:diff_from_and_to, category: :users) + expect(results.include?(@user)).to eql(false) + end + end + + describe ":org_attributes(org:)" do + before(:each) do + @expected = %i[target_url managed links + contact_name contact_email + logo_uid logo_name + feedback_enabled feedback_email_msg feedback_email_subject] + @results = @presenter.send(:org_attributes, org: @from_org) + end + it "returns an empty hash if :org is not an Org" do + expect(@presenter.send(:org_attributes, org: build(:user))).to eql({}) + end + it "includes the expected columns" do + @expected.each { |column| expect(@results[column]).to eql(@from_org.send(column)) } + end + it "does not include any other columns than the ones we expect" do + @results.each_key { |key| expect(@expected.include?(key)).to eql(true) } + end + end + + describe ":mergeable_columns" do + before(:each) do + @expected = %i[target_url managed links + contact_name contact_email + feedback_enabled feedback_email_msg feedback_email_subject] + @results = @presenter.send(:mergeable_columns) + end + it "includes the expected columns" do + @expected.each { |column| expect(@results[column].present?).to eql(true) } + end + it "does not include any other columns than the ones we expect" do + @results.each_key { |key| expect(@expected.include?(key)).to eql(true) } + end + end + describe ":mergeable_column?(column:)" do + it "returns false if the :to_org is :managed but the :from_org is not" do + @presenter.to_org.stubs(:managed?).returns(true) + @presenter.from_org.stubs(:managed?).returns(false) + expect(@presenter.send(:mergeable_column?, column: :managed)).to eql(false) + end + it "returns false if the :to_org has :feedback_enabled but the :from_org does not" do + @presenter.to_org.stubs(:feedback_enabled?).returns(true) + @presenter.from_org.stubs(:feedback_enabled?).returns(false) + expect(@presenter.send(:mergeable_column?, column: :feedback_enabled)).to eql(false) + end + it "returns false if the :from_org links is an empty json" do + @presenter.from_org.stubs(:links).returns({}) + expect(@presenter.send(:mergeable_column?, column: :links)).to eql(false) + end + it "returns false if the :to_org already has a value" do + @presenter.to_org.stubs(:contact_email).returns(Faker::Internet.email) + expect(@presenter.send(:mergeable_column?, column: :contact_email)).to eql(false) + end + it "returns false if the :from_org does not have a value" do + @presenter.from_org.stubs(:contact_email).returns(nil) + expect(@presenter.send(:mergeable_column?, column: :contact_email)).to eql(false) + end + it "returns true for :target_url" do + expect(@presenter.send(:mergeable_column?, column: :target_url)).to eql(true) + end + it "returns true for :managed" do + expect(@presenter.send(:mergeable_column?, column: :managed)).to eql(true) + end + it "returns true for :links" do + expect(@presenter.send(:mergeable_column?, column: :links)).to eql(true) + end + it "returns true for :contact_name" do + expect(@presenter.send(:mergeable_column?, column: :contact_name)).to eql(true) + end + it "returns true for :contact_email" do + expect(@presenter.send(:mergeable_column?, column: :contact_email)).to eql(true) + end + it "returns true for :feedback_enabled" do + expect(@presenter.send(:mergeable_column?, column: :feedback_enabled)).to eql(true) + end + it "returns true for :feedback_email_msg" do + expect(@presenter.send(:mergeable_column?, column: :feedback_email_msg)).to eql(true) + end + it "returns true for :feedback_email_subject" do + expect(@presenter.send(:mergeable_column?, column: :feedback_email_subject)).to eql(true) + end + end + end + +end