mysociety/alaveteli

View on GitHub
app/models/info_request.rb

Summary

Maintainability
F
1 wk
Test Coverage
# == Schema Information
# Schema version: 20220928093559
#
# Table name: info_requests
#
#  id                                    :integer          not null, primary key
#  title                                 :text             not null
#  user_id                               :integer
#  public_body_id                        :integer          not null
#  created_at                            :datetime         not null
#  updated_at                            :datetime         not null
#  described_state                       :string           not null
#  awaiting_description                  :boolean          default(FALSE), not null
#  prominence                            :string           default("normal"), not null
#  url_title                             :text             not null
#  law_used                              :string           default("foi"), not null
#  allow_new_responses_from              :string           default("anybody"), not null
#  handle_rejected_responses             :string           default("bounce"), not null
#  idhash                                :string           not null
#  external_user_name                    :string
#  external_url                          :string
#  attention_requested                   :boolean          default(FALSE)
#  comments_allowed                      :boolean          default(TRUE), not null
#  info_request_batch_id                 :integer
#  last_public_response_at               :datetime
#  reject_incoming_at_mta                :boolean          default(FALSE), not null
#  rejected_incoming_count               :integer          default(0)
#  date_initial_request_last_sent_at     :date
#  date_response_required_by             :date
#  date_very_overdue_after               :date
#  last_event_forming_initial_request_id :integer
#  use_notifications                     :boolean
#  last_event_time                       :datetime
#  incoming_messages_count               :integer          default(0)
#  public_token                          :string
#  prominence_reason                     :text
#

require 'digest/sha1'
require 'fileutils'

