berkmancenter/lumendatabase

View on GitHub
app/models/notice.rb

Summary

Maintainability
C
1 day
Test Coverage
# frozen_string_literal: true

class Notice < ApplicationRecord
  include Searchability
  include Elasticsearch::Model

  extend RecentScope

  # == Constants ============================================================
  HIGHLIGHTS = %i[
    title body tag_list topics.name sender_name recipient_name
    works.description works.infringing_urls.url works.copyrighted_urls.url
  ].freeze

  SEARCHABLE_FIELDS = [
    TermSearch.new(:term, Searchability::MULTI_MATCH_FIELDS, 'All Fields'),
    TermSearch.new(:title, :title, 'Title'),
    TermSearch.new(:topics, 'topics.name', 'Topics'),
    TermSearch.new(:tags, :tag_list, 'Tags'),
    TermSearch.new(:jurisdictions, :jurisdiction_list, 'Jurisdictions'),
    TermSearch.new(:entities_country_codes, :entities_country_codes, 'Entity Country Code'),
    TermSearch.new(:sender_name, :sender_name, 'Sender Name'),
    TermSearch.new(:principal_name, :principal_name, 'Principal Name'),
    TermSearch.new(:recipient_name, :recipient_name, 'Recipient Name'),
    TermSearch.new(:submitter_name, :submitter_name, 'Submitter Name'),
    TermSearch.new(:submitter_country_code, :submitter_country_code, 'Submitter Country'),
    TermSearch.new(:works, 'works.description', 'Works Descriptions'),
    TermSearch.new(:action_taken, :action_taken, 'Action taken')
  ].freeze

  FILTERABLE_FIELDS = [
    TermFilter.new(:topic_facet, 'Topic'),
    TermFilter.new(:sender_name_facet, 'Sender'),
    TermFilter.new(:principal_name_facet, 'Principal'),
    TermFilter.new(:recipient_name_facet, 'Recipient'),
    TermFilter.new(:submitter_name_facet, 'Submitter'),
    TermFilter.new(:tag_list_facet, 'Tags'),
    TermFilter.new(:country_code_facet, 'Country'),
    TermFilter.new(:language_facet, 'Language'),
    TermFilter.new(:submitter_country_code_facet, 'Submitter Country'),
    UnspecifiedTermFilter.new(:action_taken_facet, 'Action taken'),
    DateRangeFilter.new(:date_received_facet, :date_received, 'Date')
  ].freeze

  ORDERING_OPTIONS = [
    ResultOrdering.new('relevancy desc', [:_score, :desc], 'Most Relevant', true),
    ResultOrdering.new('relevancy asc', [:_score, :asc], 'Least Relevant'),
    ResultOrdering.new('date_received desc', [:date_received, :desc], 'Date Received - newest'),
    ResultOrdering.new('date_received asc', [:date_received, :asc], 'Date Received - oldest'),
    ResultOrdering.new('created_at desc', [:created_at, :desc], 'Reported to Lumen - newest'),
    ResultOrdering.new('created_at asc', [:created_at, :asc], 'Reported to Lumen - oldest')
  ].freeze

  REDACTABLE_FIELDS = %i[body].freeze
  PER_PAGE = 10

  UNDER_REVIEW_VALUE = 'Under review'.freeze
  RANGE_SEPARATOR = '..'.freeze

  # Base entity notice roles allow us to define additional roles on subclasses
  # without having to keep track of what they are on notice. As long as
  # subclasses define DEFAULT_ENTITY_NOTICE_ROLES =
  # BASE_ENTITY_NOTICE_ROLES | local_roles, the OR will preserve all elements
  # of both.
  BASE_ENTITY_NOTICE_ROLES = %w[submitter].freeze
  DEFAULT_ENTITY_NOTICE_ROLES = (BASE_ENTITY_NOTICE_ROLES |
                                 %w[recipient sender]).freeze

  VALID_ACTIONS = %w[Yes No Partial Unspecified].freeze

  OTHER_TOPIC = 'Uncategorized'.freeze

  TYPES_TO_TOPICS = {
    'DMCA'                  => 'Copyright',
    'Counterfeit'           => 'Counterfeit',
    'Counternotice'         => 'Copyright',
    'CourtOrder'            => 'Court Orders',
    'DataProtection'        => 'EU - Right to Be Forgotten',
    'Defamation'            => 'Defamation',
    'GovernmentRequest'     => 'Government Requests',
    'LawEnforcementRequest' => 'Law Enforcement Requests',
    'PrivateInformation'    => 'Right of Publicity',
    'Trademark'             => 'Trademark',
    'Other'                 => OTHER_TOPIC,
    'Placeholder'           => OTHER_TOPIC
  }.freeze

  TYPES = TYPES_TO_TOPICS.keys
  TOPICS = TYPES_TO_TOPICS.values

  # == Relationships ========================================================
  belongs_to :reviewer, class_name: 'User'

  has_many :topic_assignments, dependent: :destroy
  has_many :topics, through: :topic_assignments
  has_many :topic_relevant_questions, through: :topics, source: :relevant_questions

  has_many :entity_notice_roles, dependent: :destroy, inverse_of: :notice
  has_many :entities, through: :entity_notice_roles, index_errors: true

  has_and_belongs_to_many :works, index_errors: true
  has_many :infringing_urls, through: :works, index_errors: true
  has_many :copyrighted_urls, through: :works, index_errors: true

  has_many :token_urls, dependent: :destroy
  has_many :archived_token_urls, dependent: :destroy
  has_and_belongs_to_many :relevant_questions
  has_one :documents_update_notification_notice
  has_many :file_uploads

  # == Attributes ===========================================================
  delegate :country_code, to: :recipient, allow_nil: true

  %i[sender principal recipient submitter attorney] .each do |entity|
    delegate :name, :country_code, to: entity, prefix: true, allow_nil: true
  end

  # == Extensions ===========================================================
  acts_as_taggable_on :tags, :jurisdictions, :regulations

  accepts_nested_attributes_for :file_uploads,
    reject_if: ->(attributes) { [attributes['file'], attributes[:pdf_request_fulfilled]].all?(&:blank?) }

  accepts_nested_attributes_for :entity_notice_roles, allow_destroy: true

  accepts_nested_attributes_for :works, allow_destroy: true

  define_elasticsearch_mapping

  # == Validations ==========================================================
  validates_inclusion_of :action_taken, in: VALID_ACTIONS, allow_blank: true
  validates_inclusion_of :language, in: Language.codes, allow_blank: true
  validates_presence_of :works, :entity_notice_roles
  validates :date_sent, date: { after: Proc.new { Date.new(1998,10,28) }, before: Proc.new { Time.now + 1.day }, allow_blank: true }
  validates :date_received, date: { after: Proc.new { Date.new(1998,10,28) }, before: Proc.new { Time.now + 1.day }, allow_blank: true }

  # == Callbacks ============================================================
  before_save :set_topics
  before_save :set_works_json
  after_create :set_published!, if: :submitter
  # This may fail in the dev environment if you don't have ES up and running,
  # but is works in other envs.
  after_destroy do
    __elasticsearch__.delete_document ignore: 404
  end

  # == Scopes ===============================================================
  scope :top_notices_token_urls, -> { joins(:archived_token_urls).select('notices.*, COUNT(archived_token_urls.id) AS counted_archived_token_urls').group('notices.id') }

  # == Class Methods ========================================================
  def self.label
    name.titleize
  end

  def self.type_models
    (TYPES - ['Counternotice']).map(&:constantize).freeze
  end

  def self.display_models
    (TYPES - ['Placeholder']).map(&:constantize).freeze
  end

  def self.available_for_review
    where(
      review_required: true,
      reviewer_id: nil,
      hidden: false,
      spam: false
    )
  end

  def self.in_review(user)
    where(review_required: true, reviewer_id: user).order(:created_at)
  end

  def self.in_topics(topics)
    joins(topic_assignments: :topic)
      .where('topics.id' => topics)
      .distinct
  end

  def self.submitted_by(submitters)
    joins(entity_notice_roles: :entity)
      .where('entity_notice_roles.name' => :submitter)
      .where('entities.id' => submitters)
  end

  def self.find_visible(notice_id)
    self.visible.find(notice_id)
  end

  def self.visible
    where(visible_qualifiers)
  end

  def self.visible_qualifiers
    { spam: false, hidden: false, published: true, rescinded: false }
  end

  def self.find_unpublished(notice_id)
    self.where(spam: false, hidden: false, published: false).find(notice_id)
    true
  rescue
    false
  end

  def self.get_approximate_count
    ActiveRecord::Base.connection.execute("SELECT reltuples FROM pg_class WHERE relname = 'notices'").getvalue(0, 0).to_i
  end

  # == Instance Methods =====================================================

  # Using reset_type because type is ALWAYS protected (deep in the Rails code).
  # attr_protected :id, :type, :reset_type
  # attr_protected :id, :type, as: :admin
  def reset_type
    type
  end

  def reset_type=(value)
    unless value.in?(TYPES)
      fail ActiveModel::MissingAttributeError.new("Cannot reset Notice type to: #{value}")
    end
    self[:type] = value
  end

  def reset_type_enum
    TYPES
  end

  def language_enum
    Language.all.inject( {} ) { |memo, l| memo[l.label] = l.code; memo }
  end

  def model_serializer
    if rescinded?
      RescindedNoticeSerializer
    else
      "#{self.class.name}Serializer".safe_constantize
    end
  end

  def other_entity_notice_roles
    other_roles = EntityNoticeRole.all_roles_names - Notice::DEFAULT_ENTITY_NOTICE_ROLES
    entity_notice_roles.find_all do |entity_notice_role|
      other_roles.include?(entity_notice_role.name)
    end
  end

  def submitter
    @submitter ||= submitters.first
  end

  def sender
    @sender ||= senders.first
  end

  def principal
    @principal ||= principals.first
  end

  def recipient
    @recipient ||= recipients.first
  end

  def attorney
    @attorney ||= attorneys.first
  end

  def auto_redact
    InstanceRedactor.new.redact(self)
  end

  def mark_for_review
    # We can't do this before notice creation, because the assessment may
    # depend on values of related entities, and the relationships aren't
    # available to traverse before persistence.
    unless persisted?
      Rails.logger.warn('Attempted to mark a notice for review before creation')
      return
    end

    update_column(:review_required, RiskAssessment.new(self).high_risk?)
  end

  def redacted(field)
    if review_required?
      UNDER_REVIEW_VALUE
    else
      send(field)
    end
  end

  def next_requiring_review
    self.class
      .where('id > ? and review_required = ?', id, true)
      .order('id asc')
      .first
  end

  def tag_list
    @tag_list ||= super
  end

  def jurisdiction_list
    @jurisdiction_list ||= super
  end

  def tag_list=(tag_list_value = '')
    unless tag_list_value.nil?
      tag_list_value = if tag_list_value.respond_to?(:each)
                         tag_list_value.flatten.map(&:downcase)
                       else
                         tag_list_value.downcase
                       end
    end

    super(tag_list_value)
  end

  def original_documents
    file_uploads.where(kind: 'original')
  end

  def supporting_documents
    file_uploads.where(kind: 'supporting')
  end

  def on_behalf_of_principal?
    return unless sender_name.present?
    principal_name.present? && principal_name != sender_name
  end

  def publication_delay
    submitter && submitter.publication_delay ? submitter.publication_delay : 0
  end

  def time_to_publish
    created_at + publication_delay.seconds
  end

  def should_be_published?
    time_to_publish <= Time.now
  end

  def set_published!
    self.published = should_be_published?
    save
  end

  def notice_topic_map
    topic = TYPES_TO_TOPICS.key?(self.type) ? TYPES_TO_TOPICS[self.type] : OTHER_TOPIC
    Topic.find_or_create_by(name: topic)
  end

  def hide_identities?
    false
  end

  def set_topics
    topic = notice_topic_map
    topics << topic unless topics.include?(topic)
  end

  def set_works_json
    self.works_json = works.map { |w| prep_work_json(w) }
  end

  def prep_work_json(work)
    json = {
      kind: work.kind,
      description: work.description,
      copyrighted_urls: work.copyrighted_urls.map { |u| prep_url_json(u) },
      infringing_urls: work.infringing_urls.map { |u| prep_url_json(u) }
    }

    if work.description_original && work.description_original != work.description
      json[:description_original] = work.description_original
    end

    json
  end

  def prep_url_json(url_instance)
    json = { url: url_instance.url }

    if url_instance.url_original && url_instance.url_original != url_instance.url
      json[:url_original] = url_instance.url_original
    end

    json
  end

  def restricted_to_researchers?
    submitter&.full_notice_only_researchers
  end

  def token_urls_count
    archived_token_urls.count
  end

  private

  def submitters
    select_roles 'submitter'
  end

  def senders
    select_roles 'sender'
  end

  def principals
    select_roles 'principal'
  end

  def recipients
    select_roles 'recipient'
  end

  def attorneys
    select_roles 'attorney'
  end

  def select_roles(role_name)
    entity_notice_roles.select{ |entity_notice_role| entity_notice_role.name == role_name }.map(&:entity)
  end

  def entities_country_codes
    entities.map(&:country_code).uniq
  end
end