Newer
Older
dmpopidor / app / models / plan.rb
@Bodacious Bodacious on 25 Jul 2018 31 KB Add extra validations for all models
# == 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
#  slug                              :string
#  title                             :string
#  visibility                        :integer          default(3), not null
#  created_at                        :datetime
#  updated_at                        :datetime
#  template_id                       :integer
#
# Indexes
#
#  index_plans_on_template_id  (template_id)
#
# Foreign Keys
#
#  fk_rails_...  (template_id => templates.id)
#

class Plan < ActiveRecord::Base
  include ConditionalUserMailer
  include ExportablePlan
  include ValidationMessages
  include ValidationValues

  # =============
  # = Constants =
  # =============

  ##
  # (in mm)
  A4_PAGE_HEIGHT = 297

  ##
  # (in mm)
  A4_PAGE_WIDTH = 210

  ##
  # Round estimate up to nearest 5%
  ROUNDING = 5

  ##
  # convert font point size to mm
  FONT_HEIGHT_CONVERSION_FACTOR = 0.35278

  ##
  # Assume glyph width averages 2/5 the height
  FONT_WIDTH_HEIGHT_RATIO = 0.4


  # public is a Ruby keyword so using publicly
  enum visibility: [:organisationally_visible, :publicly_visible, :is_test, :privately_visible]

  # ================
  # = 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 :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

  # ==============
  # = Attributes =
  # ==============

  accepts_nested_attributes_for :template

  accepts_nested_attributes_for :roles

  # ===============
  # = 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 =
  # ==========

  # Scope queries
  # Note that in ActiveRecord::Enum the mappings are exposed through a class method with the pluralized attribute name (e.g visibilities rather than visibility)
  scope :publicly_visible, -> { includes(:template).where(:visibility => visibilities[:publicly_visible]) }

  # Retrieves any plan in which the user has an active role and it is not a reviewer
  scope :active, -> (user) {
    includes([:template, :roles]).where({ "roles.active": true, "roles.user_id": user.id }).where(Role.not_reviewer_condition)
  }

  # Retrieves any plan organisationally or publicly visible for a given org id
  scope :organisationally_or_publicly_visible, -> (user) {
    includes(:template, {roles: :user})
      .where({
        visibility: [visibilities[:organisationally_visible], visibilities[:publicly_visible]],
        "roles.access": Role.access_values_for(:creator, :administrator, :editor, :commenter).min,
        "users.org_id": user.org_id})
      .where(['NOT EXISTS (SELECT 1 FROM roles WHERE plan_id = plans.id AND user_id = ?)', user.id])
  }

  scope :search, -> (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, -> (id) {
    Plan.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_method :super_settings, :settings


  ##
  # 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.
  # `key` is required by rails-settings, so it's required here, too.
  #
  # @param key [Key] a key required by rails
  # @return [Settings] settings for this plan's template
  def settings(key)
    self_settings = self.super_settings(key)
    return self_settings if self_settings.value?
    self.template.settings(key) unless self.template.nil?
  end

  ##
  # returns the most recent answer to the given question id
  # optionally can create an answer if none exists
  #
  # @param qid [Integer] the id for the question to find the answer for
  # @param create_if_missing [Boolean] if true, will genereate a default answer to the question
  # @return [Answer,nil] the most recent answer to the question, or a new question with default value, or 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 then
      answer = Answer.new
      answer.plan_id = id
      answer.question_id = qid
      answer.text = question.default_value
      default_options = Array.new
      question.question_options.each do |option|
        if option.is_default
          default_options << option
        end
      end
      answer.question_options = default_options
    end
    return answer
  end

# TODO: This just retrieves all of the guidance associated with the themes within the template
#       so why are we transferring it here to the plan?
  ##
  # returns all of the sections for this version of the plan, and for the project's organisation
  #
  # @return [Array<Section>,nil] either a list of sections, or nil if none were found
  def set_possible_guidance_groups
    # find all the themes in this plan
    # and get the guidance groups they belong to
    ggroups = []
    self.template.phases.each do |phase|
      phase.sections.each do |section|
        section.questions.each do |question|
          question.themes.each do |theme|
            theme.guidances.each do |guidance|
              ggroups << guidance.guidance_group if guidance.guidance_group.published
              # only show published guidance groups
            end
          end
        end
      end
    end

    self.guidance_groups = ggroups.uniq
  end

  ##
  # returns all of the possible guidance groups for the plan (all options to
  # be selected by the user to display)
  #
  # @return Array<Guidance>
  def get_guidance_group_options
    # find all the themes in this plan
    # and get the guidance groups they belong to
    ggroups = []
    Template.includes(phases: [sections: [questions: [themes: [guidances: [guidance_group: :org]]]]]).find(self.template_id).phases.each do |phase|
      phase.sections.each do |section|
        section.questions.each do |question|
          question.themes.each do |theme|
            theme.guidances.each do |guidance|
              ggroups << guidance.guidance_group if guidance.guidance_group.published
              # only show published guidance groups
            end
          end
        end
      end
    end
    return ggroups.uniq
  end

  ##
  # 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
        val = Role.access_values_for(:reviewer, :commenter).min
        self.feedback_requested = true

        # Share the plan with each org admin as the reviewer role
        admins = user.org.org_admins
        admins.each do |admin|
          self.roles << Role.new(user: admin, access: val)
        end

        if self.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
          true
        else
          false
        end
      rescue Exception => e
        Rails.logger.error e
        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
        vals = Role.access_values_for(:reviewer)
        self.roles.delete(Role.where(plan: self, access: vals))

        if self.save!
          # Send an email confirmation to the owners and co-owners
          owners = User.joins(:roles).where('roles.plan_id =? AND roles.access IN (?)', self.id, Role.access_values_for(:administrator))
          deliver_if(recipients: owners, key: 'users.feedback_provided') do |r|
            UserMailer.feedback_complete(r, self, org_admin).deliver_now
          end
          true
        else
          false
        end
      rescue Exception => e
        Rails.logger.error e
        false
      end
    end
  end

  # Returns all of the plan's available guidance by question as a hash for use on the write plan page
  # {
  #   QUESTION: {
  #     GUIDANCE_GROUP: {
  #       THEME: [GUIDANCE, GUIDANCE],
  #       THEME: [GUIDANCE]
  #     }
  #   }
  # }
  def guidance_by_question_as_hash
    # Get all of the selected guidance groups for the plan
    guidance_groups_ids = self.guidance_groups.collect(&:id)
    guidance_groups =  GuidanceGroup.joins(:org).where("guidance_groups.published = ? AND guidance_groups.id IN (?)",
                                                       true, guidance_groups_ids)

    # Gather all of the Themes used in the plan as a hash
    # {
    #  QUESTION: [THEME, THEME],
    #  QUESTION: [THEME]
    # }
    question_themes = {}
    themes_used = []
    self.questions.joins(:themes).pluck('questions.id', 'themes.title').each do |qt|
      themes_used << qt[1] unless themes_used.include?(qt[1])
      question_themes[qt[0]] = [] unless question_themes[qt[0]].present?
      question_themes[qt[0]] << qt[1] unless question_themes[qt[0]].include?(qt[1])
    end

    # Gather all of the Guidance available for the themes used in the plan as a hash
    # {
    #  THEME: {
    #    GUIDANCE_GROUP: [GUIDANCE, GUIDANCE],
    #    GUIDANCE_GROUP: [GUIDANCE]
    #  }
    # }
    theme_guidance = {}
    GuidanceGroup.includes(guidances: :themes).joins(:guidances).
          where('guidance_groups.published = ? AND guidances.published = ? AND themes.title IN (?) AND guidance_groups.id IN (?)', true, true, themes_used, guidance_groups.collect(&:id)).
          pluck('guidance_groups.name', 'themes.title', 'guidances.text').each do |tg|

      theme_guidance[tg[1]] = {} unless theme_guidance[tg[1]].present?
      theme_guidance[tg[1]][tg[0]] = [] unless theme_guidance[tg[1]][tg[0]].present?
      theme_guidance[tg[1]][tg[0]] << tg[2] unless theme_guidance[tg[1]][tg[0]].include?(tg[2])
    end

    # Generate a hash for the view that contains all of a question guidance
    # {
    #   QUESTION: {
    #     GUIDANCE_GROUP: {
    #       THEME: [GUIDANCE, GUIDANCE],
    #       THEME: [GUIDANCE]
    #     }
    #   }
    # }
    question_guidance = {}
    question_themes.keys.each do |question|
      ggs = {}
      # Gather all of the guidance groups applicable to the themes assigned to the question
      groups = []
      question_themes[question].each do |theme|
        groups << theme_guidance[theme].keys if theme_guidance[theme].present?
      end

      # Loop through all of the applicable guidance groups and collect their themed guidance
      groups.flatten.uniq.each do |guidance_group|
        guidances_by_theme = {}

        # Collect all of the guidances for each theme used by the question
        question_themes[question].each do |theme|
          if theme_guidance[theme].present? && theme_guidance[theme][guidance_group].present?
            guidances_by_theme[theme] = [] unless guidances_by_theme[theme].present?
            guidances_by_theme[theme] = theme_guidance[theme][guidance_group]
          end
        end

        ggs[guidance_group] = guidances_by_theme unless ggs[guidance_group]
      end

      question_guidance[question] = ggs
    end

    question_guidance
  end

  ##
  # determines if the plan is editable by the specified user
  #
  # @param user_id [Integer] the id for a user
  # @return [Boolean] true if user can edit the plan
  def editable_by?(user_id)
    user_id = user_id.id if user_id.is_a?(User)
    has_role(user_id, :editor)
  end

  ##
  # determines if the plan is readable by the specified user
  #
  # @param user_id [Integer] the id for a user
  # @return [Boolean] true if the user can read the plan
  def readable_by?(user_id)
    user           = user_id.is_a?(User) ? user_id : User.find(user_id)
    owner_orgs     = owner_and_coowners.collect(&:org)
    sys_permission = Branding.fetch(:service_configuration, :plans,
                                    :org_admins_read_all)

    # Super Admins can view plans read-only
    if user.can_super_admin?
      return true
    # Org Admins can view their Org's plans if system permission allows
    elsif user.can_org_admin? && owner_orgs.include?(user.org) && sys_permission
      return true
    # ...otherwise the user must have the commenter role.
    elsif has_role(user.id, :commenter)
      return true
    else
      return false
    end
  end

  ##
  # determines if the plan is readable by the specified user
  #
  # @param user_id [Integer] the id for a user
  # @return [Boolean] true if the user can read the plan
  def commentable_by?(user_id)
    user_id = user_id.id if user_id.is_a?(User)
    has_role(user_id, :commenter)
  end

  ##
  # determines if the plan is administerable by the specified user
  #
  # @param user_id [Integer] the id for the user
  # @return [Boolean] true if the user can administer the plan
  def administerable_by?(user_id)
    user_id = user_id.id if user_id.is_a?(User)
    has_role(user_id, :administrator)
  end

  ##
  # determines if the plan is owned by the specified user
  #
  # @param user_id [Integer] the id for the user
  # @return [Boolean] true if the user can administer the plan
  def owned_by?(user_id)
    user_id = user_id.id if user_id.is_a?(User)
    has_role(user_id, :creator)
  end

  ##
  # determines if the plan is reviewable by the specified user
  #
  # @param user_id [Integer] the id for the user
  # @return [Boolean] true if the user can administer the plan
  def reviewable_by?(user_id)
    user_id = user_id.id if user_id.is_a?(User)
    has_role(user_id, :reviewer)
  end

  ##
  # determines whether or not the specified user has any rol on the plan
  #
  # @param user_id [Integer] the id for the user
  # @return [Boolean] true if the user has any rol
  def any_role?(user)
    user_id = user.id if user.is_a?(User)
    !self.roles.index{ |rol| rol.user_id == user_id }.nil?
  end

  ##
  # defines and returns the status of the plan
  # status consists of a hash of the num_questions, num_answers, sections, questions, and spaced used.
  # For each section, it contains the id's of each of the questions
  # for each question, it contains the answer_id, answer_created_by, answer_text, answer_options_id, aand answered_by
  #
  # @return [Status]

  def status
    status = {
      "num_questions" => 0,
      "num_answers" => 0,
      "sections" => {},
      "questions" => {},
      "space_used" => 0 # percentage of available space in pdf used
    }

    space_used = height_of_text(self.title, 2, 2)

    section_ids = sections.map {|s| s.id}

    # we retrieve this is 2 joins:
    #   1. sections and questions
    #   2. questions and answers
    # why? because Rails 4 doesn't have any sensible left outer join.
    # when we change to RAILS 5 it is meant to have so this can be fixed then

    records = Section.joins(questions: :question_format)
                     .select('sections.id as sectionid,
                              sections.title as stitle,
                              questions.id as questionid,
                              questions.text as questiontext,
                              question_formats.title as qformat')
                     .where("sections.id in (?) ", section_ids)
                     .to_a

    # extract question ids to get answers
    question_ids = records.map {|r| r.questionid}.uniq
    status["num_questions"] = question_ids.count

    arecords = Question.joins(answers: :user)
                       .select('questions.id as questionid,
                                answers.id as answerid,
                                answers.plan_id as plan_id,
                                answers.text as answertext,
                                answers.updated_at as updated,
                                users.email as username')
                       .where("questions.id in (?) and answers.plan_id = ?",question_ids, self.id)
                       .to_a

    # we want answerids to extract options later
    answer_ids = arecords.map {|r| r.answerid}.uniq
    status["num_answers"] = answer_ids.count

    # create map from questionid to answer structure
    qa_map = {}
    arecords.each do |rec|
      qa_map[rec.questionid] = {
        plan: rec.plan_id,
        id: rec.answerid,
        text: rec.answertext,
        updated: rec.updated,
        user: rec.username
      }
    end


    # build main status structure
    records.each do |rec|
      sid = rec.sectionid
      stitle = rec.stitle
      qid = rec.questionid
      qtext = rec.questiontext
      format = rec.qformat

      answer = nil
      if qa_map.has_key?(qid)
        answer = qa_map[qid]
      end

      aid = answer.nil? ? nil : answer[:id]
      atext = answer.nil? ? nil : answer[:text]
      updated = answer.nil? ? nil : answer[:updated]
      uname = answer.nil? ? nil : answer[:user]

      space_used += height_of_text(stitle, 1, 1)

      shash = status["sections"]
      if !shash.has_key?(sid)
        shash[sid] = {}
        shash[sid]["num_questions"] = 0
        shash[sid]["num_answers"] = 0
        shash[sid]["questions"] = Array.new
      end

      shash[sid]["questions"] << qid
      shash[sid]["num_questions"] += 1

      space_used += height_of_text(qtext) unless qtext == stitle
      if atext.present?
        space_used += height_of_text(atext)
      else
        space_used += height_of_text(_('Question not answered.'))
      end

      if answer.present? then
        shash[sid]["num_answers"] += 1
      end

      status["questions"][qid] = {
        "format" => format,
        "answer_id" => aid,
        "answer_updated_at" => updated.to_i,
        "answer_text" => atext,
        "answered_by" => uname
      }

    end

    records = Answer.joins(:question_options).select('answers.id as answerid, question_options.id as optid').where(id: answer_ids).to_a
    opt_hash = {}
    records.each do |rec|
      aid = rec.answerid
      optid = rec.optid
      if !opt_hash.has_key?(aid)
        opt_hash[aid] = Array.new
      end
      opt_hash[aid] << optid
    end

    status["questions"].each_key do |questionid|
      answerid = status["questions"][questionid]["answer_id"]
      status["questions"][questionid]["answer_option_ids"] = opt_hash[answerid]
    end

    status['space_used'] = estimate_space_used(space_used)

    return status
  end


  ##
  # assigns the passed user_id to the creater_role for the project
  # gives the user rights to read, edit, administrate, and defines them as creator
  #
  # @param user_id [Integer] the user to be given priveleges' id
  def assign_creator(user_id)
    user_id = user_id.id if user_id.is_a?(User)
    add_user(user_id, true, true, true)
  end

  ##
  # returns the funder id for the plan
  #
  # @return [Integer, nil] the id for the funder
  def funder_id
    if self.template.nil? then
      return nil
    end
    return self.template.org
  end

  ##
  # returns the funder organisation for the project or nil if none is specified
  #
  # @return [Organisation, nil] the funder for project, or nil if none exists
  def funder
    template = self.template
    if template.nil? then
      return nil
    end

    if template.customization_of
      return template.customization_of.org
    else
      return template.org
    end
  end

  ##
  # assigns the passed user_id as an editor for the project
  # gives the user rights to read and edit
  #
  # @param user_id [Integer] the user to be given priveleges' id
  def assign_editor(user_id)
    add_user(user_id, true)
  end

  ##
  # assigns the passed user_id as a reader for the project
  # gives the user rights to read
  #
  # @param user_id [Integer] the user to be given priveleges' id
  def assign_reader(user_id)
    add_user(user_id)
  end

  ##
  # assigns the passed user_id as an administrator for the project
  # gives the user rights to read, adit, and administrate the project
  #
  # @param user_id [Integer] the user to be given priveleges' id
  def assign_administrator(user_id)
    add_user(user_id, true, true)
  end

  ##
  # the datetime for the latest update of this plan
  #
  # @return [DateTime] the time of latest update
  def latest_update
    latest_update = updated_at
    phases.each do |phase|
      if phase.updated_at > latest_update then
        latest_update = phase.updated_at
      end
    end
    return latest_update
  end

  # Getters to match 'My plans' columns

  ##
  # the title of the project
  #
  # @return [String] the title of the project
  def name
    self.title
  end

  ##
  # the owner of the project
  #
  # @return [User] the creater of the project
  def owner
    vals = Role.access_values_for(:creator)
    User.joins(:roles).where('roles.plan_id = ? AND roles.access IN (?)', self.id, vals).first
  end

  ##
  # returns the shared roles of a plan, excluding the creator
  def shared
    role_values = Role.where(plan: self).where(Role.not_creator_condition).any?
  end

  ##
  # the owner and co-owners of the project
  #
  # @return [Users]
  def owner_and_coowners
    vals = Role.access_values_for(:creator).concat(Role.access_values_for(:administrator))
    User.joins(:roles).where("roles.plan_id = ? AND roles.access IN (?)", self.id, vals)
  end

  ##
  # the time the project was last updated, formatted as a date
  #
  # @return [Date] last update as a date
  def last_edited
    self.latest_update.to_date
  end

  # Returns the number of answered questions from the entire plan
  def num_answered_questions
    return Answer.where(id: answers.map(&:id)).includes({ question: :question_format }, :question_options).reduce(0) do |m, a|
      if a.is_valid?
        m+=1
      end
      m
    end
  end

  # Returns a section given its id or nil if does not exist for the current plan
  def get_section(section_id)
    self.sections.find { |s| s.id == section_id }
  end

  # Returns the number of questions for a plan.
  def num_questions
    return sections.includes(:questions).joins(:questions).reduce(0){ |m, s| m + s.questions.length }
  end
  # the following two methods are for eager loading. One gets used for the plan/show
  # page and the oter for the plan/edit. The difference is just that one pulls in more than
  # the other.
  # TODO: revisit this and work out for sure that maintaining the difference is worthwhile.
  # it may not be. Also make sure nether is doing more thanit needs to.
  #
  def self.eager_load(id)
    Plan.includes(
      [{template: [
                   {phases: {sections: {questions: :answers}}}
                  ]},
       {plans_guidance_groups: {guidance_group: :guidances}}
      ]).find(id)
  end

  # 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(id, phase_id)
    plan = Plan
      .joins(template: { phases: { sections: :questions }})
      .preload(template: { phases: { sections: :questions }}) # Preserves the default order defined in the model relationships
      .where("plans.id = :id AND phases.id = :phase_id", { id: id, phase_id: phase_id })
      .merge(Plan.includes(answers: :notes))[0]
    phase = plan.template.phases.find {|p| p.id==phase_id.to_i }

    return plan, phase
  end

  # deep copy the given plan and all of it's associations
  #
  # @params [Plan] plan to be deep copied
  # @return [Plan] saved copied plan
  def self.deep_copy(plan)
    plan_copy = plan.dup
    plan_copy.title = "Copy of " + plan.title
    plan_copy.save!
    plan.answers.each do |answer|
      answer_copy = Answer.deep_copy(answer)
      answer_copy.plan_id = plan_copy.id
      answer_copy.save!
    end
    plan.guidance_groups.each do |guidance_group|
      if guidance_group.present?
        plan_copy.guidance_groups << GuidanceGroup.where(id: guidance_group.id).first
      end
    end
    return plan_copy
  end

  # Returns visibility message given a Symbol type visibility passed, otherwise nil
  def self.visibility_message(type)
    message = {
      :organisationally_visible => _('organisational'),
      :publicly_visible => _('public'),
      :is_test => _('test'),
      :privately_visible => _('private')
    }
    message[type]
  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
  def visibility_allowed?
    value=(self.num_answered_questions().to_f/self.num_questions()*100).round(2)
    !self.is_test? && value >= Rails.application.config.default_plan_percentage_answered
  end

  # Determines whether or not a question (given its id) exists for the self plan
  def question_exists?(question_id)
    Plan.joins(:questions).exists?(id: self.id, "questions.id": question_id)
  end

  # Checks whether or not the number of questions matches the number of valid answers
  def no_questions_matches_no_answers?
    num_questions = question_ids.length
    pre_fetched_answers = Answer
      .includes({ question: :question_format }, :question_options)
      .where(id: answer_ids)
    num_answers = pre_fetched_answers.reduce(0) do |m, a|
      if a.is_valid?
        m+=1
      end
      m
    end
    return num_questions == num_answers
  end

  private

  # Returns whether or not the user has the specified role for the plan
  def has_role(user_id, role_as_sym)
    if user_id.is_a?(Integer) && role_as_sym.is_a?(Symbol)
      vals = Role.access_values_for(role_as_sym)
      self.roles.where(user_id: user_id, access: vals, active: true).first.present?
    else
      false
    end
  end

  ##
  # adds a user to the project
  # if no flags are specified, the user is given read privleges
  #
  # @param user_id [Integer] the user to be given privleges
  # @param is_editor [Boolean] whether or not the user can edit the project
  # @param is_administrator [Boolean] whether or not the user can administrate the project
  # @param is_creator [Boolean] wheter or not the user created the project
  # @return [Array<ProjectGroup>]
  #
  # TODO: change this to specifying uniqueness of user/plan association and handle
  # that way
  #
  def add_user(user_id, is_editor = false, is_administrator = false, is_creator = false)
    Role.where(plan_id: self.id, user_id: user_id).each do |r|
      r.destroy
    end

    role = Role.new
    role.user_id = user_id
    role.plan_id = id

    # if you get assigned a role you can comment
    role.commenter= true

    # the rest of the roles are inclusing so creator => administrator => editor
    if is_creator
      role.creator =  true
      role.administrator = true
      role.editor = true
    end

    if is_administrator
      role.administrator = true
      role.editor = true
    end

    if is_editor
      role.editor = true
    end

    role.save

    # This is necessary because we're creating the associated record but not assigning it
    # to roles. Auto-saving like this may be confusing when coding upstream in a controller,
    # view or api. Should probably change this to:
    #    self.roles << role
    # and then let the save be called manually via:
    #    plan.save!
    #self.reload
  end

  ##
  # creates a plan for each phase in the template associated with this project
  # unless the phase is unpublished, it creates a new plan, and a new version of the plan and adds them to the project's plans
  #
  # @return [Array<Plan>]
  def create_plans
    self.template.phases.each do |phase|
      latest_published_version = phase.latest_published_version
      unless latest_published_version.nil?
        new_plan = Plan.new
        new_plan.version = latest_published_version
        plans << new_plan
      end
    end
  end



  ##
  # Based on the height of the text gathered so far and the available vertical
  # space of the pdf, estimate a percentage of how much space has been used.
  # This is highly dependent on the layout in the pdf. A more accurate approach
  # would be to render the pdf and check how much space had been used, but that
  # could be very slow.
  # NOTE: This is only an estimate, rounded up to the nearest 5%; it is intended
  # for guidance when editing plan data, not to be 100% accurate.
  #
  # @param used_height [Integer] an estimate of the height used so far
  # @return [Integer] the estimate of space used of an A4 portrain
  def estimate_space_used(used_height)
    @formatting ||= self.settings(:export).formatting

    return 0 unless @formatting[:font_size] > 0

    margin_height    = @formatting[:margin][:top].to_i + @formatting[:margin][:bottom].to_i
    page_height      = A4_PAGE_HEIGHT - margin_height # 297mm for A4 portrait
    available_height = page_height * self.template.settings(:export).max_pages

    percentage = (used_height / available_height) * 100
    (percentage / ROUNDING).ceil * ROUNDING # round up to nearest five
  end

  ##
  # Take a guess at the vertical height (in mm) of the given text based on the
  # font-size and left/right margins stored in the plan's settings.
  # This assumes a fixed-width for each glyph, which is obviously
  # incorrect for the font-face choices available; the idea is that
  # they'll hopefully average out to that in the long-run.
  # Allows for hinting different font sizes (offset from base via font_size_inc)
  # and vertical margins (i.e. for heading text)
  #
  # @param text [String] the text to estimate size of
  # @param font_size_inc [Integer] the size of the font of the text, defaults to 0
  # @param vertical_margin [Integer] the top margin above the text, defaults to 0
  def height_of_text(text, font_size_inc = 0, vertical_margin = 0)
    @formatting     ||= self.settings(:export).formatting
    @margin_width   ||= @formatting[:margin][:left].to_i + @formatting[:margin][:right].to_i
    @base_font_size ||= @formatting[:font_size]

    return 0 unless @base_font_size > 0

    font_height = FONT_HEIGHT_CONVERSION_FACTOR * (@base_font_size + font_size_inc)
    font_width  = font_height * FONT_WIDTH_HEIGHT_RATIO # Assume glyph width averages at 2/5s the height
    leading     = font_height / 2

    chars_in_line = (A4_PAGE_WIDTH - @margin_width) / font_width # 210mm for A4 portrait
    num_lines = (text.length / chars_in_line).ceil

    (num_lines * font_height) + vertical_margin + leading
  end

  # Initialize the title for new templates
  # --------------------------------------------------------
  def set_creation_defaults
    # Only run this before_validation because rails fires this before save/create
    if self.id.nil?
      self.title = "My plan (#{self.template.title})" if self.title.nil? && !self.template.nil?
    end
  end
end