robertgauld/osm

View on GitHub
lib/osm/member.rb

Summary

Maintainability
F
6 days
Test Coverage
module Osm

  class Member < Osm::Model
    # Constants for group id
    GID_PRIMARY_CONTACT = 1
    GID_SECONDARY_CONTACT = 2
    GID_EMERGENCY_CONTACT = 3
    GID_DOCTOR_CONTACT = 4
    GID_CUSTOM = 5
    GID_MEMBER_CONTACT = 6
    GID_FLOATING = 7

    # Constants for column id
    CID_FIRST_NAME = 2
    CID_LAST_NAME = 3
    CID_ADDRESS_1 = 7
    CID_ADDRESS_2 = 8
    CID_ADDRESS_3 = 9
    CID_ADDRESS_4 = 10
    CID_POSTCODE = 11
    CID_EMAIL_1 = 12
    CID_RECIEVE_EMAIL_1 = 13
    CID_EMAIL_2 = 14
    CID_RECIEVE_EMAIL_2 = 15
    CID_PHONE_1 = 18
    CID_RECIEVE_PHONE_1 = 19
    CID_PHONE_2 = 20
    CID_RECIEVE_PHONE_2 = 21
    CID_GENDER = 34
    CID_SURGERY = 54
    CORE_FIELD_IDS = (1..21).to_a + [34, 54]


    # @!attribute [rw] id
    #   @return [Fixnum] the id for the member
    # @!attribute [rw] section_id
    #   @return [Fixnum] the section the member belongs to
    # @!attribute [rw] first_name
    #   @return [String] the member's first name
    # @!attribute [rw] last_name
    #   @return [String] the member's last name
    # @!attribute [rw] grouping_id
    #   @return [Fixnum] the ID of the grouping within the section that the member belongs to
    # @!attribute [rw] grouping_label
    #   @return [String] the name of the grouping within the section that the member belongs to
    # @!attribute [rw] grouping_leader
    #   @return [Fixnum] whether the member is the grouping leader (0=no, 1=seconder/APL, 2=sixer/PL, 3=senior PL)
    # @!attribute [rw] grouping_leader_label
    #   @return [String] whether the member is the grouping leader
    # @!attribute [rw] age
    #   @return [String] the member's current age (yy/mm) 
    # @!attribute [rw] gender
    #   @return [Symbol] the member's gender (:male, :female, :other or :unspecified)
    # @!attribute [rw] date_of_birth
    #   @return [Date] the member's date of birth
    # @!attribute [rw] started_section
    #   @return [Date] when the member started the section they were retrieved for
    # @!attribute [rw] finished_section
    #   @return [Date] when the member finished the section they were retrieved for
    # @!attribute [rw] joined_movement
    #   @return [Date] when the member joined the movement
    # @!attribute [rw] additional_information
    #   @return [DirtyHashy] the Additional Information (key is OSM's variable name, value is the data)
    # @!attribute [rw] additional_information_labels
    #   @return [DirtyHashy] the labels for the additional information (key is OSM's variable name, value is the label)
    # @!attribute [rw] contact
    #   @return [Osm::Member::MemberContact, nil] the member's contact details (nil if hidden in OSM)
    # @!attribute [rw] primary_contact
    #   @return [Osm::Member::PrimaryContact, nil] the member's primary contact (primary contact 1 in OSM) (nil if hidden in OSM)
    # @!attribute [rw] secondary_contact
    #   @return [Osm::Member::SecondaryContact, nil] the member's secondary contact (primary contact 2 in OSM) (nil if hidden in OSM)
    # @!attribute [rw] emergency_contact
    #   @return [Osm::Member::EmergencyContact, nil] the member's emergency contact (nil if hidden in OSM)
    # @!attribute [rw] doctor
    #   @return [Osm::Member::DoctorContact, nil] the member's doctor (nil if hidden in OSM)

    attribute :id, :type => Integer
    attribute :section_id, :type => Integer
    attribute :first_name, :type => String
    attribute :last_name, :type => String
    attribute :grouping_id, :type => Integer
    attribute :grouping_label, :type => String
    attribute :grouping_leader, :type => Integer
    attribute :grouping_leader_label, :type => String
    attribute :age, :type => String
    attribute :date_of_birth, :type => Date
    attribute :started_section, :type => Date
    attribute :finished_section, :type => Date
    attribute :joined_movement, :type => Date
    attribute :gender, :type => Object
    attribute :additional_information, :type => Object, :default => DirtyHashy.new
    attribute :additional_information_labels, :type => Object, :default => DirtyHashy.new
    attribute :contact, :type => Object
    attribute :primary_contact, :type => Object
    attribute :secondary_contact, :type => Object
    attribute :emergency_contact, :type => Object
    attribute :doctor, :type => Object

    if ActiveModel::VERSION::MAJOR < 4
      attr_accessible :id, :section_id, :first_name, :last_name,
                      :grouping_id, :grouping_leader,
                      :date_of_birth, :started_section, :finished_section, :joined_movement, :age,
                      :grouping_label, :grouping_leader_label, :gender,
                      :additional_information, :additional_information_labels,
                      :contact, :primary_contact, :secondary_contact, :emergency_contact, :doctor
    end

    unless ActiveModel::VERSION::MAJOR < 4
      validates_presence_of :grouping_label, :allow_blank => true
      validates_presence_of :grouping_leader_label, :allow_blank => true
      validates_presence_of :additional_information, :allow_blank => true
      validates_presence_of :additional_information_labels, :allow_blank => true
      validates_presence_of :finished_section, :allow_nil=>true
    end
    validates_numericality_of :id, :only_integer=>true, :greater_than=>0, :unless => Proc.new { |r| r.id.nil? }
    validates_numericality_of :section_id, :only_integer=>true, :greater_than=>0
    validates_numericality_of :grouping_id, :only_integer=>true, :greater_than_or_equal_to=>-2
    validates_numericality_of :grouping_leader, :only_integer=>true, :greater_than_or_equal_to=>0, :less_than_or_equal_to=>14, :allow_nil => true
    validates_presence_of :first_name
    validates_presence_of :last_name
    validates_presence_of :date_of_birth
    validates_presence_of :started_section
    validates_presence_of :joined_movement
    validates_format_of :age, :with => /\A[0-9]{1,3} \/ (?:0?[0-9]|1[012])\Z/, :message => 'age is not in the correct format (yy / mm)', :allow_blank => true
    validates_inclusion_of :gender, :in => [:male, :female, :other, :unspecified], :allow_nil => true
    validates :contact, :validity=>{allow_nil: true}
    validates :primary_contact, :validity=>{allow_nil: true}
    validates :secondary_contact, :validity=>{allow_nil: true}
    validates :emergency_contact, :validity=>{allow_nil: true}
    validates :doctor, :validity=>{allow_nil: true}


    # Get members for a section
    # @param [Osm::Api] api The api to use to make the request
    # @param [Osm::Section, Fixnum, #to_i] section The section (or its ID) to get the members for
    # @param [Osm::Term, Fixnum, #to_i, nil] term The term (or its ID) to get the members for, passing nil causes the current term to be used
    # @!macro options_get
    # @return [Array<Osm::Member>]
    def self.get_for_section(api, section, term=nil, options={})
      require_ability_to(api, :read, :member, section, options)
      if term.nil?
        section = Osm::Section.get(api, section) if section.is_a?(Fixnum)
        term = section.waiting? ? -1 : Osm::Term.get_current_term_for_section(api, section)
      end
      cache_key = ['members', section.to_i, term.to_i]

      if !options[:no_cache] && cache_exist?(api, cache_key)
        return cache_read(api, cache_key)
      end

      result = Array.new

      api_response = api.perform_query('ext/members/contact/grid/?action=getMembers', {
        'section_id' => section.to_i,
        'term_id' => term.to_i,
      })

      data = api_response['data'].is_a?(Hash) ? api_response['data'].values : []
      structure = (api_response['meta'] || {})['structure'] || []
      structure = Hash[ structure.map{ |i| [i['group_id'].to_i, i ] } ] # Make a hash of identifier to group data hash

      custom_labels = {}
      key_key = 'column_id'   # the key in the data from OSM to use as the key in additional_information and labels hashes
      structure.each do |gid, group|
        columns = group['columns'] || []
        custom_labels[gid.to_i] = Hash[ columns.map.select{ |a| gid.eql?(GID_CUSTOM) || !CORE_FIELD_IDS.include?(a['column_id'].to_i) }.map{ |c| [c[key_key], c['label']] } ]
      end

      data.each do |item|
        item_data = Hash[ item['custom_data'].map{ |k,v| [k.to_i, v] } ]
        member_contact = item_data[GID_MEMBER_CONTACT].nil? ? nil : Hash[ item_data[GID_MEMBER_CONTACT].map{ |k,v| [k.to_i, v] }.select{ |k,v| CORE_FIELD_IDS.include?(k) } ]
        member_custom = item_data[GID_MEMBER_CONTACT].nil? ? DirtyHashy.new : DirtyHashy[ item_data[GID_MEMBER_CONTACT].select{ |k,v| !CORE_FIELD_IDS.include?(k.to_i) }.map{ |k,v| [k.to_i, v] } ]
        primary_contact = item_data[GID_PRIMARY_CONTACT].nil? ? nil : Hash[ item_data[GID_PRIMARY_CONTACT].map{ |k,v| [k.to_i, v] }.select{ |k,v| CORE_FIELD_IDS.include?(k) } ]
        primary_custom = item_data[GID_PRIMARY_CONTACT].nil? ? DirtyHashy.new : DirtyHashy[ item_data[GID_PRIMARY_CONTACT].select{ |k,v| !CORE_FIELD_IDS.include?(k.to_i) }.map{ |k,v| [k.to_i, v] } ]
        secondary_contact = item_data[GID_SECONDARY_CONTACT].nil? ? nil : Hash[ item_data[GID_SECONDARY_CONTACT].map{ |k,v| [k.to_i, v] }.select{ |k,v| CORE_FIELD_IDS.include?(k) } ]
        secondary_custom = item_data[GID_SECONDARY_CONTACT].nil? ? DirtyHashy.new : DirtyHashy[ item_data[GID_SECONDARY_CONTACT].select{ |k,v| !CORE_FIELD_IDS.include?(k.to_i) }.map{ |k,v| [k.to_i, v] } ]
        emergency_contact = item_data[GID_EMERGENCY_CONTACT].nil? ? nil : Hash[ item_data[GID_EMERGENCY_CONTACT].map{ |k,v| [k.to_i, v] }.select{ |k,v| CORE_FIELD_IDS.include?(k) } ]
        emergency_custom = item_data[GID_EMERGENCY_CONTACT].nil? ? DirtyHashy.new : DirtyHashy[ item_data[GID_EMERGENCY_CONTACT].select{ |k,v| !CORE_FIELD_IDS.include?(k.to_i) }.map{ |k,v| [k.to_i, v] } ]
        doctor_contact = item_data[GID_DOCTOR_CONTACT].nil? ? nil : Hash[ item_data[GID_DOCTOR_CONTACT].map{ |k,v| [k.to_i, v] }.select{ |k,v| CORE_FIELD_IDS.include?(k) } ]
        doctor_custom = item_data[GID_DOCTOR_CONTACT].nil? ? DirtyHashy.new : DirtyHashy[ item_data[GID_DOCTOR_CONTACT].select{ |k,v| !CORE_FIELD_IDS.include?(k.to_i) }.map{ |k,v| [k.to_i, v] } ]
        floating_data = item_data[GID_FLOATING].nil? ? {} : Hash[ item_data[GID_FLOATING].map{ |k,v| [k.to_i, v] }.select{ |k,v| CORE_FIELD_IDS.include?(k) } ]
        custom_data = item_data[GID_CUSTOM].nil? ? DirtyHashy.new : DirtyHashy[ item_data[GID_CUSTOM].map{ |k,v| [k.to_i, v] } ]

        result.push Osm::Member.new(
          :id => Osm::to_i_or_nil(item['member_id']),
          :section_id => Osm::to_i_or_nil(item['section_id']),
          :first_name => item['first_name'],
          :last_name => item['last_name'],
          :grouping_id => Osm::to_i_or_nil(item['patrol_id']),
          :grouping_label => item['patrol'],
          :grouping_leader => item['patrol_role_level'],
          :grouping_leader_label => item['patrol_role_level_label'],
          :age => item['age'],
          :date_of_birth => Osm::parse_date(item['date_of_birth'], :ignore_epoch => true),
          :started_section => Osm::parse_date(item['joined']),
          :finished_section => Osm::parse_date(item['end_date']),
          :joined_movement => Osm::parse_date(item['started']),
          :gender => {'male'=>:male, 'female'=>:female, 'other'=>:other, 'unspecified'=>:unspecified}[(floating_data[CID_GENDER] || '').downcase],
          :contact => member_contact.nil? ? nil : MemberContact.new(
            first_name: item['first_name'],
            last_name: item['last_name'],
            address_1: member_contact[CID_ADDRESS_1],
            address_2: member_contact[CID_ADDRESS_2],
            address_3: member_contact[CID_ADDRESS_3],
            address_4: member_contact[CID_ADDRESS_4],
            postcode: member_contact[CID_POSTCODE],
            phone_1: member_contact[CID_PHONE_1],
            phone_2: member_contact[CID_PHONE_2],
            email_1: member_contact[CID_EMAIL_1],
            email_2: member_contact[CID_EMAIL_2],
            receive_phone_1: member_contact[CID_RECIEVE_PHONE_1].eql?('yes'),
            receive_phone_2: member_contact[CID_RECIEVE_PHONE_2].eql?('yes'),
            receive_email_1: member_contact[CID_RECIEVE_EMAIL_1].eql?('yes'),
            receive_email_2: member_contact[CID_RECIEVE_EMAIL_2].eql?('yes'),
            additional_information: member_custom,
            additional_information_labels: custom_labels[GID_MEMBER_CONTACT],
          ),
          :primary_contact => primary_contact.nil? ? nil : PrimaryContact.new(
            first_name: primary_contact[CID_FIRST_NAME],
            last_name: primary_contact[CID_LAST_NAME],
            address_1: primary_contact[CID_ADDRESS_1],
            address_2: primary_contact[CID_ADDRESS_2],
            address_3: primary_contact[CID_ADDRESS_3],
            address_4: primary_contact[CID_ADDRESS_4],
            postcode: primary_contact[CID_POSTCODE],
            phone_1: primary_contact[CID_PHONE_1],
            phone_2: primary_contact[CID_PHONE_2],
            email_1: primary_contact[CID_EMAIL_1],
            email_2: primary_contact[CID_EMAIL_2],
            receive_phone_1: primary_contact[CID_RECIEVE_PHONE_1].eql?('yes'),
            receive_phone_2: primary_contact[CID_RECIEVE_PHONE_2].eql?('yes'),
            receive_email_1: primary_contact[CID_RECIEVE_EMAIL_1].eql?('yes'),
            receive_email_2: primary_contact[CID_RECIEVE_EMAIL_2].eql?('yes'),
            additional_information: primary_custom,
            additional_information_labels: custom_labels[GID_PRIMARY_CONTACT],
          ),
          :secondary_contact => secondary_contact.nil? ? nil : SecondaryContact.new(
            first_name: secondary_contact[CID_FIRST_NAME],
            last_name: secondary_contact[CID_LAST_NAME],
            address_1: secondary_contact[CID_ADDRESS_1],
            address_2: secondary_contact[CID_ADDRESS_2],
            address_3: secondary_contact[CID_ADDRESS_3],
            address_4: secondary_contact[CID_ADDRESS_4],
            postcode: secondary_contact[CID_POSTCODE],
            phone_1: secondary_contact[CID_PHONE_1],
            phone_2: secondary_contact[CID_PHONE_2],
            email_1: secondary_contact[CID_EMAIL_1],
            email_2: secondary_contact[CID_EMAIL_2],
            receive_phone_1: secondary_contact[CID_RECIEVE_PHONE_1].eql?('yes'),
            receive_phone_2: secondary_contact[CID_RECIEVE_PHONE_2].eql?('yes'),
            receive_email_1: secondary_contact[CID_RECIEVE_EMAIL_1].eql?('yes'),
            receive_email_2: secondary_contact[CID_RECIEVE_EMAIL_2].eql?('yes'),
            additional_information: secondary_custom,
            additional_information_labels: custom_labels[GID_SECONDARY_CONTACT],
          ),
          :emergency_contact => emergency_contact.nil? ? nil : EmergencyContact.new(
            first_name: emergency_contact[CID_FIRST_NAME],
            last_name: emergency_contact[CID_LAST_NAME],
            address_1: emergency_contact[CID_ADDRESS_1],
            address_2: emergency_contact[CID_ADDRESS_2],
            address_3: emergency_contact[CID_ADDRESS_3],
            address_4: emergency_contact[CID_ADDRESS_4],
            postcode: emergency_contact[CID_POSTCODE],
            phone_1: emergency_contact[CID_PHONE_1],
            phone_2: emergency_contact[CID_PHONE_2],
            email_1: emergency_contact[CID_EMAIL_1],
            email_2: emergency_contact[CID_EMAIL_2],
            additional_information: emergency_custom,
            additional_information_labels: custom_labels[GID_EMERGENCY_CONTACT],
          ),
          :doctor => doctor_contact.nil? ? nil : DoctorContact.new(
            first_name: doctor_contact[CID_FIRST_NAME],
            last_name: doctor_contact[CID_LAST_NAME],
            surgery: doctor_contact[CID_SURGERY],
            address_1: doctor_contact[CID_ADDRESS_1],
            address_2: doctor_contact[CID_ADDRESS_2],
            address_3: doctor_contact[CID_ADDRESS_3],
            address_4: doctor_contact[CID_ADDRESS_4],
            postcode: doctor_contact[CID_POSTCODE],
            phone_1: doctor_contact[CID_PHONE_1],
            phone_2: doctor_contact[CID_PHONE_2],
            additional_information: doctor_custom,
            additional_information_labels: custom_labels[GID_DOCTOR_CONTACT],
          ),
          additional_information: custom_data,
          additional_information_labels: custom_labels[GID_CUSTOM],
        )
      end

      cache_write(api, cache_key, result)
      return result
    end


    # @!method initialize
    #   Initialize a new Member
    #   @param [Hash] attributes The hash of attributes (see attributes for descriptions, use Symbol of attribute name as the key)


    # Create the user in OSM
    # @param [Osm::Api] api The api to use to make the request
    # @return [Boolan, nil] whether the member was successfully added or not (nil is returned if the user was created but not with all the data)
    # @raise [Osm::ObjectIsInvalid] If the Member is invalid
    # @raise [Osm::Error] If the member already exists in OSM
    def create(api)
      raise Osm::Error, 'the member already exists in OSM' unless id.nil?
      raise Osm::ObjectIsInvalid, 'member is invalid' unless valid?
      require_ability_to(api, :write, :member, section_id)

      data = api.perform_query("users.php?action=newMember", {
        'firstname' => first_name,
        'lastname' => last_name,
        'dob' => date_of_birth.strftime(Osm::OSM_DATE_FORMAT),
        'started' => joined_movement.strftime(Osm::OSM_DATE_FORMAT),
        'startedsection' => started_section.strftime(Osm::OSM_DATE_FORMAT),
        'sectionid' => section_id,
      })

      if (data.is_a?(Hash) && (data['result'] == 'ok') && (data['scoutid'].to_i > 0))
        self.id = data['scoutid'].to_i
        # The cached members for the section will be out of date - remove them
        Osm::Term.get_for_section(api, section_id).each do |term|
          cache_delete(api, ['members', section_id, term.id])
        end
        # Now it's created we need to give OSM the rest of the data
        updated = update(api, true)
        return updated ? true : nil
      else
        return false
      end
    end

    # Update the member in OSM
    # @param [Osm::Api] api The api to use to make the request
    # @param [Boolean] force Whether to force updates (ie tell OSM every attribute changed even if we don't think it did)
    # @return [Boolean] whether the member was successfully updated or not
    # @raise [Osm::ObjectIsInvalid] If the Member is invalid
    def update(api, force=false)
      raise Osm::ObjectIsInvalid, 'member is invalid' unless valid?
      require_ability_to(api, :write, :member, section_id)

      updated = true

      # Do core attributes
      attribute_map = [
        ['first_name', 'firstname', first_name],
        ['last_name', 'lastname', last_name],
        ['grouping_id', 'patrolid', grouping_id],
        ['grouping_leader', 'patrolleader', grouping_leader],
        ['date_of_birth', 'dob', date_of_birth.strftime(Osm::OSM_DATE_FORMAT)],
        ['started_section', 'startedsection', started_section.strftime(Osm::OSM_DATE_FORMAT)],
        ['joined_movement', 'started', joined_movement.strftime(Osm::OSM_DATE_FORMAT)],
      ] # our name => OSM name
      attribute_map.select{ |attr,col,val| force || changed_attributes.include?(attr) }.each do |attr,col,val|
        data = api.perform_query("ext/members/contact/?action=update", {
          'scoutid' => self.id,
          'column' => col,
          'value' => val,
          'sectionid' => section_id,
        })
        updated = updated && data.is_a?(Hash) && data['ok'].eql?(true)
      end # each attr to update

      # Do 'floating' attributes
      if force || changed_attributes.include?('gender')
        new_value = {male: 'Male', female: 'Female', other: 'Other'}[gender] || 'Unspecified'
        data = api.perform_query("ext/customdata/?action=updateColumn&section_id=#{section_id}", {
          'associated_id' => self.id,
          'associated_type' => 'member',
          'value' => new_value,
          'column_id' => CID_GENDER,
          'group_id' => GID_FLOATING,
          'context' => 'members',
        })
        updated = updated && data.is_a?(Hash) && data['data'].is_a?(Hash) && data['data']['value'].eql?(new_value)
      end

      # Do custom attributes
      additional_information.keys.select{ |a| force || additional_information.changes.keys.include?(a) }.each do |attr|
        new_value = additional_information[attr]
        data = api.perform_query("ext/customdata/?action=updateColumn&section_id=#{section_id}", {
          'associated_id' => self.id,
          'associated_type' => 'member',
          'value' => new_value,
          'column_id' => attr,
          'group_id' => GID_CUSTOM,
          'context' => 'members',
        })
        updated = updated && data.is_a?(Hash) && data['data'].is_a?(Hash) && data['data']['value'].to_s.eql?(new_value.to_s)
      end # each attr to update

      # Do contacts
      updated = (contact.nil? || contact.update(api, self, force)) && updated
      updated = (primary_contact.nil? || primary_contact.update(api, self, force)) && updated
      updated = (secondary_contact.nil? || secondary_contact.update(api, self, force)) && updated
      updated = (emergency_contact.nil? ||emergency_contact.update(api, self, force)) && updated
      updated = (doctor.nil? || doctor.update(api, self, force)) && updated

      # Finish off
      if updated
        reset_changed_attributes
        additional_information.clean_up!
        # The cached columns for the members will be out of date - remove them
        Osm::Term.get_for_section(api, section_id).each do |term|
          Osm::Model.cache_delete(api, ['members', section_id, term.id])
        end
      end
      return updated
    end

    # Get the years element of this scout's age
    # @return [Fixnum] the number of years this scout has been alive
    def age_years
      return age[0..1].to_i
    end

    # Get the months element of this scout's age
    # @return [Fixnum] the number of months since this scout's last birthday
    def age_months
      return age[-2..-1].to_i
    end

    # Get the full name
    # @param [String] seperator What to split the member's first name and last name with
    # @return [String] this scout's full name seperated by the optional seperator
    def name(seperator=' ')
      return [first_name, last_name].select{ |i| !i.blank? }.join(seperator)
    end

    # Check if the member is in the leaders grouping
    # @return [Boolean]
    def leader?
      grouping_id.eql?(-2)
    end

    # Check if the member is in a non-leaders grouping
    # @return [Boolean]
    def youth?
      grouping_id > 0
    end

    # Check if the member is male
    # @return [Boolean]
    def male?
      gender == :male
    end

    # Check if the member is male
    # @return [Boolean]
    def female?
      gender == :female
    end

    # Check if this is a current member of the section they were retrieved for
    # @param date [Date] The date to check membership status for
    # @return true, false
    def current?(date=Date.today)
      return nil if started_section.nil? and finished_section.nil?
      if finished_section.nil?
        started_section <= date
      elsif started_section.nil?
        finished_section >= date
      else
        (started_section <= date) && (finished_section >= date)
      end
    end

    # @!method all_emails
    # Get an array of all email addresses from all contacts for the member (not emergency or doctor)
    # @return [Array<String>]
    # @!method all_emails_with_name
    # Get an array of all email addresses from all contacts for the member in a format which includes the contact's name (not emergency or doctor)
    # @return [Array<String>]
    # @!method enabled_emails
    # Get an array of all email addresses from all contacts for the member (not emergency or doctor)
    # @return [Array<String>]
    # @!method enabled_emails_with_name
    # Get an array of all email addresses from all contacts for the member in a format which includes the contact's name (not emergency or doctor)
    # @return [Array<String>]
    # @!method all_phones
    # Get an array of all phone numbers from all contacts for the member (not emergency or doctor)
    # @return [Array<String>]
    # @!method enabled_phones
    # Get an array of enabled phone numbers from all contacts for the member (not emergency or doctor)
    # @return [Array<String>]
    [:all_emails, :all_emails_with_name, :enabled_emails, :enabled_emails_with_name, :all_phones, :enabled_phones].each do |meth|
      define_method meth do
        items = []
        [:contact, :primary_contact, :secondary_contact].each do |cont|
          items.push *send(cont).send(meth)
        end
        return items
      end
    end

    # Get the Key to use in My.SCOUT links for this member
    # @param [Osm::Api] api The api to use to make the request
    # @return [String] the key
    # @raise [Osm::ObjectIsInvalid] If the Member is invalid
    # @raise [Osm::Error] if the member does not already exist in OSM or the member's My.SCOUT key could not be retrieved from OSM
    def myscout_link_key(api)
      raise Osm::ObjectIsInvalid, 'member is invalid' unless valid?
      require_ability_to(api, :read, :member, section_id)
      raise Osm::Error, 'the member does not already exist in OSM' if id.nil?

      if @myscout_link_key.nil?
        data = api.perform_query("api.php?action=getMyScoutKey&sectionid=#{section_id}&scoutid=#{self.id}")
        raise Osm::Error, 'Could not retrieve the key for the link from OSM' unless data['ok']
        @myscout_link_key = data['key']
      end

      return @myscout_link_key
    end

    # Get the member's photo
    # @param [Osm::Api] api The api to use to make the request
    # @param [Boolean] black_and_white Whether you want the photo in blank and white (defaults to false unless the member is not active)
    # @!macro options_get
    # @raise [Osm:Error] if the member doesn't exist in OSM
    # @return the photo of the member
    def get_photo(api, black_and_white=!current?, options={})
      raise Osm::ObjectIsInvalid, 'member is invalid' unless valid?
      require_ability_to(api, :read, :member, section_id)
      raise Osm::Error, 'the member does not already exist in OSM' if id.nil?

      cache_key = ['member_photo', self.id, black_and_white]

      if !options[:no_cache] && cache_exist?(api, cache_key)
        return cache_read(api, cache_key)
      end

      url = "ext/members/contact/images/member.php?sectionid=#{section_id}&scoutid=#{self.id}&bw=#{black_and_white}"
      image = api.perform_query(url)

      cache_write(api, cache_key, image) unless image.nil?
      return image
    end

    # Get the My.SCOUT link for this member
    # @param [Osm::Api] api The api to use to make the request
    # @param [Symbol] link_to The page in My.SCOUT to link to (:payments, :events, :programme, :badges, :notice, :details, :census or :giftaid)
    # @param [#to_i] item_id Allows you to link to a specfic item (only for :events)
    # @return [String] the URL for this member's My.SCOUT
    # @raise [Osm::ObjectIsInvalid] If the Member is invalid
    # @raise [Osm::ArgumentIsInvalid] If link_to is not an allowed Symbol
    # @raise [Osm::Error] if the member does not already exist in OSM or the member's My.SCOUT key could not be retrieved from OSM
    def myscout_link(api, link_to=:badges, item_id=nil)
      raise Osm::ObjectIsInvalid, 'member is invalid' unless valid?
      require_ability_to(api, :read, :member, section_id)
      raise Osm::Error, 'the member does not already exist in OSM' if id.nil?
      raise Osm::ArgumentIsInvalid, 'link_to is invalid' unless [:payments, :events, :programme, :badges, :notice, :details, :census, :giftaid].include?(link_to)

      link = "#{api.base_url}/parents/#{link_to}.php?sc=#{self.id}&se=#{section_id}&c=#{myscout_link_key(api)}"
      link += "&e=#{item_id.to_i}" if item_id && link_to.eql?(:events)
      return link
    end

    # Compare member based on section_id, grouping_id, grouping_leader (descending), last_name then first_name
    def <=>(another)
      result = self.section_id <=> another.try(:section_id)
      result = self.grouping_id <=> another.try(:grouping_id) if result == 0
      result = -(self.grouping_leader <=> another.try(:grouping_leader)) if result == 0
      result = self.last_name <=> another.try(:last_name) if result == 0
      result = self.first_name <=> another.try(:first_name) if result == 0
      return result
    end


    module EmailableContact
      # Get an array of all emails for the contact
      # @return [Array<String>]
      def all_emails
        [email_1, email_2].select{ |e| !e.blank? }
      end

      # Get an array of all emails for the contact in a format which includes their name
      # @return [Array<String>]
      def all_emails_with_name
        [email_1, email_2].select{ |e| !e.blank? }.map{ |e| "\"#{name}\" <#{e}>" }
      end

    end

    module EnableableEmailableContact
      include EmailableContact

      # Get an array of enabled emails for the contact
      # @return [Array<String>]
      def enabled_emails
        emails = []
        emails.push email_1 if receive_email_1
        emails.push email_2 if receive_email_2
        emails.select{ |e| !e.blank? }
      end

      # Get an array of enabled emails for the contact in a format which includes their name
      # @return [Array<String>]
      def enabled_emails_with_name
        emails = []
        emails.push email_1 if receive_email_1
        emails.push email_2 if receive_email_2
        emails.select{ |e| !e.blank? }.map{ |e| "\"#{name}\" <#{e}>" }
      end
    end

    module EnableablePhoneableContact
      # Get an array of enabled phone numbers for the contact
      def enabled_phones
        phones = []
        phones.push phone_1.gsub(/[^\d\+]/, '') if receive_phone_1
        phones.push phone_2.gsub(/[^\d\+]/, '') if receive_phone_2
        phones.select{ |n| !n.blank? }.map{ |n| n }
      end
    end


    class Contact < Osm::Model
      # @!attribute [rw] first_name
      #   @return [String] the contact's first name
      # @!attribute [rw] last_name
      #   @return [String] the contact's last name
      # @!attribute [rw] address_1
      #   @return [String] the 1st line of the address
      # @!attribute [rw] address_2
      #   @return [String] the 2nd line of the address
      # @!attribute [rw] address_3
      #   @return [String] the 3rd line of the address
      # @!attribute [rw] address_4
      #   @return [String] the 4th line of the address
      # @!attribute [rw] postcode
      #   @return [String] the postcode of the address
      # @!attribute [rw] phone_1
      #   @return [String] the primary phone number
      # @!attribute [rw] phone_2
      #   @return [String] the secondary phone number
      # @!attribute [rw] additional_information
      #   @return [DirtyHashy] the additional information (key is OSM's variable name, value is the data)
      # @!attribute [rw] additional_information_labels
      #   @return [DirtyHashy] the labels for the additional information (key is OSM's variable name, value is the label)

      attribute :first_name, :type => String
      attribute :last_name, :type => String
      attribute :address_1, :type => String
      attribute :address_2, :type => String
      attribute :address_3, :type => String
      attribute :address_4, :type => String
      attribute :postcode, :type => String
      attribute :phone_1, :type => String
      attribute :phone_2, :type => String
      attribute :additional_information, :type => Object, :default => DirtyHashy.new
      attribute :additional_information_labels, :type => Object, :default => DirtyHashy.new

      if ActiveModel::VERSION::MAJOR < 4
        attr_accessible :first_name, :last_name,
                        :address_1, :address_2, :address_3, :address_4,
                        :postcode, :phone_1, :phone_2,
                        :additional_information, :additional_information_labels
      end

      # @!method initialize
      #   Initialize a new Contact
      #   @param [Hash] attributes The hash of attributes (see attributes for descriptions, use Symbol of attribute name as the key)

      # Get the full name
      # @param [String] seperator What to split the contact's first name and last name with
      # @return [String] this scout's full name seperated by the optional seperator
      def name(seperator=' ')
        return [first_name, last_name].select{ |i| !i.blank? }.join(seperator)
      end

      # Get an array of all phone numbers for the contact
      # @return [Array<String>]
      def all_phones
        [phone_1, phone_2].select{ |n| !n.blank? }.map{ |n| n.gsub(/[^\d\+]/, '') }
      end

      # Update the contact in OSM
      # @param [Osm::Api] api The api to use to make the request
      # @param [Osm::Member] section The member to update the contact for
      # @param [Boolean] force Whether to force updates (ie tell OSM every attribute changed even if we don't think it did)
      # @return [Boolean] whether the member was successfully updated or not
      # @raise [Osm::ObjectIsInvalid] If the Contact is invalid
      def update(api, member, force=false)
        raise Osm::ObjectIsInvalid, 'member is invalid' unless valid?
        require_ability_to(api, :write, :member, member.section_id)

        attribute_map = {
          'first_name' => 'data[firstname]',
          'last_name' => 'data[lastname]',
          'surgery' => 'data[surgery]',
          'address_1' => 'data[address1]',
          'address_2' => 'data[address2]',
          'address_3' => 'data[address3]',
          'address_4' => 'data[address4]',
          'postcode' => 'data[postcode]',
          'phone_1' => 'data[phone1]',
          'receive_phone_1' => 'data[phone1_sms]',
          'phone_2' => 'data[phone2]',
          'receive_phone_2' => 'data[phone2_sms]',
          'email_1' => 'data[email1]',
          'receive_email_1' => 'data[email1_leaders]',
          'email_2' => 'data[email2]',
          'receive_email_2' => 'data[email2_leaders]',
        } # our name => OSM name

        data = {}
        attributes.keys.select{ |a| !['additional_information', 'additional_information_labels'].include?(a) }.select{ |a| force || changed_attributes.include?(a) }.each do |attr|
          value = send(attr)
          value = 'yes' if value.eql?(true)
          data[attribute_map[attr]] = value
        end
        additional_information.keys.select{ |a| force || additional_information.changes.keys.include?(a) }.each do |attr|
          data["data[#{attr}]"] = additional_information[attr]
        end

        updated = true
        unless data.empty?
          result = api.perform_query("ext/customdata/?action=update&section_id=#{member.section_id}", {
            'associated_id' => member.id,
            'associated_type' => 'member',
            'context' => 'members',
            'group_id' => self.class::GROUP_ID,
          }.merge(data))
          updated = result.is_a?(Hash) && result['status'].eql?(true)
        end

        # Finish off
        if updated
          reset_changed_attributes
          additional_information.clean_up!
        end
        return updated
      end

    end


    class MemberContact < Osm::Member::Contact
      include EnableableEmailableContact
      include EnableablePhoneableContact

      GROUP_ID = Osm::Member::GID_MEMBER_CONTACT

      # @!attribute [rw] email_1
      #   @return [String] the primary email address for the member
      # @!attribute [rw] email_2
      #   @return [String] the secondary email address for the member
      # @!attribute [rw] receive_email_1
      #   @return [Boolean] whether the member should receive emails from leaders on their primary email address
      # @!attribute [rw] receive_email_2
      #   @return [Boolean] whether the member should receive emails from leaders on their secondary email address
      # @!attribute [rw] receive_phone_1
      #   @return [Boolean] whether the member should receive SMSs from leaders on their primary phone number
      # @!attribute [rw] receive_phone_2
      #   @return [Boolean] whether the member should receive SMSs from leaders on their secondary phone number

      attribute :email_1, :type => String
      attribute :receive_email_1, :type => Boolean, :default => false
      attribute :email_2, :type => String
      attribute :receive_email_2, :type => Boolean, :default => false
      attribute :receive_phone_1, :type => Boolean, :default => false
      attribute :receive_phone_2, :type => Boolean, :default => false

      if ActiveModel::VERSION::MAJOR < 4
        attr_accessible :email_1, :email_2, :receive_email_1, :receive_email_2,
                        :receive_phone_1, :receive_phone_2
      end

      validates_inclusion_of :receive_email_1, :in => [true, false]
      validates_inclusion_of :receive_email_2, :in => [true, false]
      validates_inclusion_of :receive_phone_1, :in => [true, false]
      validates_inclusion_of :receive_phone_2, :in => [true, false]
    end


    class PrimaryContact < Osm::Member::Contact
      include EnableableEmailableContact
      include EnableablePhoneableContact

      GROUP_ID = Osm::Member::GID_PRIMARY_CONTACT

      # @!attribute [rw] email_1
      #   @return [String] the primary email address for the contact
      # @!attribute [rw] email_2
      #   @return [String] the secondary email address for the contact
      # @!attribute [rw] receive_email_1
      #   @return [Boolean] whether the contact should receive emails from leaders on their primary email address
      # @!attribute [rw] receive_email_2
      #   @return [Boolean] whether the contact should receive emails from leaders on their secondary email address
      # @!attribute [rw] receive_phone_1
      #   @return [Boolean] whether the contact should receive SMSs from leaders on their primary phone number
      # @!attribute [rw] receive_phone_2
      #   @return [Boolean] whether the contact should receive SMSs from leaders on their secondary phone number

      attribute :email_1, :type => String
      attribute :receive_email_1, :type => Boolean, :default => false
      attribute :email_2, :type => String
      attribute :receive_email_2, :type => Boolean, :default => false
      attribute :receive_phone_1, :type => Boolean, :default => false
      attribute :receive_phone_2, :type => Boolean, :default => false

      if ActiveModel::VERSION::MAJOR < 4
        attr_accessible :email_1, :email_2,
                        :receive_email_1, :receive_email_2, :receive_phone_1, :receive_phone_2
      end

      validates_inclusion_of :receive_email_1, :in => [true, false]
      validates_inclusion_of :receive_email_2, :in => [true, false]
      validates_inclusion_of :receive_phone_1, :in => [true, false]
      validates_inclusion_of :receive_phone_2, :in => [true, false]
    end # class PrimaryContact

    class SecondaryContact < Osm::Member::PrimaryContact
      GROUP_ID = Osm::Member::GID_SECONDARY_CONTACT
    end # class SecondaryContact

    class EmergencyContact < Osm::Member::Contact
      include EmailableContact

      GROUP_ID = Osm::Member::GID_EMERGENCY_CONTACT

      # @!attribute [rw] email_1
      #   @return [String] the primary email address for the contact
      # @!attribute [rw] email_2
      #   @return [String] the secondary email address for the contact

      attribute :email_1, :type => String
      attribute :email_2, :type => String

      if ActiveModel::VERSION::MAJOR < 4
        attr_accessible :email_1, :email_2
      end

    end # class EmergencyContact


    class DoctorContact < Osm::Member::Contact
      GROUP_ID = Osm::Member::GID_DOCTOR_CONTACT

      # @!attribute [rw] surgery
      #   @return [String] the surgery name

      attribute :surgery, :type => String

      if ActiveModel::VERSION::MAJOR < 4
        attr_accessible :surgery
      end

    end # class DoctorContact

  end # Class Member

end # Module