openaustralia/planningalerts

View on GitHub
app/models/authority.rb

Summary

Maintainability
A
0 mins
Test Coverage
B
80%
# typed: strict
# frozen_string_literal: true

class Authority < ApplicationRecord
  extend T::Sig

  has_many :applications, dependent: :restrict_with_exception
  has_many :comments, through: :applications
  has_one :github_issue, dependent: :destroy
  has_paper_trail ignore: %i[last_scraper_run_log boundary]

  validate :short_name_encoded_is_unique

  validates :state, inclusion: { in: %w[NSW VIC QLD SA WA TAS NT ACT] }

  scope(:enabled, -> { where(disabled: false) })
  scope(:active, -> { where(disabled: false).where("morph_name != '' AND morph_name IS NOT NULL") })

  sig { void }
  def short_name_encoded_is_unique
    other = Authority.find_short_name_encoded(short_name_encoded)
    return if other.nil? || other.id == id

    errors.add(:short_name, "is not unique when encoded")
  end

  sig { returns(String) }
  def full_name_and_state
    "#{full_name}, #{state}"
  end

  sig { returns(T::Boolean) }
  def covered?
    morph_name.present?
  end

  # Total population of Australia based on the 2021 Census
  # https://www.abs.gov.au/statistics/people/population/population-census/2021
  sig { returns(Integer) }
  def self.total_population_2021
    25422788
  end

  sig { returns(Numeric) }
  def self.total_population_2021_covered_by_all_active_authorities
    Authority.active.sum(:population_2021)
  end

  sig { returns(Float) }
  def self.percentage_population_covered_by_all_active_authorities
    (total_population_2021_covered_by_all_active_authorities.to_f / total_population_2021) * 100
  end

  sig { returns(T.nilable(Integer)) }
  def median_new_applications_per_week
    v = new_applications_per_week.select { |a| a[1].positive? }.pluck(1).sort
    v[v.count / 2]
  end

  sig { returns(T::Array[[Date, Integer]]) }
  def new_applications_per_week
    # Sunday is the beginning of the week (and the date returned here)
    h = applications.group("DATE(first_date_scraped) - CAST(EXTRACT(DOW FROM first_date_scraped) AS INT)").count
    min = h.keys.min
    max = Time.zone.today - Time.zone.today.wday
    (min..max).step(7) do |date|
      h[date] = 0 unless h.key?(date)
    end
    h.sort
  end

  # TODO: More strict type checking on contents of array
  sig { returns(T::Array[[Date, Integer]]) }
  def comments_per_week
    # Sunday is the beginning of the week to match new_applications_per_week
    Date.beginning_of_week = :sunday

    results = []

    e = earliest_date
    if e
      results = comments.visible.group("DATE(published_at) - CAST(EXTRACT(DOW FROM published_at) AS INT)").count

      earliest_week_with_applications = e.at_beginning_of_week.to_date
      latest_week = Time.zone.today.at_beginning_of_week

      (earliest_week_with_applications..latest_week).step(7) do |date|
        results[date] = 0 unless results.key?(date)
      end
    end

    results.sort
  end

  # When this authority started on PlanningAlerts. Just the date of the earliest scraped application
  sig { returns(T.nilable(Time)) }
  def earliest_date
    applications.minimum(:first_date_scraped)
  end

  # So that the encoding function can be used elsewhere
  sig { params(short_name: String).returns(String) }
  def self.short_name_encoded(short_name)
    short_name.downcase.tr(" ", "_").gsub(/\W/, "")
  end

  sig { returns(String) }
  def short_name_encoded
    Authority.short_name_encoded(short_name)
  end

  sig { params(name: String).returns(T.nilable(Integer)) }
  def self.find_id_short_name_encoded(name)
    select(:id, :short_name).to_a.find { |a| a.short_name_encoded == name }&.id
  end

  # TODO: Replace this with using friendly_id gem
  # TODO: Also loads the whole boundary into memory. Do we want this?
  sig { params(name: String).returns(T.nilable(Authority)) }
  def self.find_short_name_encoded(name)
    id = find_id_short_name_encoded(name)
    find(id) if id
  end

  sig { params(name: String).returns(Authority) }
  def self.find_short_name_encoded!(name)
    r = find_short_name_encoded(name)
    # In production environment raising RecordNotFound will produce an error code 404
    raise ActiveRecord::RecordNotFound if r.nil?

    r
  end

  # When the last entirely new application was scraped. Applications being updated is ignored.
  sig { returns(T.nilable(Time)) }
  def date_last_new_application_scraped
    applications.maximum(:first_date_scraped)
  end

  # If the latest application is over two weeks old, the scraper's probably broken
  sig { returns(T::Boolean) }
  def broken?
    applications.recent.empty?
  end

  sig { returns(ActiveRecord::Relation) }
  def alerts
    # Doing this as a sub-query so we don't send a long boundary string to the database
    Alert.active.where("ST_Covers((?), lonlat)", Authority.where(id:).limit(1).select(:boundary))
  end
end