Newer
Older
dmpopidor / app / models / template.rb
class Template < ActiveRecord::Base
  include GlobalHelpers
  include ActiveModel::Validations
  validates_with TemplateLinksValidator

  before_validation :set_creation_defaults

  # Stores links as an JSON object: { funder: [{"link":"www.example.com","text":"foo"}, ...], sample_plan: [{"link":"www.example.com","text":"foo"}, ...]}
  # The links is validated against custom validator allocated at validators/template_links_validator.rb
  serialize :links, JSON
  
  ##
  # Associations
  belongs_to :org
  has_many :plans
  has_many :phases, dependent: :destroy
  has_many :sections, through: :phases
  has_many :questions, through: :sections

  has_many :customizations, class_name: 'Template', foreign_key: 'dmptemplate_id'
  belongs_to :dmptemplate, class_name: 'Template'

  ##
  # Possibly needed for active_admin
  #   -relies on protected_attributes gem as syntax depricated in rails 4.2
  attr_accessible :id, :org_id, :description, :published, :title, :locale, :customization_of,
                  :is_default, :guidance_group_ids, :org, :plans, :phases, :dmptemplate_id,
                  :migrated, :version, :visibility, :published, :links, :as => [:default, :admin]

  # A standard template should be organisationally visible. Funder templates that are 
  # meant for external use will be publicly visible. This allows a funder to create 'funder' as
  # well as organisational templates. The default template should also always be publicly_visible
  enum visibility: [:organisationally_visible, :publicly_visible]

  # defines the export setting for a template object
  has_settings :export, class_name: 'Settings::Template' do |s|
    s.key :export, defaults: Settings::Template::DEFAULT_SETTINGS
  end

  validates :org, :title, :version, presence: {message: _("can't be blank")}

  scope :valid,  -> { where(migrated: false) }
  scope :published, -> { where(published: true) }

  # Retrieves all valid and published templates
  scope :valid_published, -> (is_default: false)  {
    Template.where(templates: { is_default: is_default }).valid().published()
  }

  scope :publicly_visible, -> { where(:visibility => Template.visibilities[:publicly_visible]) }
  scope :organisationally_visible, -> { where(:visibility => Template.visibilities[:organisationally_visible]) }
  
  # Retrieves template with distinct dmptemplate_id that are valid (e.g. migrated false) and customization_of is nil. Note,
  # if organisation ids are passed, the query will filter only those distinct dmptemplate_ids for those organisations
  scope :families, -> (org_ids=nil) {
    if org_ids.is_a?(Array) 
      valid.where(org_id: org_ids, customization_of: nil).distinct
    else
      valid.where(customization_of: nil).distinct
    end 
  }
  # Retrieves the maximum version for the array of dmptemplate_ids passed. If dmptemplate_ids is missing, every maximum
  # version for each different dmptemplate_id will be retrieved
  scope :dmptemplate_ids_with_max_version, -> (dmptemplate_ids=nil) {
    if dmptemplate_ids.is_a?(Array)
      select("MAX(version) AS version", :dmptemplate_id).where(dmptemplate_id: dmptemplate_ids).group(:dmptemplate_id)
    else
      select("MAX(version) AS version", :dmptemplate_id).group(:dmptemplate_id)
    end
  }
  # Retrieves the maximum version for the array of customization_ofs passed. If customization_ofs is missing, every maximum
  # version for each different customization_of will be retrieved
  scope :customization_ofs_with_max_version, -> (customization_ofs=nil) {
    if customization_ofs.is_a?(Array)
      select("MAX(version) AS version", :customization_of).where(customization_of: customization_ofs).group(:customization_of)
    else
      select("MAX(version) AS version", :customization_of).group(:customization_of)
    end
  }
  # Retrieves the latest template version, i.e. the one with maximum version for each dmptemplate_id
  scope :latest_version, -> (dmptemplate_ids=nil) {
    from(dmptemplate_ids_with_max_version(dmptemplate_ids), :current)
    .joins("INNER JOIN templates ON current.version = templates.version"\
      " AND current.dmptemplate_id = templates.dmptemplate_id")
  }
  # Retrieves the latest customized version, i.e. the one with maximum version for each customization_of=dmptemplate_id
  scope :latest_customization, -> (org_id, dmptemplate_ids=nil) {
    from(customization_ofs_with_max_version(dmptemplate_ids), :current)
    .joins("INNER JOIN templates ON current.version = templates.version"\
      " AND current.customization_of = templates.customization_of")
    .where('templates.org_id = ?', org_id)
  }
  
  scope :search, -> (term) {
    search_pattern = "%#{term}%"
    joins(:org).where("templates.title LIKE ? OR orgs.name LIKE ?", search_pattern, search_pattern)
  }
  
  # Retrieves the list of all dmptemplate_ids (template versioning families) for the specified Org
  def self.dmptemplate_ids
    Template.all.valid.distinct.pluck(:dmptemplate_id)
  end

  # Retrieves the most recent version of the template for the specified dmptemplate_id
  def self.current(dmptemplate_id)
    Template.where(dmptemplate_id: dmptemplate_id).order(version: :desc).valid.first
  end

  # Retrieves the current published version of the template for the specified Org and dmptemplate_id
  def self.live(dmptemplate_id)
    if dmptemplate_id.respond_to?(:each)
      Template.where(dmptemplate_id: dmptemplate_id, published: true).valid
    else
      Template.where(dmptemplate_id: dmptemplate_id, published: true).valid.first
    end
  end

  def self.default
    Template.valid.where(is_default: true, published: true).order(:version).last
  end

  ##
  # Retrieves the most current customization of the template for the
  # specified org and dmptemplate_id
  # returns nil if no customizations found
  #
  # @params  dmptemplate_ids of the original template
  # @params [integer] org_id for the customizing organisation
  # @return [nil, Template] the customized template or nil
  def self.org_customizations(dmptemplate_ids, org_id)
    template_ids = latest_customization(org_id, dmptemplate_ids).pluck(:id)
    includes(:org).where(id: template_ids)
  end
  
  # Retrieves current templates with their org associated for a set of valid orgs
  # TODO pass an array of org ids instead of Org instances
  def self.get_latest_template_versions(orgs)
    if orgs.respond_to?(:each)
      families_ids = families(orgs.map(&:id)).pluck(:dmptemplate_id)
    elsif orgs.is_a?(Org)
      families_ids = families([orgs.id]).pluck(:dmptemplate_id)
    else
      families_ids = []
    end
    template_ids = latest_version(families_ids).pluck(:id)
    includes(:org).where(id: template_ids)
  end
  
  # Retrieves current templates with their org associated for a set of valid orgs
  # TODO pass an array of org ids instead of Org instances
  def self.get_public_published_template_versions(orgs)
    if orgs.respond_to?(:each)
      families_ids = families(orgs.map(&:id)).pluck(:dmptemplate_id)
    elsif orgs.is_a?(Org)
      families_ids = families([orgs.id]).pluck(:dmptemplate_id)
    else
      families_ids = []
    end
    includes(:org).where(dmptemplate_id: families_ids, published: true, visibility: Template.visibilities[:publicly_visible])
  end
  
  ##
  # create a new version of the most current copy of the template
  #
  # @return [Template] new version
  def get_new_version
    if self.id.present?
      new_version = Template.deep_copy(self)
      new_version.version = (self.version + 1)
      new_version.published = false 
      new_version.visibility = self.visibility # do not change the visibility 
      new_version.save!
      new_version
    else
      nil
    end
  end
  
  ##
  # deep copy the given template and all of it's associations
  #
  # @params [Template] template to be deep copied
  # @return [Template] saved copied template
  def self.deep_copy(template)
    template_copy = template.dup
    template_copy.save!
    template.phases.each do |phase|
      phase_copy = Phase.deep_copy(phase)
      phase_copy.template_id = template_copy.id
      phase_copy.save!
    end
    return template_copy
  end


  # EVALUATE CLASS AND INSTANCE METHODS BELOW
  #
  # What do they do? do they do it efficiently, and do we need them?


  ##
  # convert the given template to a hash and return with all it's associations
  # to use, please pre-fetch org, phases, section, questions, annotations,
  #   question_options, question_formats,
  # TODO: Themes & guidance?
  #
  # @return [hash] hash of template, phases, sections, questions, question_options, annotations
  def to_hash
    hash = {}
    hash[:template] = {}
    hash[:template][:data] = self
    hash[:template][:org] = self.org
    phases = {}
    hash[:template][:phases] = phases
    self.phases.each do |phase|
      phases[phase.number] = {}
      phases[phase.number][:data] = phase
      phases[phase.number][:sections] = {}
      phase.sections.each do |section|
        phases[phase.number][:sections][section.number] = {}
        phases[phase.number][:sections][section.number][:data] = section
        phases[phase.number][:sections][section.number][:questions] = {}
        section.questions.each do |question|
          phases[phase.number][:sections][section.number][:questions][question.number] = {}
          phases[phase.number][:sections][section.number][:questions][question.number][:data] = question
          phases[phase.number][:sections][section.number][:questions][question.number][:annotations] = {}
          question.annotations.each do |annotation|
            phases[phase.number][:sections][section.number][:questions][question.number][:annotations][annotation.id] = {}
            phases[phase.number][:sections][section.number][:questions][question.number][:annotations][annotation.id][:data] = annotation
          end
          phases[phase.number][:sections][section.number][:questions][question.number][:question_options] = {}
          question.question_options.each do |question_option|
            phases[phase.number][:sections][section.number][:questions][question.number][:question_options][:data] = question_option
            phases[phase.number][:sections][section.number][:questions][question.number][:question_format] = question.question_format
          end
        end
      end
    end
    return hash
  end

# TODO: Why are we passing in an org and template here?
  ##
  # Verify if a template has customisation by given organisation
  #
  # @param org_id [integer] the integer id for an organisation
  # @param temp [dmptemplate] a template object
  # @return [Boolean] true if temp has customisation by the given organisation
  def has_customisations?(org_id, temp)
    modifiable = true
    phases.each do |phase|
      modifiable = modifiable && phase.modifiable
    end
    return !modifiable
  end

  # --------------------------------------------------------
  private
  # Initialize the published and dirty flags for new templates
  def set_creation_defaults
    # Only run this before_validation because rails fires this before save/create
    if self.id.nil?
      self.published = false
      self.migrated = false
      self.dirty = false
      self.visibility = 1
      self.is_default = false
      self.version = 0 if self.version.nil?
      self.visibility = Template.visibilities[:organisationally_visible] if self.visibility.nil?

      # Generate a unique identifier for the dmptemplate_id if necessary
      if self.dmptemplate_id.nil?
        self.dmptemplate_id = loop do
          random = rand 2147483647
          break random unless Template.exists?(dmptemplate_id: random)
        end
      end
    end
  end
end