app/models/topic.rb

Summary

Maintainability
A
0 mins
Test Coverage
class Topic < ActiveRecord::Base
  include PgSearch
  include PgSearchCustomisations
  multisearchable against: %i[
    title
    short_summary
    description
    raw_tag_list
    extended_content_values
  ]

  # this is where the actual content lives
  # using the extended_fields associated with this topic's topic_type
  # generate a form
  # the results of the form are stored in a extended_content column for the topic
  # as an xml doc
  # when displaying the topic we pull the xml doc out
  # and make the tag's values available as variables to the template
  belongs_to :topic_type

  # each topic or content item lives in exactly one basket
  # , :counter_cache => true
  belongs_to :basket

  scope :in_basket, lambda { |basket| { conditions: { basket_id: basket } } }

  # a topic may be the designated index page for it's basket
  belongs_to :index_for_basket, class_name: 'Basket', foreign_key: 'index_for_basket_id'

  # where we handle creator and contributor tracking
  include HasContributors

  # all our ZOOM_CLASSES need this to be searchable by zebra
  # include ConfigureActsAsZoomForKete

  # we can't use object.comments, because that is used by related content stuff
  # has_many :comments, :as => :commentable, :dependent => :destroy, :order => 'position'
  include KeteCommentable

  # this is where we handled "related to"
  has_many :content_item_relations, order: 'position', dependent: :delete_all

  # Content Item Relationships when the topic is on the related_item end
  # of the relationship, and another topic occupies topic_id.
  has_many :child_content_item_relations, class_name: 'ContentItemRelation', as: :related_item, dependent: :delete_all
  has_many :parent_related_topics, through: :child_content_item_relations, source: :topic

  def self.updated_since(date)
    # Topic.where( <Topic or its join tables is newer than date>  )

    taggings_sql =                         Tagging.uniq.select(:taggable_id).where(taggable_type: 'Topic').where('created_at > ?', date).to_sql
    contributions_sql =                    Contribution.uniq.select(:contributed_item_id).where(contributed_item_type: 'Topic').where('updated_at > ?', date).to_sql
    content_item_relations_sql_1 =         ContentItemRelation.uniq.select(:related_item_id).where(related_item_type: 'Topic').where('updated_at > ?', date).to_sql
    content_item_relations_sql_2 =         ContentItemRelation.uniq.select(:topic_id).where('updated_at > ?', date).to_sql
    deleted_content_item_relations_sql_1 = "SELECT DISTINCT related_item_id FROM deleted_content_item_relations WHERE related_item_type = 'Topic' AND updated_at > ?"
    deleted_content_item_relations_sql_2 = 'SELECT DISTINCT topic_id FROM deleted_content_item_relations WHERE updated_at > ?'

    and_query = Topic.where('topics.  updated_at > ?', date)
                     .where("topics.id IN ( #{taggings_sql} )") # Tagging doesn't have an updated_at column.
                     .where("topics.id IN ( #{contributions_sql} )")
                     .where("topics.id IN ( #{content_item_relations_sql_1} )")
                     .where("topics.id IN ( #{content_item_relations_sql_2} )")
                     .where("topics.id IN ( #{deleted_content_item_relations_sql_1} )", date)
                     .where("topics.id IN ( #{deleted_content_item_relations_sql_2} )", date)

    or_query = and_query.where_values.join(' OR ')

    Topic.where(or_query).uniq # avoid repeated results from repeating ids.
  end

  def self.pre_load_associations
    # Speed up request with pre-loading of associations.
    includes(:creators).includes(:license).includes(:topic_type).includes(:basket)
  end

  def child_topic_content_relations
    content_item_relations.where(related_item_type: 'Topic').order(:position)
  end

  def child_related_topics
    join_as_related_item = 'JOIN content_item_relations ON content_item_relations.related_item_id = topics.id'
    Topic.joins(join_as_related_item).merge(child_topic_content_relations).includes(:basket)
  end

  # ZOOM_CLASSES:

  # ROB: I'd rather do these assocations as has_many() but I can't get this assocation working:
  #   has_many :audio_recording_relations, -> { where(related_item_type: "AudioRecording").order(:position) }, class_name: 'content_item_relations'
  # It'll probably be fixed in a later Rails.

  def still_images
    still_image_content_relations = content_item_relations.where(related_item_type: 'StillImage').order(:position)
    StillImage.joins(:content_item_relations).merge(still_image_content_relations).includes(:basket)
  end

  def audio_recordings
    audio_recording_content_relations = content_item_relations.where(related_item_type: 'AudioRecording').order(:position)
    AudioRecording.joins(:content_item_relations).merge(audio_recording_content_relations).includes(:basket)
  end

  def videos
    video_content_relations = content_item_relations.where(related_item_type: 'Video').order(:position)
    Video.joins(:content_item_relations).merge(video_content_relations).includes(:basket)
  end

  def web_links
    web_link_content_relations = content_item_relations.where(related_item_type: 'WebLink').order(:position)
    WebLink.joins(:content_item_relations).merge(web_link_content_relations).includes(:basket)
  end

  def documents
    document_content_relations = content_item_relations.where(related_item_type: 'Document').order(:position)
    ::Document.joins(:content_item_relations).merge(document_content_relations).includes(:basket)
  end

  # this allows us to turn on/off email notification per item
  attr_accessor :skip_email_notification

  # note, since acts_as_taggable doesn't support versioning
  # out of the box
  # we also track each versions raw_tag_list input
  # so we can revert later if necessary

  # Tags are tracked on a per-privacy basis.
  acts_as_taggable_on :public_tags
  acts_as_taggable_on :private_tags

  # we override acts_as_versioned dependent => delete_all
  # because of the complexity our relationships of our models
  # delete_all won't do the right thing (at least not in migrations)
  acts_as_versioned association_options: { dependent: :destroy }

  # acts as licensed but this is not versionable (cant change a license once it is applied)
  acts_as_licensed

  # this is a little tricky
  # the acts_as_taggable declaration for the original
  # is different than how we use tags on the versioned model
  # where we use it for flagging moderator options, like 'flag as inappropriate'
  # where 'inappropriate' is actually a tag on that particular version

  # Moderation flags are tracked in a separate context.
  Topic::Version.class_eval <<-RUBY
    acts_as_taggable_on :flags
    alias_method :tags, :flags
    alias_method :tag_list, :flag_list
    alias_method :tag_list=, :flag_list=
    alias_method :tag_counts, :flag_counts
    def latest_version
      @latest_version ||= Topic.find_by_id(self.topic_id)
    end
    def basket
      latest_version.basket
    end
    def first_related_image
      latest_version.first_related_image
    end
    def disputed_or_not_available?
      (title == SystemSetting.no_public_version_title) || (title == SystemSetting.blank_title)
    end
    include FriendlyUrls
    def to_param; format_for_friendly_urls(true); end
  RUBY

  validates_xml :fixed_extended_content
  validates_presence_of :title

  def fixed_extended_content
    add_xml_fix(extended_content)
  end

  # TODO: add validation that prevents markup in short_summary
  # globalize stuff, uncomment later
  # translates :title, :description, :short_summary, :extended_content

  # methods related to handling the xml kept in extended_content column
  include ExtendedContent

  # methods and declarations related to moderation and flagging
  include Flagging

  # convenience methods for a topics relations
  include RelatedItems

  # methods for merging values from versions together
  include Merge

  # Private Item mixin
  include ItemPrivacy::ActsAsVersionedOverload
  include ItemPrivacy::TaggingOverload
  non_versioned_columns << 'private_version_serialized'

  after_save :store_correct_versions_after_save

  # Kieran Pilkington - 2008/10/21
  # Named scopes used in the index page controller for recent topics
  scope :recent, lambda { where('1 = 1').order('created_at DESC').limit(5) }
  scope :public, lambda { where('title != ?', SystemSetting.no_public_version_title) }
  scope :exclude_baskets_and_id, lambda { |basket_ids, id| where('basket_id NOT IN (?) AND id != ?', basket_ids, id) }

  after_save :update_taggings_basket_id

  def update_taggings_basket_id
    taggings.each do |tagging|
      tagging.update_attribute(:basket_id, basket_id)
    end
  end

  # Walter McGinnis, 2011-02-15
  # oEmbed Functionality

  # EOIN: disable Oembed for the moment. Is there a use case for it?
  # include OembedProvidable
  # oembed_providable_as :link
  # include KeteCommonOembedSupport

  # perhaps in the future we will store thumbnails for links (i.e. webpage previews)
  # for topics, but not at the moment
  # return nil for these
  %w(url height width).each do |method_stub|
    define_method('thumbnail_' + method_stub) do
      nil
    end
  end

  def related_topics(only_non_pending = false)
    if only_non_pending
      parent_related_topics.find_all_public_non_pending +
        child_related_topics.find_all_public_non_pending
    else
      parent_related_topics + child_related_topics
    end
  end

  def still_images
    content_item_relations.where(related_item_type: 'StillImage').map(&:related_item)
  end

  def first_related_image
    still_images.first || {}
  end

  def title_for_license
    title
  end

  def author_for_license
    creator.user_name
  end

  def author_url_for_license
    "/#{Basket.find(1).urlified_name}/account/show/#{creator.to_param}"
  end

  # turn pretty urls on or off here
  include FriendlyUrls
  alias to_param format_for_friendly_urls

  def to_i
    id
  end

  # All available extended field mappings for this topic instance, including those from ancestors
  # of our TopicType.
  def all_field_mappings
    topic_type.all_field_mappings
  rescue
    []
  end

  def basket_or_default
    basket.present? ? basket : Basket.find_by_urlified_name(SystemSetting.default_basket)
  end
end