SysMO-DB/seek

View on GitHub
app/models/person.rb

Summary

Maintainability
D
2 days
Test Coverage
require 'grouped_pagination'

class Person < ActiveRecord::Base

  include Seek::Rdf::RdfGeneration
  include Seek::Taggable
  include Seek::AdminDefinedRoles

  alias_attribute :title, :name

  acts_as_yellow_pages
  scope :default_order, order("last_name, first_name")

  before_save :first_person_admin
  before_destroy :clean_up_and_assign_permissions

  acts_as_notifiee
  acts_as_annotatable :name_field=>:name

  validates_presence_of :email

  #FIXME: consolidate these regular expressions into 1 holding class
  validates_format_of :email,:with => RFC822::EMAIL
  validates_format_of :web_page, :with=>/(^$)|(^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(([0-9]{1,5})?\/.*)?$)/ix,:allow_nil=>true,:allow_blank=>true

  validates_uniqueness_of :email,:case_sensitive => false

  has_and_belongs_to_many :disciplines

  has_many :group_memberships, :dependent => :destroy

  has_many :favourite_group_memberships, :dependent => :destroy
  has_many :favourite_groups, :through => :favourite_group_memberships

  #MERGENOTE - needs checking
  has_many :work_groups, :through=>:group_memberships, :after_add => [:subscribe_to_work_group_project, :touch_work_group_project],
  :after_remove => [:unsubscribe_to_work_group_project, :touch_work_group_project]
  has_many :studies_for_person, :as=>:contributor, :class_name=>"Study"  
  has_many :assays,:foreign_key => :owner_id
  has_many :investigations_for_person,:as=>:contributor, :class_name=>"Investigation"
  has_many :presentations_for_person,:as=>:contributor, :class_name=>"Presentation"

  validate :orcid_id_must_be_valid_or_blank

  has_one :user, :dependent=>:destroy

  has_many :assets_creators, :dependent => :destroy, :foreign_key => "creator_id"
  has_many :created_data_files, :through => :assets_creators, :source => :asset, :source_type => "DataFile"
  has_many :created_models, :through => :assets_creators, :source => :asset, :source_type => "Model"
  has_many :created_sops, :through => :assets_creators, :source => :asset, :source_type => "Sop"
  has_many :created_publications, :through => :assets_creators, :source => :asset, :source_type => "Publication"
  has_many :created_presentations,:through => :assets_creators,:source=>:asset,:source_type => "Presentation"

  searchable(:auto_index => false) do
    text :project_roles
    text :disciplines do
      disciplines.map{|d| d.title}
    end
  end if Seek::Config.solr_enabled

  scope :with_group, :include=>:group_memberships, :conditions=>"group_memberships.person_id IS NOT NULL"
  scope :without_group, :include=>:group_memberships, :conditions=>"group_memberships.person_id IS NULL"
  scope :registered,:include=>:user,:conditions=>"users.person_id != 0"

  #FIXME: change userless_people to use this scope - unit tests
  scope :not_registered,:include=>:user,:conditions=>"users.person_id IS NULL"

  alias_attribute :webpage,:web_page

  has_many :project_subscriptions, :before_add => proc {|person, ps| ps.person = person},:uniq=> true, :dependent => :destroy
  accepts_nested_attributes_for :project_subscriptions, :allow_destroy => true

  has_many :subscriptions,:dependent => :destroy

  def subscribe_to_work_group_project wg
    #subscribe direct project
    project_subscriptions.build :project => wg.project unless project_subscriptions.detect{|ps| ps.project_id == wg.project_id}

  end

  def unsubscribe_to_work_group_project wg
     # clear project_subscriptions and all subscriptions if person is not project member
    if work_groups.empty?
          project_subscriptions.delete_all
          subscriptions.delete_all
    elsif ps = project_subscriptions.detect{|ps| ps.project_id == wg.project_id}
      #unsunscribe direct project subscriptions
      project_subscriptions.delete ps
    end

  end

  #MERGENOTE - check this
  #touch project to expire cache for project members on project show page?
  def touch_work_group_project wg
    wg.project.touch
  end

  #MERGENOTE - why are we not inculding AuthLookup module?
  after_commit :queue_update_auth_table

  def queue_update_auth_table
    if previous_changes.keys.include?("roles_mask")
      AuthLookupUpdateJob.add_items_to_queue self
    end
  end

  def guest_project_member?
    project = Project.find_by_title('BioVeL Portal Guests')
    !project.nil? && self.projects == [project]
  end

  #those that have updated time stamps and avatars appear first. A future enhancement could be to judge activity by last asset updated timestamp
  def self.active
    Person.unscoped.order("avatar_id is null, updated_at DESC")
  end

  def receive_notifications
    member? and super
  end

  def registered?
    !user.nil?
  end

  def person
    self
  end

  def email_uri
    URI.escape("mailto:"+email)
  end

  def studies
    result = studies_for_person
    if user
      result = (result | user.studies).compact
    end
    result.uniq
  end

  def investigations
    result = investigations_for_person
    if user
      result = (result | user.investigations).compact
    end
    result.uniq
  end

  def presentations
    result = presentations_for_person
    if user
      result = (result | user.investigations).compact
    end
    result.uniq
  end

  def related_samples
    user_items = []
    user_items =  user.try(:send,:samples) if user.respond_to?(:samples)
    user_items
  end

  def programmes
    self.projects.collect{|p| p.programme}.uniq
  end


  #MERGENOTE - perhaps need programmes in here too
  RELATED_RESOURCE_TYPES = [:data_files,:models,:sops,:presentations,:events,:publications, :investigations]
  RELATED_RESOURCE_TYPES.each do |type|
    define_method "related_#{type}" do
      user_items = []
      user_items =  user.try(:send,type) if user.respond_to?(type) && [:events,:investigations].include?(type)
      user_items =  user_items | self.send("created_#{type}".to_sym) if self.respond_to? "created_#{type}".to_sym
      user_items = user_items | self.send("#{type}_for_person".to_sym) if self.respond_to? "#{type}_for_person".to_sym
      user_items.uniq
    end
  end


  def self.userless_people
    p=Person.all
    return p.select{|person| person.user.nil?}
  end

  #returns an array of Person's where the first and last name match
  def self.duplicates
    people=Person.all
    dup=[]
    people.each do |p|
      peeps=people.select{|p2| p.name==p2.name}
      dup = dup | peeps if peeps.count>1
    end
    return dup
  end

  # get a list of people with their email for autocomplete fields
  def self.get_all_as_json
    all_people = Person.order("ID asc")
    names_emails = all_people.collect{ |p| {"id" => p.id,
        "name" => (p.first_name.blank? ? (logger.error("\n----\nUNEXPECTED DATA: person id = #{p.id} doesn't have a first name\n----\n"); "(NO FIRST NAME)") : h(p.first_name)) + " " +
                  (p.last_name.blank? ? (logger.error("\n----\nUNEXPECTED DATA: person id = #{p.id} doesn't have a last name\n----\n"); "(NO LAST NAME)") : h(p.last_name)),
        "email" => (p.email.blank? ? "unknown" : h(p.email)) } }
    return names_emails.to_json
  end

  def validates_associated(*associations)
    associations.each do |association|
      class_eval do
        validates_each(associations) do |record, associate_name, value|
          associates = record.send(associate_name)
          associates = [associates] unless associates.respond_to?('each')
          associates.each do |associate|
            if associate && !associate.valid?
              associate.errors.each do |key, value|
                record.errors.add(key, value)
              end
            end
          end
        end
      end
    end
  end

  def people_i_may_know
    res=[]
    institutions.each do |i|
      i.people.each do |p|
        res << p unless p==self or res.include? p
      end
    end

    projects.each do |proj|
      proj.people.each do |p|
        res << p unless p==self or res.include? p
      end
    end
    return  res
  end

  def can_create_new_items?
    member?
  end

  def workflows
     self.try(:user).try(:workflows) || []
  end

  def runs
    self.try(:user).try(:taverna_player_runs) || []
  end

  def sweeps
    self.try(:user).try(:sweeps) || []
  end

  def institutions
    work_groups.collect {|wg| wg.institution }.uniq
  end


  def projects
      #updating workgroups doesn't change groupmemberships until you save. And vice versa.
      work_groups.collect {|wg| wg.project }.uniq | group_memberships.collect{|gm| gm.work_group.project}
  end

  def member?
    !projects.empty?
  end

  def member_of?(item_or_array)
    array = Array(item_or_array)
    #MERGENOTE - needs a closer look, and simplifying
    array.detect {|item|Rails.cache.fetch([:member_of?, self.cache_key, item.cache_key]) { (item.is_a?(Project) && projects.include?(item)) || item.people.include?(self)}}
  end

  def locations
    # infer all person's locations from the institutions where the person is member of
    locations = self.institutions.collect(&:country).select { |l| !l.blank? }
    return locations
  end

  def email_with_name
    name + " <" + email + ">"
  end

  def name
    firstname=first_name
    firstname||=""
    lastname=last_name
    lastname||=""
    #capitalize, including double barrelled names
    #TODO: why not just store them like this rather than processing each time? Will need to reprocess exiting entries if we do this.
    return (firstname.gsub(/\b\w/) {|s| s.upcase} + " " + lastname.gsub(/\b\w/) {|s| s.upcase}).strip
  end

  #returns true this is an admin person, and they are the only one defined - indicating they are person creating during setting up SEEK
  def only_first_admin_person?
    Person.count==1 && [self]==Person.all && Person.first.is_admin?
  end

  #the roles defined within the project
  def project_roles
    project_roles = []
    group_memberships.each do |gm|
      project_roles = project_roles | gm.project_roles
    end
    project_roles
  end

  def update_first_letter
    no_last_name=last_name.nil? || last_name.strip.blank?
    first_letter = strip_first_letter(last_name) unless no_last_name
    first_letter = strip_first_letter(name) if no_last_name
    #first_letter = "Other" unless ("A".."Z").to_a.include?(first_letter)
    self.first_letter=first_letter
  end

  def project_roles_of_project(projects_or_project)
    #Get intersection of all project memberships + person's memberships to find project membership
    #MERGENOTE - just make it into an array    
    if projects_or_project.is_a? Array
      memberships = group_memberships.select{|g| projects_or_project.include? g.work_group.project}
    else
      memberships = group_memberships.select{|g| g.work_group.project == projects_or_project}
    end
    return memberships.collect{|m| m.project_roles}.flatten
  end

  def assets
    created_data_files | created_models | created_sops | created_publications | created_presentations
  end

  #can be edited by:
  #(admin or project managers of this person) and (this person does not have a user or not the other admin)
  #themself
  def can_be_edited_by?(subject)
    return false if subject.nil?
    subject = subject.user if subject.is_a?(Person)
    subject == self.user || subject.is_admin? || self.is_managed_by?(subject)
  end

  #determines if this person is the member of a project for which the user passed is a project manager,
  # #and the current person is not an admin
  def is_managed_by? user
    return false if self.is_admin?
    match = self.projects.find do |p|
      user.person.is_project_manager?(p)
    end
    !match.nil?
  end

  #admin can administer other people, project manager can administer other people except other admins and themself
  def can_be_administered_by?(user)
    return false if user.nil? || user.person.nil?


    user.is_admin? || (user.person.is_project_manager_of_any_project? && (self.is_admin? || self!=user.person))
  end

  def can_view? user = User.current_user
    !user.nil? || !Seek::Config.is_virtualliver
  end

  def can_edit? user = User.current_user
    new_record? || can_be_edited_by?(user)
  end



  def can_manage? user = User.current_user
    user.try(:is_admin?)
  end

  def can_destroy? user = User.current_user
    can_manage? user
  end

  def title_is_public?
    true
  end

  def expertise= tags
    if tags.kind_of? Hash
      return tag_with_params(tags,"expertise")
    else
      return tag_with(tags,"expertise")
    end
  end

  def tools= tags
    if tags.kind_of? Hash
      tag_with_params tags,"tool"
    else
      tag_with tags,"tool"
    end
  end

  def expertise
    annotations_with_attribute("expertise").collect{|a| a.value}
  end

  def tools
    annotations_with_attribute("tool").collect{|a| a.value}
  end

    #retrieve the items that this person is contributor (owner for assay)
  def related_items
     related_items = []
     related_items |= assays
     unless user.blank?
       related_items |= user.assets
       related_items |= user.presentations
       related_items |= user.events
       related_items |= user.investigations
       related_items |= user.studies
     end
     related_items
  end

  #remove the permissions which are set on this person
  def remove_permissions
    permissions = Permission.where(["contributor_type =? and contributor_id=?", 'Person', id])
    permissions.each do |p|
      p.destroy
    end
  end

  def clean_up_and_assign_permissions
    #remove the permissions which are set on this person
    remove_permissions

    #retrieve the items that this person is contributor (owner for assay)
    person_related_items = related_items

    #check if anyone has manage right on the related_items
    #if not or if only the contributor then assign the manage right to pis||pals
    person_related_items.each do |item|
      people_can_manage_item = item.people_can_manage
      if people_can_manage_item.blank? || (people_can_manage_item == [[id, "#{name}", Policy::MANAGING]])
        #find the projects which this person and item belong to
        projects_in_common = projects & item.projects
        pis = projects_in_common.collect{|p| p.pis}.flatten.uniq
        pis.reject!{|pi| pi.id == id}
        item.policy_or_default
        policy = item.policy
        unless pis.blank?
          pis.each do |pi|
            policy.permissions.build(:contributor => pi, :access_type => Policy::MANAGING)
            policy.save
          end
        else
          pals = projects_in_common.collect{|p| p.pals}.flatten.uniq
          pals.reject!{|pal| pal.id == id}
          pals.each do |pal|
            policy.permissions.build(:contributor => pal, :access_type => Policy::MANAGING)
            policy.save
          end
        end
      end
    end
  end

  private

  #a before_save trigger, that checks if the person is the first one created, and if so defines it as admin
  def first_person_admin
    self.is_admin = true if Person.count==0
  end

  def orcid_id_must_be_valid_or_blank
    unless orcid.blank? || valid_orcid_id?(orcid.gsub("http://orcid.org/",""))
        errors.add("Orcid identifier"," isn't a valid ORCID identifier.")
    end
  end

  #checks the structure of the id, and whether is conforms to ISO/IEC 7064:2003
  def valid_orcid_id? id
    if id =~ /[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9,X]{4}/
      id = id.gsub("-","")
      id[15] == orcid_checksum(id)
    else
      false
    end
  end

  #calculating the checksum according to ISO/IEC 7064:2003, MOD 11-2 ; see - http://support.orcid.org/knowledgebase/articles/116780-structure-of-the-orcid-identifier
  def orcid_checksum(id)
    total=0
    (0...15).each { |x| total = (total + id[x].to_i) * 2 }
    remainder = total % 11
    result = (12 - remainder) % 11
    result == 10 ? "X" : result.to_s
  end

  include Seek::ProjectHierarchies::PersonExtension if Seek::Config.project_hierarchy_enabled
end