hummingbird-me/kitsu-server

View on GitHub
app/resources/library_entry_resource.rb

Summary

Maintainability
A
2 hrs
Test Coverage
D
60%
# frozen_string_literal: true

class LibraryEntryResource < BaseResource
  TITLE_SORT = /\A([^\.]+)\.titles\.([^\.]+)\z/

  class TitleSortableFields
    def initialize(whitelist)
      @whitelist = whitelist
    end

    def include?(key)
      return true if @whitelist.include?(key)
      # Magic match-handling code
      match = TITLE_SORT.match(key.to_s)
      return false unless match
      media, title = match[1..-1]
      return false unless %w[anime manga drama].include?(media.downcase)
      return true if title.casecmp('canonical')
      return false unless /[a-z]{2}(_[a-z]{2})?/.match?(title)
      true
    end
  end

  attributes :status, :progress, :volumes_owned, :reconsuming, :reconsume_count,
    :notes, :private, :reaction_skipped, :progressed_at, :started_at, :finished_at

  filters :user_id, :media_id, :media_type, :status, :anime_id, :manga_id,
    :drama_id

  filter :status, apply: ->(records, values, _options) {
    values = values.map(&:underscore)
    statuses = LibraryEntry.statuses.values_at(*values).compact
    statuses = values if statuses.empty?
    records.where(status: statuses)
  }

  filter :kind, apply: ->(records, values, _options) {
    records.by_kind(*values)
  }

  filter :since, apply: ->(records, values, _options) {
    time = values.join.to_time
    records.where('library_entries.updated_at >= ?', time)
  }

  filter :following, apply: ->(records, values, _options) {
    records.following(values.join(','))
  }

  has_one :user
  has_one :anime
  has_one :manga
  has_one :drama
  has_one :review, eager_load_on_include: false
  has_one :media_reaction
  has_one :media, polymorphic: true
  has_one :unit, polymorphic: true, eager_load_on_include: false
  has_one :next_unit, polymorphic: true, eager_load_on_include: false

  paginator :library

  search_with LibrarySearchService
  query :title

  # DEPRECATED: These methods are for until all clients have switched to
  # rating_twenty
  attributes :rating, :rating_twenty
  def rating
    ((_model.rating.to_f / 2).floor.to_f / 2).to_s
  end

  def rating=(value)
    return unless value
    _model.rating = value.to_f * 4
  end

  def rating_twenty
    _model.rating
  end

  def rating_twenty=(value)
    _model.rating = value
  end
  # END DEPRECATED

  def self.status_counts(filters, opts = {})
    return if should_query?(filters)
    find_records(filters, opts).group(:status).count
  end

  def self.sortable_fields(context)
    fields = super + %i[anime.subtype manga.subtype drama.subtype
                        anime.episode_count manga.chapter_count
                        anime.user_count manga.user_count
                        anime.average_rating manga.average_rating]
    TitleSortableFields.new(fields)
  end

  def self.apply_sort(records, order_options, context = {})
    # For each requested sort option, decide whether to use the title sort logic
    order_options = order_options.map do |field, dir|
      [(TITLE_SORT.match?(field) ? :title : :other), field, dir]
    end
    # Combine consecutive sort options of the same type into lists
    order_options = order_options.each_with_object([]) do |curr, acc|
      type, field, dir = curr
      acc << [type, {}] unless acc.last&.first == type
      acc.last[1][field] = dir
    end
    # Send each list to either apply_title_sort or super
    order_options.each do |(type, sorts)|
      records = if type == :title
        apply_title_sort(records, sorts, context)
      else
        super(records, sorts, context)
      end
    end
    records
  end

  def self.apply_title_sort(records, order_options, _context = {})
    order_options.each_pair do |field, direction|
      media, title = TITLE_SORT.match(field.to_s)[1..-1]
      direction = direction.upcase

      records = records.joins(Arel.sql(<<-JOINS.squish))
        LEFT JOIN #{media} AS #{media}_sort
        ON #{media}_sort.id = library_entries.#{media}_id
      JOINS

      if title == 'canonical'
        records = records.order(Arel.sql(<<~ORDER))
          #{media}_sort.titles->#{media}_sort.canonical_title #{direction}
        ORDER
      elsif /[a-z]{2}(_[a-z]{2})?/i.match?(title)
        records = records.order(Arel.sql(<<~ORDER.squish))
          COALESCE(
            NULLIF(#{media}_sort.titles->'#{title}', ''),
            NULLIF(#{media}_sort.titles->#{media}_sort.canonical_title, ''),
            NULLIF(#{media}_sort.titles->'en_jp', '')
          ) #{direction}
        ORDER
      end
    end

    records
  end
end