openaustralia/publicwhip

View on GitHub
app/models/people_distance.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

class PeopleDistance < ApplicationRecord
  belongs_to :person1, class_name: "Person"
  belongs_to :person2, class_name: "Person"

  def agreement_fraction_without_absences
    1 - distance_b
  end

  def total_votes
    nvotessame + nvotesdiffer
  end

  def overlap_dates
    result = []
    person1.members.reorder(:entered_house).each do |member1|
      person2.members.reorder(:entered_house).each do |member2|
        r = PeopleDistance.overlap_dates_members(member1, member2)
        next if r.nil?

        if !result.empty? && r.begin == result.last.end
          # Merge new range with previous range because they're consecutive
          p = result.pop
          r = p.begin...r.end
        end
        result << r
      end
    end
    result
  end

  # Returns nil if there is no overlap between members
  def self.overlap_dates_members(member1, member2)
    start_date = [member1.entered_house, member2.entered_house].max
    end_date = [member1.left_house, member2.left_house].min

    start_date...end_date if start_date < end_date && member1.house == member2.house
  end

  def self.update_person(person1)
    # We're only populating half of the matrix
    person1.overlapping_people.select { |p| p.id >= person1.id }.each do |person2|
      params = calculate_distances(person1, person2)
      # TODO: If distance_b is -1 then we don't even want a PeopleDistance record. This would
      # allow us to remove further checks for distance_b != -1
      # Matrix is symmetric so we don't have to calculate twice
      PeopleDistance.find_or_initialize_by(person1: person1, person2: person2).update!(params)
      PeopleDistance.find_or_initialize_by(person1: person2, person2: person1).update!(params)
    end
  end

  # By setting "date" we can also restrict the calculation to just a particular date or
  # a range of dates
  def self.calculate_distances(person1, person2, date = nil)
    r = Division
        .joins("INNER JOIN votes AS votes1 on votes1.division_id = divisions.id")
        .joins("INNER JOIN votes AS votes2 on votes2.division_id = divisions.id")
        .joins("INNER JOIN members AS members1 on members1.id = votes1.member_id")
        .joins("INNER JOIN members AS members2 on members2.id = votes2.member_id")
        .where(members1: { person_id: person1.id })
        .where(members2: { person_id: person2.id })
    r = r.where(divisions: { date: date }) if date
    r = r.group("votes1.vote = votes2.vote").count
    same = r[1] || 0
    differ = r[0] || 0
    {
      nvotessame: same,
      nvotesdiffer: differ,
      distance_b: Distance.new(same: same, differ: differ).distance
    }
  end

  # Divisions where the two people voted differently
  def divisions_different
    Division
      .joins("INNER JOIN votes AS votes1 on votes1.division_id = divisions.id")
      .joins("INNER JOIN votes AS votes2 on votes2.division_id = divisions.id")
      .joins("INNER JOIN members AS members1 on members1.id = votes1.member_id")
      .joins("INNER JOIN members AS members2 on members2.id = votes2.member_id")
      .where(members1: { person_id: person1_id })
      .where(members2: { person_id: person2_id })
      .where("votes1.vote != votes2.vote")
  end

  # Divisions where the two people voted the same
  def divisions_same
    Division
      .joins("INNER JOIN votes AS votes1 on votes1.division_id = divisions.id")
      .joins("INNER JOIN votes AS votes2 on votes2.division_id = divisions.id")
      .joins("INNER JOIN members AS members1 on members1.id = votes1.member_id")
      .joins("INNER JOIN members AS members2 on members2.id = votes2.member_id")
      .where(members1: { person_id: person1_id })
      .where(members2: { person_id: person2_id })
      .where("votes1.vote = votes2.vote")
  end
end