class InfoRequest < ApplicationRecord
  OLD_AGE_IN_DAYS = 21.days

  include Rails.application.routes.url_helpers
  include AlaveteliPro::RequestSummaries
  include AlaveteliFeatures::Helpers
  include InfoRequest::BatchPagination
  include InfoRequest::PublicToken
  include InfoRequest::Sluggable
  include InfoRequest::TitleValidation
  include Categorisable
  include Taggable
  include Notable
  include LinkToHelper

  admin_columns exclude: %i[title url_title],
                include: %i[rejected_incoming_count]

  def self.admin_title
    'Request'
  end

  strip_attributes allow_empty: true
  strip_attributes only: [:title],
                   replace_newlines: true, collapse_spaces: true

  belongs_to :user,
             inverse_of: :info_requests,
             counter_cache: true

  validate :must_be_internal_or_external

  belongs_to :public_body,
             inverse_of: :info_requests,
             counter_cache: true
  belongs_to :info_request_batch,
             inverse_of: :info_requests

  validates_presence_of :public_body, message: N_("Please select an authority")

  has_many :info_request_events,
           -> { order(:created_at, :id) },
           inverse_of: :info_request,
           dependent: :destroy
  has_many :outgoing_messages,
           -> { order(:created_at) },
           inverse_of: :info_request,
           dependent: :destroy
  has_many :incoming_messages,
           -> { order(:created_at) },
           inverse_of: :info_request,
           dependent: :destroy
  has_many :user_info_request_sent_alerts,
           inverse_of: :info_request,
           dependent: :destroy
  has_many :track_things,
           -> { order(created_at: :desc) },
           inverse_of: :info_request,
           dependent: :destroy
  has_many :widget_votes,
           inverse_of: :info_request,
           dependent: :destroy
  has_many :citations,
           -> (info_request) { unscope(:where).for_request(info_request) },
           as: :citable,
           inverse_of: :citable,
           dependent: :destroy
  has_many :comments,
           -> { order(:created_at) },
           inverse_of: :info_request,
           dependent: :destroy
  has_many :censor_rules,
           -> { order(created_at: :desc) },
           inverse_of: :info_request,
           dependent: :destroy
  has_many :mail_server_logs,
           -> { order(:mail_server_log_done_id, :order) },
           inverse_of: :info_request,
           dependent: :destroy
  has_one :embargo,
          inverse_of: :info_request,
          class_name: 'AlaveteliPro::Embargo',
          dependent: :destroy

  has_many :foi_attachments, through: :incoming_messages

  has_many :project_submissions, class_name: 'Project::Submission'
  has_many :classification_project_submissions,
           -> { classification },
           class_name: 'Project::Submission'
  has_many :extraction_project_submissions,
           -> { extraction },
           class_name: 'Project::Submission'

  attr_reader :followup_bad_reason

  scope :internal, -> { where.not(user_id: nil) }
  scope :external, -> { where(user_id: nil) }

  scope :pro, ProQuery.new
  scope :is_public, Prominence::PublicQuery.new
  scope :is_searchable, Prominence::SearchableQuery.new
  scope :embargoed, Prominence::EmbargoedQuery.new
  scope :not_embargoed, Prominence::NotEmbargoedQuery.new
  scope :embargo_expiring, Prominence::EmbargoExpiringQuery.new
  scope :embargo_expired_today, Prominence::EmbargoExpiredTodayQuery.new
  scope :visible_to_requester, Prominence::VisibleToRequesterQuery.new
  scope :been_published, Prominence::BeenPublishedQuery.new

  scope :awaiting_response, State::AwaitingResponseQuery.new
  scope :response_received, State::ResponseReceivedQuery.new
  scope :clarification_needed, State::ClarificationNeededQuery.new
  scope :complete, State::CompleteQuery.new
  scope :other, State::OtherQuery.new
  scope :overdue, State::OverdueQuery.new
  scope :very_overdue, State::VeryOverdueQuery.new

  scope :for_project, Project::InfoRequestQuery.new

  scope :surveyable, Survey::InfoRequestQuery.new

  class << self
    alias in_progress awaiting_response
  end
  scope :action_needed, State::ActionNeededQuery.new
  scope :updated_before, ->(ts) { where('"info_requests"."updated_at" < ?', ts) }

  # user described state (also update in info_request_event, admin_request/edit.rhtml)
  validate :must_be_valid_state
  validates_inclusion_of :prominence, in: Prominence::VALUES

  validates_inclusion_of :law_used, in: Legislation.keys

  # who can send new responses
  validates_inclusion_of :allow_new_responses_from, in: [
    'anybody', # anyone who knows the request email address
    'authority_only', # only people from authority domains
    'nobody'
  ]
  # what to do with refused new responses
  validates_inclusion_of :handle_rejected_responses, in: [
    'bounce', # return them to sender
    'holding_pen', # put them in the holding pen
    'blackhole' # just dump them
  ]

  after_initialize :set_defaults
  before_create :set_use_notifications
  before_validation :compute_idhash
  before_validation :set_law_used, on: :create
  after_create :notify_public_body
  after_save :update_counter_cache
  after_update :reindex_request_events, if: :reindexable_attribute_changed?
  before_destroy :expire
  after_destroy :update_counter_cache

  # Return info request corresponding to an incoming email address, or nil if
  # none found. Checks the hash to ensure the email came from the public body -
  # only they are sent the email address with the has in it. (We don't check
  # the prefix and domain, as sometimes those change, or might be elided by
  # copying an email, and that doesn't matter)
  def self.find_by_incoming_email(incoming_email)
    id, hash = InfoRequest._extract_id_hash_from_email(incoming_email)
    if hash_from_id(id) == hash
      # Not using find(id) because we don't exception raised if nothing found
      find_by_id(id)
    end
  end

  # Public: Find by a list of incoming email addresses.
  # TODO: It would be better to make this return a chainable
  # ActiveRecord::Relation
  #
  # Examples:
  #
  #   InfoRequest.matching_incoming_email('request-1-ae63fb73@localhost')
  #   InfoRequest.matching_incoming_email(@array_of_email_addresses)
  #
  # Returns an Array
  def self.matching_incoming_email(emails)
    Array(emails).map { |email| find_by_incoming_email(email) }.compact
  end

  # Subset of states accepted via the API
  def self.allowed_incoming_states
    %w[
      waiting_response
      rejected
      successful
      partially_successful
    ]
  end

  def self.custom_states_loaded
    @@custom_states_loaded
  end

  # Public: Attempt to find InfoRequests by matching against extracted `id` and
  # `idhash` elements of an `incoming_email`.
  #
  # emails - A String email address or an Array of String email addresses.
  #
  # Returns an Array
  def self.guess_by_incoming_email(*emails)
    guesses = emails.flatten.reduce([]) do |memo, email|
      id, idhash = _extract_id_hash_from_email(email)
      id, idhash = _guess_idhash_from_email(email) if idhash.nil? || id.nil?

      memo << Guess.new(
        find_by_id(id), email: email, id: id, idhash: idhash
      )
      memo << Guess.new(
        find_by_idhash(idhash), email: email, id: id, idhash: idhash
      )
    end

    # Unique Guesses where we've found an `InfoRequest`
    guesses.select(&:info_request).uniq(&:info_request)
  end

  # Internal function used by guess_by_incoming_email
  def self._guess_idhash_from_email(incoming_email)
    incoming_email = incoming_email.downcase
    incoming_email =~ /request\-?(\w+)-?(\w{8})@/

    id = _id_string_to_i(_clean_idhash($1))
    id_hash = $2

    if id_hash.nil? && incoming_email.include?('@')
      # try to grab the last 8 chars of the local part of the address instead
      local_part = incoming_email[0..incoming_email.index('@')-1]
      id_hash =
        (_clean_idhash(local_part[-8..-1]) if local_part.length >= 8)
    end

    [id, id_hash]
  end

  # Internal function - attempts to convert a guessed id String from incoming
  # email addresses to an Integer. Returns nil if it fails to avoid accidentally
  # stripping trailing letters e.g. '123ab' should not match 123
  #
  # Returns an Integer
  def self._id_string_to_i(id_string)
    Integer(id_string) if id_string
  rescue ArgumentError
    nil
  end

  # Internal function used to clean the id_hash from incoming email addresses.
  # Converts l to 1, and o to 0. FOI officers quite often retype the email
  # address and make this kind of error.
  def self._clean_idhash(hash)
    return unless hash

    hash.gsub(/l/, "1").gsub(/o/, "0")
  end

  # Public: Attempt to find InfoRequests by matching against extracted `subject`
  # element of an `incoming_email`.
  #
  # subject_line - A String an email subject line
  # Returns an Array
  def self.guess_by_incoming_subject(subject_line)
    return [] unless subject_line

    # try to find a match on InfoRequest#title
    reply_format = InfoRequest.new(title: '').email_subject_followup
    requests_by_title = InfoRequest.left_joins(:incoming_messages).
      where(title: subject_line.gsub(/#{reply_format}/i, '').strip)

    # try to find a match on IncomingMessage#subject
    requests_by_subject = InfoRequest.left_joins(:incoming_messages).
      where(incoming_messages: {
              subject: [subject_line.gsub(/^Re: /i, ''), subject_line].uniq
            })

    requests = requests_by_title.or(requests_by_subject).
      distinct.
      where.not(url_title: 'holding_pen').
      limit(25)

    guesses = requests.each.reduce([]) do |memo, request|
      memo << Guess.new(request, subject: subject_line)
    end

    # Unique Guesses where we've found an `InfoRequest`
    guesses.select(&:info_request).uniq(&:info_request)
  end

  # Internal function used by find_by_magic_email and guess_by_incoming_email
  def self._extract_id_hash_from_email(incoming_email)
    # Match case insensitively, FOI officers often write Request with capital R.
    incoming_email = incoming_email.downcase

    # The optional bounce- dates from when we used to have separate emails for the envelope from.
    # (that was abandoned because councils would send hand written responses to them, not just
    # bounce messages)
    incoming_email =~ /request-(?:bounce-)?([a-z0-9]+)-([a-z0-9]+)/

    id = _id_string_to_i($1)
    hash = _clean_idhash($2)

    [id, hash]
  end

  # When constructing a new request, use this to check user hasn't double submitted.
  # TODO: could have a date range here, so say only check last month's worth of new requests. If somebody is making
  # repeated requests, say once a quarter for time information, then might need to do that.
  # TODO: this *should* also check outgoing message joined to is an initial
  # request (rather than follow up)
  def self.find_existing(title, public_body_id, body)
    conditions = { title: title&.strip, public_body_id: public_body_id }

    InfoRequest.
      includes(:outgoing_messages).
        where(conditions).
          merge(OutgoingMessage.with_body(body)).
            references(:outgoing_messages).
              first
  end

  # The "holding pen" is a special request which stores incoming emails whose
  # destination request is unknown.
  def self.holding_pen_request
    ir = InfoRequest.find_by_url_title("holding_pen")
    if ir.nil?
      ir = InfoRequest.new(
        user: User.internal_admin_user,
        public_body: PublicBody.internal_admin_body,
        title: 'Holding pen',
        described_state: 'waiting_response',
        awaiting_description: false,
        prominence: 'hidden'
      )
      om = OutgoingMessage.new({
        status: 'ready',
        message_type: 'initial_request',
        body: 'This is the holding pen request. It shows responses that were sent to invalid addresses, and need moving to the correct request by an administrator.',
        last_sent_at: Time.zone.now,
        what_doing: 'normal_sort'

      })
      ir.outgoing_messages << om
      om.info_request = ir
      ir.save!
      ir.log_event(
        'sent',
        outgoing_message_id: om.id,
        email: ir.public_body.request_email
      )
    end
    ir
  end

  # states which require administrator action (hence email administrators
  # when they are entered, and offer state change dialog to them)
  def self.requires_admin_states
    %w(requires_admin error_message attention_requested)
  end

  # Display version of status
  def self.get_status_description(status)
    descriptions = {
      'waiting_classification'        => _("Awaiting classification."),
      'waiting_response'              => _("Awaiting response."),
      'waiting_response_overdue'      => _("Delayed."),
      'waiting_response_very_overdue' => _("Long overdue."),
      'not_held'                      => _("Information not held."),
      'rejected'                      => _("Refused."),
      'partially_successful'          => _("Partially successful."),
      'successful'                    => _("Successful."),
      'waiting_clarification'         => _("Waiting clarification."),
      'gone_postal'                   => _("Handled by postal mail."),
      'internal_review'               => _("Awaiting internal review."),
      'error_message'                 => _("Delivery error"),
      'requires_admin'                => _("Unusual response."),
      'attention_requested'           => _("Reported for administrator attention."),
      'user_withdrawn'                => _("Withdrawn by the requester."),
      'vexatious'                     => _("Considered by administrators as " \
                                           "vexatious."),
      'not_foi'                       => _("Considered by administrators as " \
                                           "not an FOI request.")
    }
    if descriptions[status]
      descriptions[status]
    elsif respond_to?(:theme_display_status)
      theme_display_status(status)
    else
      raise _("unknown status {{status}}", status: status)
    end
  end

  def self.magic_email_for_id(prefix_part, id)
    magic_email = AlaveteliConfiguration.incoming_email_prefix
    magic_email += prefix_part + id.to_s
    magic_email += "-" + InfoRequest.hash_from_id(id)
    magic_email += "@" + AlaveteliConfiguration.incoming_email_domain
    magic_email
  end

  def self.build_from_attributes(info_request_atts, outgoing_message_atts, user=nil)
    info_request = new(info_request_atts)
    default_message_params = {
      status: 'ready',
      message_type: 'initial_request',
      what_doing: 'normal_sort'
    }

    attrs = outgoing_message_atts.merge(default_message_params)

    if attrs.respond_to?(:permit)
      attrs.permit(:body, :what_doing, :status, :message_type, :what_doing)
    end

    outgoing_message = OutgoingMessage.new(attrs)
    info_request.outgoing_messages << outgoing_message
    outgoing_message.info_request = info_request
    info_request.user = user
    info_request
  end

  def self.from_draft(draft)
    info_request = new(title: draft.title,
                       user: draft.user,
                       public_body: draft.public_body)
    info_request.outgoing_messages.new(body: draft.body,
                                       status: 'ready',
                                       message_type: 'initial_request',
                                       what_doing: 'normal_sort',
                                       info_request: info_request)
    if draft.embargo_duration
      info_request.embargo = AlaveteliPro::Embargo.new(
        embargo_duration: draft.embargo_duration,
        info_request: info_request
      )
    end
    info_request
  end

  def self.hash_from_id(id)
    Digest::SHA1.hexdigest(id.to_s + AlaveteliConfiguration.incoming_email_secret)[0,8]
  end

  # Used to find when event last changed
  def self.last_event_time_clause(event_type=nil, join_table=nil, join_clause=nil)
    event_type_clause = ''
    if event_type
      event_type_clause = " AND info_request_events.event_type = '#{event_type}'"
    end
    tables = ['info_request_events']
    tables << join_table if join_table
    join_clause = "AND #{join_clause}" if join_clause
    "(SELECT info_request_events.created_at
          FROM #{tables.join(', ')}
          WHERE info_request_events.info_request_id = info_requests.id
          #{event_type_clause}
          #{join_clause}
          ORDER BY created_at desc
          LIMIT 1)"
  end

  def self.where_old_unclassified(age_in_days=nil)
    age_in_days =
      if age_in_days
        age_in_days.days
      else
        OLD_AGE_IN_DAYS
      end

    where("awaiting_description = ?
          AND last_public_response_at < ?
          AND url_title != 'holding_pen'
          AND user_id IS NOT NULL",
          true, Time.zone.now - age_in_days)
  end

  def self.download_zip_dir
    File.join(Rails.root, "cache", "zips", Rails.env)
  end

  def self.reject_incoming_at_mta(options)
    query = InfoRequest.where(["updated_at < (now() -
                                interval ?)
                                AND allow_new_responses_from = 'nobody'
                                AND rejected_incoming_count >= ?
                                AND reject_incoming_at_mta = ?
                                AND url_title <> 'holding_pen'",
                                "#{options[:age_in_months]} months",
                                options[:rejection_threshold], false])
    yield query.pluck(:id) if block_given?

    if options[:dryrun]
      0
    else
      query.update_all(reject_incoming_at_mta: true)
    end
  end

  def self.requests_old_after_months
    AlaveteliConfiguration.restrict_new_responses_on_old_requests_after_months
  end

  def self.requests_very_old_after_months
    requests_old_after_months * 4
  end

  # This is called from cron regularly.
  def self.stop_new_responses_on_old_requests
    # 'old' months since last change to request, only allow new incoming
    # messages from authority domains
    InfoRequest.
      been_published.
      where(allow_new_responses_from: 'anybody').
      where.not(url_title: 'holding_pen').
      updated_before(requests_old_after_months.months.ago.to_date).
      distinct.
      find_in_batches do |batch|
        batch.each do |info_request|
          old_allow_new_responses_from = info_request.allow_new_responses_from

          info_request.
            update_column(:allow_new_responses_from, 'authority_only')

          params =
            { old_allow_new_responses_from: old_allow_new_responses_from,
              allow_new_responses_from: info_request.allow_new_responses_from,
              editor: 'InfoRequest.stop_new_responses_on_old_requests' }

          info_request.log_event('edit', params)
        end
      end

    # 'very_old' months since last change to request, don't allow any new
    # incoming messages
    InfoRequest.
      been_published.
      where(allow_new_responses_from: %w[anybody authority_only]).
      where.not(url_title: 'holding_pen').
      updated_before(requests_very_old_after_months.months.ago.to_date).
      distinct.
      find_in_batches do |batch|
        batch.each do |info_request|
          old_allow_new_responses_from = info_request.allow_new_responses_from

          info_request.
            update_column(:allow_new_responses_from, 'nobody')

          params =
            { old_allow_new_responses_from: old_allow_new_responses_from,
              allow_new_responses_from: info_request.allow_new_responses_from,
              editor: 'InfoRequest.stop_new_responses_on_old_requests' }

          info_request.log_event('edit', params)
        end
      end
  end

  def self.request_list(filters, page, per_page, max_results)
    query = InfoRequestEvent.make_query_from_params(filters)
    search_options = {
      limit: 25,
      offset: (page - 1) * per_page,
      collapse_by_prefix: 'request_collapse' }

    xapian_object = search_events(query, search_options)
    list_results = xapian_object.results.map { |r| r[:model] }
    matches_estimated = xapian_object.matches_estimated
    show_no_more_than = [matches_estimated, max_results].min
    { results: list_results,
      matches_estimated: matches_estimated,
      show_no_more_than: show_no_more_than }
  end

  def self.recent_requests
    request_events = []
    request_events_all_successful = false
    # Get some successful requests
    begin
      query = 'variety:response (status:successful OR status:partially_successful)'
      max_count = 5
      search_options = {
        limit: max_count,
        collapse_by_prefix: 'request_title_collapse' }

      xapian_object = search_events(query, search_options)
      xapian_object.results
      request_events = xapian_object.results.map { |r| r[:model] }

      # If there are not yet enough successful requests, fill out the list with
      # other requests
      if request_events.count < max_count
        query = 'variety:sent'
        search_options[:limit] = max_count-request_events.count
        xapian_object = search_events(query, search_options)
        xapian_object.results
        more_events = xapian_object.results.map { |r| r[:model] }
        request_events += more_events
        # Overall we still want the list sorted with the newest first
        request_events.sort! { |e1,e2| e2.created_at <=> e1.created_at }
      else
        request_events_all_successful = true
      end
    rescue
      request_events = []
    end

    [request_events, request_events_all_successful]
  end

  def self.find_in_state(state)
    where(described_state: state).
      order(:last_event_time)
  end

  def self.log_overdue_events
    log_overdue_event_type('overdue')
  end

  def self.log_very_overdue_events
    log_overdue_event_type('very_overdue')
  end

  def self.log_overdue_event_type(event_type)
    date_field = case event_type
    when 'overdue'
      'date_response_required_by'
    when 'very_overdue'
      'date_very_overdue_after'
    else
      raise ArgumentError("Event type #{event_type} not handled")
    end

    query =
      where(["awaiting_description = ?
              AND described_state = ?
              AND #{date_field} < ?
              AND (SELECT id
              FROM info_request_events
              WHERE info_request_id = info_requests.id
              AND event_type = ?
              AND created_at > info_requests.#{date_field})
              IS NULL",
              false,
              'waiting_response',
              Time.zone.today,
              event_type])

    query.find_each(batch_size: 100) do |info_request|
      # Date to DateTime representing beginning of day
      created_at = info_request.send(date_field).beginning_of_day + 1.day
      event = info_request.log_event(
        event_type,
        { event_created_at: Time.zone.now },
        created_at: created_at
      )
      info_request.user.notify(event) if info_request.use_notifications?
    end
  end

  def self.request_sent_types
    %w(sent resent followup_sent followup_resent send_error)
  end

  # Possible reasons that a request could be reported for administrator attention
  def report_reasons
    [_("Contains defamatory material"),
     _("Not a valid request"),
     _("Request for personal information"),
     _("Contains personal information"),
     _("Vexatious"),
     _("Other")]
  end

  # Public: Overrides the ActiveRecord attribute accessor
  #
  # opts = Hash of options (default: {})
  #        :decorate - Wrap the string in a ProminenceCalculator decorator that
  #        has methods indicating whether the InfoRequest is public, searchable
  #        etc.
  # Returns a String or ProminenceCalculator
  def prominence(opts = {})
    decorate = opts.fetch(:decorate, false)
    if decorate
      Prominence::Calculator.new(self)
    else
      read_attribute(:prominence)
    end
  end

  # opts = Hash of options (default: {})
  # Returns a StateCalculator
  def state(_opts = {})
    State::Calculator.new(self)
  end

  def indexed_by_search?
    prominence(decorate: true).is_searchable?
  end

  # The request must either be internal, in which case it has
  # a foreign key reference to a User object and no external_url or external_user_name,
  # or else be external in which case it has no user_id but does have an external_url,
  # and may optionally also have an external_user_name.
  #
  # External requests are requests that have been added using the API, whereas internal
  # requests are requests made using the site.
  def must_be_internal_or_external
    # We must permit user_id and external_user_name both to be nil, because the system
    # allows a request to be created by a non-logged-in user.
    if user_id
      unless external_user_name.nil?
        errors.add(:external_user_name, "must be null for an internal request")
      end
      unless external_url.nil?
        errors.add(:external_url, "must be null for an internal request")
      end
    end
  end

  def internal_review_requested?
    outgoing_messages.where(what_doing: 'internal_review').any?
  end

  def is_external?
    external_url.nil? ? false : true
  end

  def user_name
    return external_user_name if is_external?

    user&.name
  end

  def from_name
    return external_user_name if is_external?

    outgoing_messages.first&.from_name || user_name
  end

  def safe_from_name
    return external_user_name if is_external?

    apply_censor_rules_to_text(from_name)
  end

  def user_name_slug
    if is_external?
      if external_user_name.nil?
        fake_slug = "anonymous"
      else
        fake_slug = MySociety::Format.simplify_url_part(external_user_name, 'external_user', 32)
      end
      (public_body.url_name || "") + "_" + fake_slug
    else
      user.url_name
    end
  end

  def user_json_for_api
    is_external? ? { name: user_name || _("Anonymous user") } : user.json_for_api
  end

  @@custom_states_loaded = false
  begin
    require 'customstates'
    include InfoRequestCustomStates
    @@custom_states_loaded = true
  rescue LoadError, NameError
  end

  def reindex_request_events
    info_request_events.find_each(&:xapian_mark_needs_index)
  end

  # Force reindex when tag string changes
  alias orig_tag_string= tag_string=
  def tag_string=(tag_string)
    ret = self.orig_tag_string=(tag_string)
    reindex_request_events
    ret
  end

  def expire(options={})
    # Clear any attachment masked_at timestamp, forcing attachments to be
    # reparsed
    clear_attachment_masks!

    # Clear out cached entries, by removing files from disk (the built in
    # Rails fragment cache made doing this and other things too hard)
    foi_fragment_cache_directories.each { |dir| FileUtils.rm_rf(dir) }

    # Remove any download zips
    FileUtils.rm_rf(download_zip_dir)

    # Remove the database caches of body / attachment text (the attachment text
    # one is after privacy rules are applied)
    clear_in_database_caches! unless options[:preserve_database_cache]

    # also force a search reindexing (so changed text reflected in search)
    reindex_request_events
  end

  def clear_attachment_masks!
    foi_attachments.update_all(masked_at: nil)
  end

  # Removes anything cached about the object in the database, and saves
  def clear_in_database_caches!
    incoming_messages.each(&:clear_in_database_caches!)
  end

  def update_last_public_response_at
    last_public_event = get_last_public_response_event
    if last_public_event
      self.last_public_response_at = last_public_event.created_at
    else
      self.last_public_response_at = nil
    end
    save!
  end

  # Remove spaces from ends (for when used in emails etc.)
  # Needed for legacy reasons, even though we call strip_attributes now
  def title
    _title = read_attribute(:title)
    _title.strip! if _title
    _title
  end

  # Email which public body should use to respond to request. This is in
  # the format PREFIXrequest-ID-HASH@DOMAIN. Here ID is the id of the
  # FOI request, and HASH is a signature for that id.
  def incoming_email
    magic_email("request-")
  end

  def incoming_name_and_email
    MailHandler.address_from_name_and_email(user_name, incoming_email)
  end

  # Subject lines for emails about the request
  def email_subject_request(opts = {})
    html = opts.fetch(:html, true)
    _('{{law_used_full}} request - {{title}}',
      law_used_full: legislation.to_s(:full),
      title: (html ? title : title.html_safe))
  end

  def email_subject_followup(opts = {})
    incoming_message = opts.fetch(:incoming_message, nil)
    html = opts.fetch(:html, true)
    if incoming_message.nil? || !incoming_message.valid_to_reply_to? || !incoming_message.subject
      'Re: ' + email_subject_request(html: html)
    elsif incoming_message.subject.match(/^Re:/i)
      incoming_message.subject
    else
      'Re: ' + incoming_message.subject
    end
  end

  def legislation
    return Legislation.find!(law_used) if law_used

    public_body&.legislation || Legislation.default
  end

  def find_existing_outgoing_message(body)
    outgoing_messages.with_body(body).first
  end

  # Has this email already been received here? Based just on message id.
  def already_received?(email)
    return false unless email.message_id

    incoming_messages.any? { email.message_id == _1.message_id }
  end

  def receive(email, raw_email_data, *args)
    return if already_received?(email)

    defaults = { override_stop_new_responses: false,
                 rejected_reason: nil,
                 source: :internal }

    opts = if args.first.is_a?(Hash)
      defaults.merge(args.shift)
    else
      defaults
    end

    if receive_mail_from_source? opts[:source]
      # Is this request allowing responses?
      accepted =
        if opts[:override_stop_new_responses]
          true
        else
          accept_incoming?(email, raw_email_data)
        end

      if accepted
        incoming_message =
          create_response!(email, raw_email_data, opts[:rejected_reason])

        # Notify the user that a new response has been received, unless the
        # request is external
        unless is_external?
          if use_notifications?
            info_request_event = info_request_events.find_by(
              event_type: 'response',
              incoming_message_id: incoming_message.id
            )
            user.notify(info_request_event)
          else
            RequestMailer.new_response(self, incoming_message).deliver_now
          end
        end
      end
    end
  end

  # Called when outgoing_messages are sent to ensure that the request
  # is not closed during an active discussion or an internal review
  def reopen_to_new_responses
    update(allow_new_responses_from: 'anybody', reject_incoming_at_mta: false)
  end

  # An annotation (comment) is made
  def add_comment(body, user)
    comment = Comment.new
    ActiveRecord::Base.transaction do
      comment.body = body
      comment.user = user
      comment.info_request = self
      comment.save!

      log_event('comment', comment_id: comment.id)
      save!
    end
    comment
  end

  def requires_admin?
    self.class.requires_admin_states.include?(described_state)
  end

  # Report this request for administrator attention
  def report!(reason, message, user)
    ActiveRecord::Base.transaction do
      log_event(
        'report_request',
        request_id: id,
        editor: user,
        reason: reason,
        message: message,
        old_attention_requested: attention_requested,
        attention_requested: true
      )

      set_described_state('attention_requested', user, "Reason: #{reason}\n\n#{message}")
      self.attention_requested = true # tells us if attention has ever been requested
      save!
    end
  end

  # change status, including for last event for later historical purposes
  # described_state should always indicate the current state of the request, as described
  # by the request owner (or, in some other cases an admin or other user)
  def set_described_state(new_state, set_by = nil, message = "")
    old_described_state = described_state
    ActiveRecord::Base.transaction do
      self.awaiting_description = false
      last_event = info_request_events.last
      last_event.described_state = new_state

      self.described_state = new_state
      last_event.save!
      save!
    end

    calculate_event_states

    if requires_admin?
      # Check there is someone to send the message "from"
      if set_by && user
        RequestMailer.requires_admin(self, set_by, message).deliver_now
      end
    end

    unless set_by.nil? || is_actual_owning_user?(set_by) || described_state == 'attention_requested'
      RequestMailer.
        old_unclassified_updated(self).deliver_now unless is_external?
    end
  end

  # Work out what state to display for the request on the site. In addition to values of
  # self.described_state, can take these values:
  #   waiting_classification
  #   waiting_response_overdue
  #   waiting_response_very_overdue
  # (this method adds an assessment of overdueness with respect to the current time to 'waiting_response'
  # states, and will return 'waiting_classification' instead of the described_state if the
  # awaiting_description flag is set on the request).
  def calculate_status(cached_value_ok=false)
    if cached_value_ok && @cached_calculated_status
      return @cached_calculated_status
    end

    @cached_calculated_status = @@custom_states_loaded ? theme_calculate_status : base_calculate_status
  end

  def base_calculate_status
    return 'waiting_classification' if awaiting_description
    return described_state unless described_state == "waiting_response"
    # Compare by date, so only overdue on next day, not if 1 second late
    return 'waiting_response_very_overdue' if
    Time.zone.now.strftime("%Y-%m-%d") > date_very_overdue_after.strftime("%Y-%m-%d")
    return 'waiting_response_overdue' if
    Time.zone.now.strftime("%Y-%m-%d") > date_response_required_by.strftime("%Y-%m-%d")

    'waiting_response'
  end

  # 'described_state' can be populated on any info_request_event but is only
  # ever used in the process populating calculated_state on the
  # info_request_event (if it represents a response, outgoing message, edit
  # or status update), or previous response or outgoing message events for
  # the same request.

  # Fill in any missing event states for first response before a description
  # was made. i.e. We take the last described state in between two responses
  # (inclusive of earlier), and set it as calculated value for the earlier
  # response. Also set the calculated state for any initial outgoing message,
  # follow up, edit or status_update to the described state of that event.

  # Note that the calculated state of the latest info_request_event will
  # be used in latest_status based searches and should match the described_state
  # of the info_request.
  def calculate_event_states
    curr_state = nil
    info_request_events.reverse.each do |event|
      event.xapian_mark_needs_index # we need to reindex all events in order to update their latest_* terms
      if curr_state.nil?
        curr_state = event.described_state if event.described_state
      end

      if curr_state && event.event_type == 'response'
        event.set_calculated_state!(curr_state)

        if event.last_described_at.nil? # TODO: actually maybe this isn't needed
          event.last_described_at = Time.zone.now
          event.save!
        end
        curr_state = nil
      elsif curr_state && (event.event_type == 'followup_sent' || event.event_type == 'sent') && event.described_state && (event.described_state == 'waiting_response' || event.described_state == 'internal_review')
        # Followups can set the status to waiting response / internal
        # review. Initial requests ('sent') set the status to waiting response.

        # We want to store that in calculated_state state so it gets
        # indexed.
        event.set_calculated_state!(event.described_state)

        # And we don't want to propagate it to the response itself,
        # as that might already be set to waiting_clarification / a
        # success status, which we want to know about.
        curr_state = nil
      elsif curr_state && (%w[edit status_update].include? event.event_type)
        # A status update or edit event should get the same calculated state as described state
        # so that the described state is always indexed (and will be the latest_status
        # for the request immediately after it has been described, regardless of what
        # other request events precede it). This means that request should be correctly included
        # in status searches for that status. These events allow the described state to propagate in
        # case there is a preceding response that the described state should be applied to.
        event.set_calculated_state!(event.described_state)
      end
    end
  end

  # Find last InfoRequestEvent which  was:
  # -- sent at all
  # -- OR the same message was resent
  # -- OR the public body requested clarification, and a follow up was sent
  def last_event_forming_initial_request
    info_request_event_id = read_attribute(:last_event_forming_initial_request_id)
    last_sent = if info_request_event_id
      InfoRequestEvent.find_by_id(info_request_event_id)
    else
      calculate_last_event_forming_initial_request
    end

    if last_sent.nil?
      raise "internal error, last_event_forming_initial_request gets nil for " \
            "request #{ id } outgoing messages count " \
            "#{ outgoing_messages.size } all events: " \
            "#{ info_request_events.to_yaml }"
    end

    last_sent
  end

  def calculate_last_event_forming_initial_request
    # TODO: This can be removed when last_event_forming_initial_request_id has
    # been populated for all requests

    expecting_clarification = false
    last_sent = nil
    info_request_events.each do |event|
      if event.described_state == 'waiting_clarification'
        expecting_clarification = true
      end

      if self.class.request_sent_types.include?(event.event_type)
        if last_sent.nil?
          last_sent = event
        elsif event.event_type == 'resent' ||
              (event.event_type == 'send_error' &&
               event.outgoing_message.message_type == 'initial_request')
          last_sent = event
        elsif expecting_clarification && event.event_type == 'followup_sent'
          # TODO: this needs to cope with followup_resent, which it doesn't.
          # Not really easy to do, and only affects cases where followups
          # were resent after a clarification.
          last_sent = event
          expecting_clarification = false
        end
      end
    end
    last_sent
  end

  # Log an event to the history of some things that have happened to this request
  def log_event(type, params, options = {})
    event = info_request_events.create!(event_type: type, params: params)
    set_due_dates(event) if event.resets_due_dates?
    if options[:created_at]
      event.update_column(:created_at, options[:created_at])
    end
    if !last_event_time || (event.created_at > last_event_time)
      update_column(:last_event_time, event.created_at)
    end
    event
  end

  def set_due_dates(sent_event)
    self.last_event_forming_initial_request_id = sent_event.id
    self.date_initial_request_last_sent_at = sent_event.created_at.to_date
    self.date_response_required_by = calculate_date_response_required_by
    self.date_very_overdue_after = calculate_date_very_overdue_after
    save!
  end

  # TODO: once date_initial_request_sent_at is populated for all
  # requests, this can be removed
  # The last time that the initial request was sent/resent
  def date_initial_request_last_sent_at
    date = read_attribute(:date_initial_request_last_sent_at)
    return date.to_date if date

    calculate_date_initial_request_last_sent_at
  end

  # TODO: once date_initial_request_sent_at is populated for all
  # requests, this can be removed
  def calculate_date_initial_request_last_sent_at
    last_sent = last_event_forming_initial_request
    last_sent.outgoing_message.last_sent_at.to_date
  end

  def late_calculator
    @late_calculator ||= DefaultLateCalculator.new
  end

  # TODO: once date_response_required_by is populated for all
  # requests, this can be removed
  def date_response_required_by
    date = read_attribute(:date_response_required_by)
    return date if date

    calculate_date_response_required_by
  end

  def calculate_date_response_required_by
    Holiday.due_date_from(date_initial_request_last_sent_at,
                          late_calculator.reply_late_after_days,
                          AlaveteliConfiguration.working_or_calendar_days)
  end

  # TODO: once date_very_overdue_after is populated for all
  # requests, this can be removed
  def date_very_overdue_after
    date = read_attribute(:date_very_overdue_after)
    return date if date

    calculate_date_very_overdue_after
  end

  def calculate_date_very_overdue_after
    Holiday.due_date_from(date_initial_request_last_sent_at,
                          late_calculator.reply_very_late_after_days,
                          AlaveteliConfiguration.working_or_calendar_days)
  end

  def last_embargo_set_event
    info_request_events.
      where(event_type: 'set_embargo').
        reorder(created_at: :desc).
          first
  end

  def last_embargo_expire_event
    info_request_events.
      where(event_type: 'expire_embargo').
        reorder(created_at: :desc).
          first
  end

  # Where the initial request is sent to
  def recipient_email
    public_body.request_email
  end

  def recipient_email_valid_for_followup?
    public_body.is_followupable?
  end

  def recipient_name_and_email
    MailHandler.address_from_name_and_email(
      # TRANSLATORS: Please don't use double quotes (") in this translation
      # or it will break the site's ability to send emails to authorities!
      _("{{law_used_short}} requests at {{public_body}}",
        law_used_short: legislation,
        public_body: public_body.short_or_long_name),
        recipient_email)
  end

  def public_response_events
    condition = <<-SQL
        info_request_events.event_type = ?
        AND incoming_messages.prominence = ?
    SQL

    info_request_events.
      joins(:incoming_message).
      where(condition, 'response', 'normal')
  end

  # The last public response is the default one people might want to reply to
  def get_last_public_response_event_id
    get_last_public_response_event.id if get_last_public_response_event
  end

  def get_last_public_response_event
    public_response_events.last
  end

  def get_last_public_response
    if get_last_public_response_event
      get_last_public_response_event.incoming_message
    end
  end

  def public_outgoing_events
    info_request_events.select { |e| e.outgoing? && e.outgoing_message.is_public? }
  end

  # The last public outgoing message
  def get_last_public_outgoing_event
    public_outgoing_events.last
  end

  # Text from the the initial request, for use in summary display
  def initial_request_text
    return '' if outgoing_messages.empty?

    body_opts = { censor_rules: applicable_censor_rules }
    first_message = outgoing_messages.first
    first_message.is_public? ? first_message.get_text_for_indexing(true, body_opts) : ''
  end

  def last_event_id_needing_description
    last_event = events_needing_description[-1]
    last_event.nil? ? 0 : last_event.id
  end

  # Returns all the events which the user hasn't described yet - an empty array if all described.
  def events_needing_description
    events = info_request_events
    i = index_of_last_described_event
    if i.nil?
      events
    else
      events[i + 1, events.size]
    end
  end

  # Public: The most recent InfoRequestEvent.
  #
  # Returns an InfoRequestEvent or nil
  def last_event
    info_request_events.last
  end

  def last_update_hash
    Digest::SHA1.hexdigest(info_request_events.last.created_at.to_i.to_s + updated_at.to_i.to_s)
  end

  # Get previous email sent to
  def get_previous_email_sent_to(info_request_event)
    last_email = nil
    info_request_events.each do |e|
      if ((info_request_event.is_sent_sort? && e.is_sent_sort?) || (info_request_event.is_followup_sort? && e.is_followup_sort?)) && e.outgoing_message_id == info_request_event.outgoing_message_id
        break if e.id == info_request_event.id

        last_email = e.params[:email]
      end
    end
    last_email
  end

  def display_status(cached_value_ok=false)
    InfoRequest.get_status_description(calculate_status(cached_value_ok))
  end

  # Called by incoming_email - and used to be called to generate separate
  # envelope from address until we abandoned it.
  def magic_email(prefix_part)
    raise "id required to create a magic email" unless id

    InfoRequest.magic_email_for_id(prefix_part, id)
  end

  def compute_idhash
    self.idhash = InfoRequest.hash_from_id(id)
  end

  def foi_fragment_cache_directories
    # return stub path so admin can expire it
    directories = []
    path = File.join("request", request_dirs)
    foi_cache_path = File.expand_path(File.join(Rails.root, 'cache', 'views'))
    directories << File.join(foi_cache_path, path)
    AlaveteliLocalization.available_locales.each do |locale|
      directories << File.join(foi_cache_path, locale, path)
    end

    directories
  end

  def is_followupable?(incoming_message)
    if is_external?
      @followup_bad_reason = "external"
      false
    elsif !OutgoingMailer.is_followupable?(self, incoming_message)
      @followup_bad_reason = if public_body.is_requestable?
        "unexpected followupable inconsistency"
      else
        public_body.not_requestable_reason
      end
      false
    else
      @followup_bad_reason = nil
      true
    end
  end

  def postal_email
    if who_can_followup_to.empty?
      public_body.request_email
    else
      who_can_followup_to[-1][1]
    end
  end

  def postal_email_name
    if who_can_followup_to.empty?
      public_body.name
    else
      who_can_followup_to[-1][0]
    end
  end

  def request_dirs
    first_three_digits = id.to_s[0..2]
    File.join(first_three_digits.to_s, id.to_s)
  end

  def download_zip_dir
    File.join(InfoRequest.download_zip_dir, "download", request_dirs)
  end

  def make_zip_cache_path(user)
    # The zip file varies depending on user because it can include different
    # messages depending on whether the user can access hidden or
    # requester_only messages. We name it appropriately, so that every user
    # with the right permissions gets a file with only the right things in.
    cache_file_dir = File.join(InfoRequest.download_zip_dir,
                               "download",
                               request_dirs,
                               last_update_hash)
    cache_file_suffix = zip_cache_file_suffix(user)
    File.join(cache_file_dir, "#{url_title}#{cache_file_suffix}.zip")
  end

  def zip_cache_file_suffix(user)
    # Simple short circuit for requests where everything is public
    if all_correspondence_is_public?
      ""
    # If the user can view hidden things, they can view anything, so no need
    # to go any further
    elsif user&.view_hidden?
      "_hidden"
    # If the user can't view hidden things, but owns the request, they can
    # see more than the public, so they get requester_only
    elsif is_owning_user?(user)
      "_requester_only"
    # Everyone else can only see public stuff, which is the default case
    else
      ""
    end
  end

  def reason_to_be_unhappy?
    classified? && State.unhappy.include?(calculate_status)
  end

  def classified?
    !awaiting_description?
  end

  def is_old_unclassified?
    !is_external? && awaiting_description && url_title != 'holding_pen' && get_last_public_response_event &&
      Time.zone.now > get_last_public_response_event.created_at + OLD_AGE_IN_DAYS
  end

  # List of incoming messages to followup, by unique email
  def who_can_followup_to(skip_message = nil)
    ret = []
    done = {}
    if skip_message
      if (email = OutgoingMailer.email_for_followup(self, skip_message))
        done[email.downcase] = 1
      end
    end
    incoming_messages.reverse.each do |incoming_message|
      next if incoming_message == skip_message

      incoming_message.safe_from_name

      next unless incoming_message.is_public?

      email = OutgoingMailer.email_for_followup(self, incoming_message)
      name = OutgoingMailer.name_for_followup(self, incoming_message)

      unless done.include?(email.downcase)
        ret += [[name, email, incoming_message.id]]
      end
      done[email.downcase] = 1
    end

    unless done.include?(public_body.request_email.downcase)
      ret += [[public_body.name, public_body.request_email, nil]]
    end
    done[public_body.request_email.downcase] = 1

    ret.reverse
  end

  # Get the list of censor rules that apply to this request
  def applicable_censor_rules
    applicable_rules = [censor_rules, CensorRule.global]
    applicable_rules << public_body.censor_rules unless public_body.blank?
    applicable_rules << user.censor_rules if user
    applicable_rules.flatten
  end

  def apply_censor_rules_to_text(text)
    applicable_censor_rules.
      reduce(text) { |t, rule| rule.apply_to_text(t) }
  end

  def apply_censor_rules_to_binary(text)
    applicable_censor_rules.
      reduce(text) { |t, rule| rule.apply_to_binary(t) }
  end

  def apply_masks(text, content_type)
    mask_options = { censor_rules: applicable_censor_rules,
                     masks: masks }
    AlaveteliTextMasker.apply_masks(text, content_type, mask_options)
  end

  # Masks we apply to text associated with this request convert email addresses
  # we know about into textual descriptions of them
  def masks
    masks = [{ to_replace: incoming_email,
               replacement: _('[FOI #{{request}} email]', request: id.to_s) },
             { to_replace: AlaveteliConfiguration.contact_email,
               replacement: _("[{{site_name}} contact email]",
                              site_name: site_name) }]
    if public_body.is_followupable?
      masks << { to_replace: public_body.request_email,
                 replacement: _("[{{public_body}} request email]",
                                   public_body: public_body.short_or_long_name) }
    end
  end

  def is_owning_user?(user)
    return false unless user

    user.id == user_id || user.owns_every_request?
  end

  def is_actual_owning_user?(user)
    return false unless user

    user.id == user_id
  end

  def all_correspondence_is_public?
    prominence(decorate: true).is_public? &&
      incoming_messages.all?(&:is_public?) &&
      outgoing_messages.all?(&:is_public?)
  end

  def json_for_api(deep)
    ret = {
      id: id,
      url_title: url_title,
      title: title,
      created_at: created_at,
      updated_at: updated_at,
      described_state: described_state,
      display_status: display_status,
      awaiting_description: awaiting_description,
      prominence: prominence,
      law_used: law_used,
      tags: tag_array

      # not sure we need to make these, mainly anti-spam, admin params public
      # allow_new_responses_from
      # handle_rejected_responses
    }

    if deep
      if user
        ret[:user] = user.json_for_api
      else
        ret[:user_name] = user_name
      end
      ret[:public_body] = public_body.json_for_api
      ret[:info_request_events] = info_request_events.map { |e| e.json_for_api(false) }
    end
    ret
  end

  # This method updates the count columns of the PublicBody that
  # store the number of "not held", "to some extent successful" and
  # "both visible and classified" requests when saving or destroying
  # an InfoRequest associated with the body:
  def update_counter_cache(body = public_body)
    body.update_counter_cache
  end

  def similar_cache_key
    "request/similar/#{id}"
  end

  # Get requests that have similar important terms
  def similar_requests(limit=10)
    ids, more = similar_ids(limit)
    [InfoRequest.includes(public_body: :translations).where(id: ids), more]
  end

  # Get the ids of similar requests, and whether there are more
  def similar_ids(limit=10)
    Rails.cache.fetch(similar_cache_key, expires_in: 3.days) do
      ids = []
      xapian_similar_more = false
      begin
        xapian_similar =
          ActsAsXapian::Similar.new([InfoRequestEvent],
                                    info_request_events,
                                    limit: limit,
                                    collapse_by_prefix: 'request_collapse')
        xapian_similar_more = (xapian_similar.matches_estimated > limit)
        ids = xapian_similar.results.map do |result|
          result[:model].info_request_id
        end
      rescue
      end
      [ids, xapian_similar_more]
    end
  end

  def move_to_public_body(destination_public_body, opts = {})
    return nil unless destination_public_body.try(:persisted?)

    old_body = public_body
    editor = opts.fetch(:editor)

    attrs = { public_body: destination_public_body }

    if destination_public_body
      attrs[:law_used] = destination_public_body.legislation.key
    end

    return_val = if update(attrs)
                   log_event(
                     'move_request',
                     editor: editor,
                     public_body_url_name: public_body.url_name,
                     old_public_body_url_name: old_body.url_name
                   )

                   reindex_request_events

                   public_body
                 end

    # HACK: Manually reset counter caches
    # https://github.com/rails/rails/issues/10865
    old_body.class.reset_counters(old_body.id, :info_requests)
    update_counter_cache(old_body)
    public_body.class.reset_counters(public_body.id, :info_requests)
    update_counter_cache
    return_val
  end

  def move_to_user(destination_user, opts = {})
    return nil unless destination_user.try(:persisted?)

    old_user = user
    editor = opts.fetch(:editor)

    return_val = if update(user: destination_user)
                   log_event(
                     'move_request',
                     editor: editor,
                     user_url_name: user.url_name,
                     old_user_url_name: old_user.url_name
                   )

                   reindex_request_events

                   user
                 end

    # HACK: Manually reset counter caches
    # https://github.com/rails/rails/issues/10865
    old_user.class.reset_counters(old_user.id, :info_requests)
    user.class.reset_counters(user.id, :info_requests)
    return_val
  end

  # Is the request currently embargoed?
  #
  # Returns Boolean
  def embargoed?
    embargo.present?
  end

  # Is the attached embargo expiring soon?
  #
  # Returns boolean
  def embargo_expiring?
    embargo ? embargo.expiring_soon? : false
  end

  # Has a previously attached embargo expired?
  #
  # Returns boolean
  def embargo_expired?
    if !embargo && last_embargo_expire_event
      true
    else
      false
    end
  end

  # Is the attached embargo still present but has reached its publication date
  #
  # Returns boolean
  def embargo_pending_expiry?
    embargo ? embargo.expired? : false
  end

  # @see RequestSummaries#should_summarise?
  def should_summarise?
    info_request_batch_id.blank?
  end

  # Requests in a batch should update their parent batch request when they
  # are updated.
  #
  # @see RequestSummaries#should_update_parent_summary?
  def should_update_parent_summary?
    info_request_batch_id.present?
  end

  # @see RequestSummaries#request_summary_parent
  def request_summary_parent
    if info_request_batch_id.blank?
      nil
    else
      info_request_batch
    end
  end

  # @see RequestSummaries#request_summary_body
  def request_summary_body
    outgoing_messages.any? ? outgoing_messages.first.body : ""
  end

  # @see RequestSummaries#request_summary_public_body_names
  def request_summary_public_body_names
    public_body.name unless public_body.blank?
  end

  # @see RequestSummaries#request_summary_categories
  def request_summary_categories
    categories = []
    if embargo_expiring?
      categories << AlaveteliPro::RequestSummaryCategory.embargo_expiring
    end
    # A request with no events is in the process of being sent (probably
    # having been created within our tests rather than in real code) and will
    # error if we try to get the phase, skip it for now because it'll be saved
    # when it's sent and trigger this code again anyway.
    if last_event_forming_initial_request_id.present?
      phase_slug = state.phase.to_s
      phase = AlaveteliPro::RequestSummaryCategory.find_by(slug: phase_slug)
      categories << phase unless phase.blank?
    end
    categories
  end

  def holding_pen_request?
    return true if url_title == 'holding_pen'

    self == self.class.holding_pen_request
  end

  def latest_refusals
    incoming_messages.select(&:refusals?).last&.refusals || []
  end

  def cached_urls
    feed_request = TrackThing.new(
      info_request: self,
      track_type: 'request_updates'
    )
    feed_body = TrackThing.new(
      public_body: public_body,
      track_type: 'public_body_updates'
    )
    feed_user = TrackThing.new(
      tracked_user: user,
      track_type: 'user_updates'
    )
    [
      '/',
      public_body_path(public_body),
      request_path(self),
      request_details_path(self),
      '^/list',
      do_track_path(feed_request, feed = 'feed'),
      '^/feed/list/',
      do_track_path(feed_body, feed = 'feed'),
      do_track_path(feed_user, feed = 'feed'),
      user_path(user),
      show_user_wall_path(url_name: user.url_name)
    ]
  end

  private

  def self.add_conditions_from_extra_params(params, extra_params)
    if extra_params[:conditions]
      condition_string = extra_params[:conditions].shift
      params[:conditions][0] += " AND #{condition_string}"
      params[:conditions] += extra_params[:conditions]
    end
  end
  private_class_method :add_conditions_from_extra_params

  def self.search_events(query, opts = {})
    defaults = {
      offset: 0,
      limit: 20,
      sort_by_prefix: 'created_at',
      sort_by_ascending: true
    }
    ActsAsXapian::Search.new([InfoRequestEvent], query, defaults.merge(opts))
  end
  private_class_method :search_events

  def receive_mail_from_source?(source)
    if source == :internal
      true
    elsif feature_enabled?(:accept_mail_from_anywhere)
      true
    elsif user.features.enabled?(:accept_mail_from_poller)
      source == :poller
    else
      source == :mailin
    end
  end

  def accept_incoming?(email, raw_email_data)
    # See if new responses are prevented
    gatekeeper = ResponseGatekeeper.for(allow_new_responses_from, self)
    # Take action if the message looks like spam
    spam_checker = ResponseGatekeeper::SpamChecker.new

    # What rejected the email – the gatekeeper or the spam checker?
    response_rejector =
      if gatekeeper.allow?(email)
        if spam_checker.allow?(email)
          nil
        else
          spam_checker
        end
      else
        gatekeeper
      end

    # Figure out how to reject the mail if it was rejected
    response_rejection =
      if response_rejector
        ResponseRejection.
          for(response_rejector.rejection_action, self, email, raw_email_data)
      end

    will_be_rejected = (response_rejector && response_rejection) ? true : false
    if will_be_rejected && response_rejection.reject(response_rejector.reason)
      # update without changing the updated_at field
      update_column(:rejected_incoming_count, rejected_incoming_count.next)
      logger.info "Rejected incoming mail: #{ response_rejector.reason } request: #{ id }"
      false
    else
      true
    end
  end

  def create_response!(_email, raw_email_data, rejected_reason = nil)
    incoming_message = incoming_messages.build

    # To avoid a deadlock when simultaneously dealing with two
    # incoming emails that refer to the same InfoRequest, we
    # lock the row for update.
    with_lock do
      # TODO: These are very tightly coupled
      raw_email = RawEmail.new
      incoming_message.raw_email = raw_email
      incoming_message.save!
      raw_email.data = raw_email_data
      raw_email.save!

      unless described_state == 'user_withdrawn'
        self.awaiting_description = true
      end

      params = { incoming_message_id: incoming_message.id }
      params[:rejected_reason] = rejected_reason.to_s if rejected_reason
      log_event('response', params)

      save!
    end

    # for the "waiting_classification" index
    reindex_request_events

    incoming_message
  end

  # Returns index of last event which is described or nil if none described.
  def index_of_last_described_event
    info_request_events.reverse.each_with_index do |event, index|
      if event.described_state
        reverse_index = info_request_events.size - 1 - index
        return reverse_index
      end
    end
    nil
  end

  def set_defaults
    self.described_state = 'waiting_response' if described_state.nil?
  rescue ActiveModel::MissingAttributeError
    # this should only happen on Model.exists? call. It can be safely ignored.
    # See http://www.tatvartha.com/2011/03/activerecordmissingattributeerror-missing-attribute-a-bug-or-a-features/
  end

  def set_law_used
    return if law_used_changed?

    self.law_used = public_body.legislation.key if public_body
  end

  def set_use_notifications
    if use_notifications.nil?
      self.use_notifications = user &&
                               user.features.enabled?(:notifications) && \
                               info_request_batch_id.present?
    end
    true
  end

  def must_be_valid_state
    unless State.all.include?(described_state)
      errors.add(:described_state, "is not a valid state")
    end
  end

  # If the URL name has changed, then all request: queries will break unless
  # we update index for every event. Also reindex if prominence changes.
  def reindexable_attribute_changed?
    %i[url_title prominence user_id].any? do |attr|
      saved_change_to_attribute?(attr)
    end
  end

  def notify_public_body
    public_body.request_created
  end
end