# frozen_string_literal: true # The central model object within this domain. Represents a Data Management # Plan for a research project. # # == Schema Information # # Table name: plans # # id :integer not null, primary key # complete :boolean default(FALSE) # data_contact :string # data_contact_email :string # data_contact_phone :string # description :text # feedback_requested :boolean default(FALSE) # funder_name :string # grant_number :string # identifier :string # principal_investigator :string # principal_investigator_email :string # principal_investigator_identifier :string # principal_investigator_phone :string # title :string # visibility :integer default(3), not null # created_at :datetime # updated_at :datetime # template_id :integer # # Indexes # # plans_template_id_idx (template_id) # # Foreign Keys # # fk_rails_... (template_id => templates.id) # class Plan < ActiveRecord::Base include ConditionalUserMailer include ExportablePlan include ValidationMessages include ValidationValues include Dmpopidor::Models::Plan # ============= # = Constants = # ============= # Returns visibility message given a Symbol type visibility passed, otherwise # nil VISIBILITY_MESSAGE = { organisationally_visible: _("organisational"), publicly_visible: _("public"), is_test: _("test"), privately_visible: _('Administrator'), privately_private_visible: _('private') } # ============== # = Attributes = # ============== # public is a Ruby keyword so using publicly enum visibility: %i[organisationally_visible publicly_visible is_test privately_visible privately_private_visible] alias_attribute :name, :title # ================ # = Associations = # ================ belongs_to :template has_many :phases, through: :template has_many :sections, through: :phases has_many :questions, through: :sections has_many :themes, through: :questions has_many :guidances, through: :themes has_many :guidance_group_options, -> { uniq.published.reorder("id") }, through: :guidances, source: :guidance_group, class_name: "GuidanceGroup" has_many :answers, dependent: :destroy has_many :notes, through: :answers has_many :roles, dependent: :destroy has_many :users, through: :roles has_and_belongs_to_many :guidance_groups, join_table: :plans_guidance_groups has_many :exported_plans has_many :roles # RESEARCH OUTPUTS has_many :research_outputs, dependent: :destroy, inverse_of: :plan do # Returns the default research output def default find_by(is_default: true) end # Toggles the default research output between default and normal # Uses the 'is_default' flag: # - Removes it if there are more than one research output # - Adds it back is there's only one research output left def toggle_default if count > 1 unless default.nil? default.update(name: 'Default research output') if default.name.nil? default.update(is_default: false) end else last&.update(is_default: true) end end end # ===================== # = Nested Attributes = # ===================== accepts_nested_attributes_for :template accepts_nested_attributes_for :roles accepts_nested_attributes_for :research_outputs, reject_if: :all_blank, allow_destroy: true # =============== # = Validations = # =============== validates :title, presence: { message: PRESENCE_MESSAGE } validates :template, presence: { message: PRESENCE_MESSAGE } validates :feedback_requested, inclusion: { in: BOOLEAN_VALUES } validates :complete, inclusion: { in: BOOLEAN_VALUES } # ============= # = Callbacks = # ============= before_validation :set_creation_defaults # ========== # = Scopes = # ========== # Retrieves any plan in which the user has an active role and # is not a reviewer scope :active, lambda { |user| plan_ids = Role.where(active: true, user_id: user.id) .not_reviewer.pluck(:plan_id) includes(:template, :roles) .where(id: plan_ids) } # Retrieves any plan organisationally or publicly visible for a given org id scope :organisationally_or_publicly_visible, -> (user) { plan_ids = user.org.plans.where(complete: true).pluck(:id) includes(:template, roles: :user) .where(id: plan_ids, visibility: [ visibilities[:organisationally_visible], visibilities[:publicly_visible] ]) .where( "NOT EXISTS (SELECT 1 FROM roles WHERE plan_id = plans.id AND user_id = ?)", user.id ) } scope :search, lambda { |term| search_pattern = "%#{term}%" joins(:template) .where("plans.title LIKE ? OR templates.title LIKE ?", search_pattern, search_pattern) } # Retrieves plan, template, org, phases, sections and questions scope :overview, lambda { |id| includes(:phases, :sections, :questions, template: [:org]).find(id) } ## # Settings for the template has_settings :export, class_name: "Settings::Template" do |s| s.key :export, defaults: Settings::Template::DEFAULT_SETTINGS end alias super_settings settings # ================= # = Class methods = # ================= # Pre-fetched a plan phase together with its sections and questions # associated. It also pre-fetches the answers and notes associated to the plan def self.load_for_phase(plan_id, phase_id) # Preserves the default order defined in the model relationships plan = Plan.joins(template: { phases: { sections: :questions } }) .preload(template: { phases: { sections: :questions } }) .where(id: plan_id, phases: { id: phase_id }) .merge(Plan.includes(answers: :notes)).first phase = plan.template.phases.find { |p| p.id == phase_id.to_i } [plan, phase] end # deep copy the given plan and all of it's associations # # plan - Plan to be deep copied # # Returns Plan # CHANGES # Added Research Output Support def self.deep_copy(plan) plan_copy = plan.dup plan_copy.title = "Copy of " + plan.title plan_copy.save! plan.research_outputs.each do |research_output| research_output_copy = ResearchOutput.deep_copy(research_output) research_output_copy.plan_id = plan_copy.id research_output_copy.save! research_output.answers.each do |answer| answer_copy = Answer.deep_copy(answer) answer_copy.plan_id = plan_copy.id answer_copy.research_output_id = research_output_copy.id answer_copy.save! end end plan.guidance_groups.each do |guidance_group| plan_copy.guidance_groups << guidance_group if guidance_group.present? end plan_copy end # =========================== # = Public instance methods = # =========================== ## # Proxy through to the template settings (or defaults if this plan doesn't # have an associated template) if there are no settings stored for this plan. # # TODO: Update this comment below. AFAIK `key` has nothing to do with Rails. # key - Is required by rails-settings, so it's required here, too. # # Returns Hash def settings(key) self_settings = super_settings(key) return self_settings if self_settings.value? template&.settings(key) end # # The most recent answer to the given question id optionally can create an answer if # # none exists. # # # # qid - The id for the question to find the answer for # # create_if_missing - If true, will genereate a default answer # # to the question (defaults: true). # # # # Returns Answer # # Returns nil # def answer(qid, create_if_missing = true) # answer = answers.where(question_id: qid).order("created_at DESC").first # question = Question.find(qid) # if answer.nil? && create_if_missing # answer = Answer.new # answer.plan_id = id # answer.question_id = qid # answer.text = question.default_value # default_options = [] # question.question_options.each do |option| # default_options << option if option.is_default # end # answer.question_options = default_options # end # answer # end alias get_guidance_group_options guidance_group_options deprecate :get_guidance_group_options, deprecator: Cleanup::Deprecators::GetDeprecator.new ## # Sets up the plan for feedback: # emails confirmation messages to owners # emails org admins and org contact # adds org admins to plan with the 'reviewer' Role def request_feedback(user) Plan.transaction do begin self.feedback_requested = true # Share the plan with each org admin as the reviewer role admins = user.org.org_admins admins.each do |admin| unless admin == user add_user!(admin.id, :reviewer) end end if save! # Send an email to the org-admin contact if user.org.contact_email.present? contact = User.new(email: user.org.contact_email, firstname: user.org.contact_name) UserMailer.feedback_notification(contact, self, user).deliver_now end return true else return false end rescue Exception => e Rails.logger.error e return false end end end ## # Finalizes the feedback for the plan: Emails confirmation messages to owners # sets flag on plans.feedback_requested to false removes org admins from the # 'reviewer' Role for the Plan. def complete_feedback(org_admin) Plan.transaction do begin self.feedback_requested = false # Remove the org admins reviewer role from the plan roles.delete(Role.where(plan: self).reviewer) if save! # Send an email confirmation to the owners and co-owners deliver_if(recipients: owner_and_coowners, key: "users.feedback_provided") do |r| UserMailer.feedback_complete(r, self, org_admin).deliver_now end true else false end rescue ArgumentError => e Rails.logger.error e false end end end ## # determines if the plan is editable by the specified user # # user_id - The id for a user # # Returns Boolean def editable_by?(user_id) Role.editor.where(plan_id: id, user_id: user_id, active: true).any? end ## # determines if the plan is readable by the specified user # # user_id - The Integer id for a user # # Returns Boolean def readable_by?(user_id) current_user = User.find(user_id) if current_user.present? # If the user is a super admin and the config allows for supers to view plans if current_user.can_super_admin? && Branding.fetch(:service_configuration, :plans, :super_admins_read_all) true # If the user is an org admin and the config allows for org admins to view plans elsif current_user.can_org_admin? && Branding.fetch(:service_configuration, :plans, :org_admins_read_all) true else commentable_by?(user_id) end else false end end # determines if the plan is readable by the specified user. # # user_id - The Integer id for a user # # Returns Boolean def commentable_by?(user_id) Role.commenter.where(plan_id: id, user_id: user_id, active: true).any? end # determines if the plan is administerable by the specified user # # user_id - The Integer id for the user # # Returns Boolean def administerable_by?(user_id) Role.administrator.where(plan_id: id, user_id: user_id, active: true).any? end # determines if the plan is reviewable by the specified user # # user_id - The Integer id for the user # # Returns Boolean def reviewable_by?(user_id) Role.reviewer.where(plan_id: id, user_id: user_id, active: true).any? end # the datetime for the latest update of this plan # # Returns DateTime def latest_update (phases.pluck(:updated_at) + [updated_at]).max end # The owner (aka :creator) of the project # # Returns User # Returns nil def owner usr_ids = Role.where(plan_id: id, active: true) .administrator .order(:created_at) .pluck(:user_id).uniq User.where(id: usr_ids).first end # Creates a role for the specified user (will update the user's # existing role if it already exists) # # Expects a User.id and access_type from the following list: # :creator, :administrator, :editor, :commenter # # Returns Boolean def add_user!(user_id, access_type = :commenter) user = User.where(id: user_id).first if user.present? role = Role.find_or_initialize_by(user_id: user_id, plan_id: self.id) # Access is cumulative, so set the appropriate flags # (e.g. an administrator can also edit and comment) case access_type when :creator role.creator = true role.administrator = true role.editor = true when :administrator role.administrator = true role.editor = true when :editor role.editor = true when :reviewer role.reviewer = true end role.commenter = true role.save else false end end ## # Whether or not the plan is associated with users other than the creator # # Returns Boolean def shared? roles.where(Role.not_creator_condition).any? end alias shared shared? deprecate :shared, deprecator: Cleanup::Deprecators::PredicateDeprecator.new # The owner and co-owners (aka :creator and :administrator) of the project # # Returns ActiveRecord::Relation def owner_and_coowners # We only need to search for :administrator in the bitflag # since :creator includes :administrator rights usr_ids = Role.where(plan_id: id, active: true) .administrator .pluck(:user_id).uniq User.where(id: usr_ids) end # The creator, administrator and editors # # Returns ActiveRecord::Relation def authors # We only need to search for :editor in the bitflag # since :creator and :administrator include :editor rights usr_ids = Role.where(plan_id: id, active: true) .editor .pluck(:user_id).uniq User.where(id: usr_ids) end # The number of answered questions from the entire plan # # Returns Integer def num_answered_questions Answer.where(id: answers.map(&:id)) .includes(:question_options, question: :question_format) .to_a .sum { |answer| answer.is_valid? ? 1 : 0 } end # The number of questions for a plan. # # Returns Integer def num_questions questions.count end # Determines whether or not visibility changes are permitted according to the # percentage of the plan answered in respect to a threshold defined at # application.config # # Returns Boolean def visibility_allowed? !is_test? && phases.select{ |phase| phase.visibility_allowed?(self) }.any? end # Determines whether or not a question (given its id) exists for the self plan # # Returns Boolean def question_exists?(question_id) Plan.joins(:questions).exists?(id: id, "questions.id": question_id) end # Checks whether or not the number of questions matches the number of valid # answers # # Returns Boolean def no_questions_matches_no_answers? num_questions = question_ids.length pre_fetched_answers = Answer.includes(:question_options, question: :question_format) .where(id: answer_ids) num_answers = pre_fetched_answers.reduce(0) do |m, a| m += 1 if a.is_valid? m end num_questions == num_answers end # Deactivates the plan (sets all roles to inactive and visibility to :private) # # Returns Boolean # def deactivate! # # If no other :creator, :administrator or :editor is attached # # to the plan, then also deactivate all other active roles # # and set the plan's visibility to :private # if authors.size == 0 # roles.where(active: true).update_all(active: false) # self.visibility = Plan.visibilities[:privately_visible] # save! # else # false # end # end private # Initialize the title for new templates # # Returns nil # Returns String def set_creation_defaults # Only run this before_validation because rails fires this before # save/create return if id? self.title = "My plan (#{template.title})" if title.nil? && !template.nil? end end