otwcode/otwarchive

View on GitHub
app/controllers/tags_controller.rb

Summary

Maintainability
F
3 days
Test Coverage
class TagsController < ApplicationController
  include TagWrangling

  before_action :load_collection
  before_action :check_user_status, except: [:show, :index, :show_hidden, :search, :feed]
  before_action :check_permission_to_wrangle, except: [:show, :index, :show_hidden, :search, :feed]
  before_action :load_tag, only: [:edit, :update, :wrangle, :mass_update]
  before_action :load_tag_and_subtags, only: [:show]
  around_action :record_wrangling_activity, only: [:create, :update, :mass_update]

  caches_page :feed

  def load_tag
    @tag = Tag.find_by_name(params[:id])
    unless @tag && @tag.is_a?(Tag)
      raise ActiveRecord::RecordNotFound, "Couldn't find tag named '#{params[:id]}'"
    end
  end

  # improved performance for show page
  def load_tag_and_subtags
    @tag = Tag.includes(:direct_sub_tags).find_by_name(params[:id])
    unless @tag && @tag.is_a?(Tag)
      raise ActiveRecord::RecordNotFound, "Couldn't find tag named '#{params[:id]}'"
    end
  end

  # GET /tags
  def index
    if @collection
      @tags = Freeform.canonical.for_collections_with_count([@collection] + @collection.children)
    else
      no_fandom = Fandom.find_by_name(ArchiveConfig.FANDOM_NO_TAG_NAME)
      @tags = no_fandom.children.by_type('Freeform').first_class.limit(ArchiveConfig.TAGS_IN_CLOUD)
      # have to put canonical at the end so that it doesn't overwrite sort order for random and popular
      # and then sort again at the very end to make it alphabetic
      @tags = if params[:show] == 'random'
                @tags.random.canonical.sort
              else
                @tags.popular.canonical.sort
              end
    end
  end

  def search
    options = params[:tag_search].present? ? tag_search_params : {}
    options.merge!(page: params[:page]) if params[:page].present?
    @search = TagSearchForm.new(options)
    @page_subtitle = ts("Search Tags")

    return if params[:tag_search].blank?

    @page_subtitle = ts("Tags Matching '%{query}'", query: options[:name]) if options[:name].present?

    @tags = @search.search_results
    flash_search_warnings(@tags)
  end

  # if user is Admin or Tag Wrangler, show them details about the tag
  # if user is not logged in or a regular user, show them
  #   1. the works, if the tag had been wrangled and we can redirect them to works using it or its canonical merger
  #   2. the tag, the works and the bookmarks using it, if the tag is unwrangled (because we can't redirect them
  #       to the works controller)
  def show
    @page_subtitle = @tag.name
    if @tag.is_a?(Banned) && !logged_in_as_admin?
      flash[:error] = ts('Please log in as admin')
      redirect_to(tag_wranglings_path) && return
    end
    # if tag is NOT wrangled, prepare to show works and bookmarks that are using it
    if !@tag.canonical && !@tag.merger
      if logged_in? # current_user.is_a?User
        @works = @tag.works.visible_to_registered_user.paginate(page: params[:page])
      elsif logged_in_as_admin?
        @works = @tag.works.visible_to_owner.paginate(page: params[:page])
      else
        @works = @tag.works.visible_to_all.paginate(page: params[:page])
      end
      @bookmarks = @tag.bookmarks.visible.paginate(page: params[:page])
    end
    # cache the children, since it's a possibly massive query
    @tag_children = Rails.cache.fetch "views/tags/#{@tag.cache_key}/children" do
      children = {}
      (@tag.child_types - %w(SubTag)).each do |child_type|
        tags = @tag.send(child_type.underscore.pluralize).order('taggings_count_cache DESC').limit(ArchiveConfig.TAG_LIST_LIMIT + 1)
        children[child_type] = tags.to_a.uniq unless tags.blank?
      end
      children
    end
  end

  def feed
    begin
      @tag = Tag.find(params[:id])
    rescue ActiveRecord::RecordNotFound
      raise ActiveRecord::RecordNotFound, "Couldn't find tag with id '#{params[:id]}'"
    end
    @tag = @tag.merger if !@tag.canonical? && @tag.merger
    # Temp for testing
    if %w(Fandom Character Relationship).include?(@tag.type.to_s) || @tag.name == 'F/F'
      if @tag.canonical?
        @works = @tag.filtered_works.visible_to_all.order('created_at DESC').limit(25)
      else
        @works = @tag.works.visible_to_all.order('created_at DESC').limit(25)
      end
    else
      redirect_to(tag_works_path(tag_id: @tag.to_param)) && return
    end

    respond_to do |format|
      format.html
      format.atom
    end
  end

  def show_hidden
    unless params[:creation_id].blank? || params[:creation_type].blank? || params[:tag_type].blank?
      model = case params[:creation_type].downcase
              when "series"
                Series
              when "work"
                Work
              when "chapter"
                Chapter
              end
      @display_creation = model.find(params[:creation_id]) if model.is_a? Class

      # Tags aren't directly on series, so we need to handle them differently
      if params[:creation_type] == 'Series'
        if params[:tag_type] == 'warnings'
          @display_tags = @display_creation.works.visible.collect(&:archive_warnings).flatten.compact.uniq.sort
        else
          @display_tags = @display_creation.works.visible.collect(&:freeforms).flatten.compact.uniq.sort
        end
      else
        @display_tags = case params[:tag_type]
                        when 'warnings'
                          @display_creation.archive_warnings
                        when 'freeforms'
                          @display_creation.freeforms
                        end
      end

      # The string used in views/tags/show_hidden.js.erb
      if params[:tag_type] == 'warnings'
        @display_category = 'warnings'
      else
        @display_category = @display_tags.first.class.name.tableize
      end
    end

    respond_to do |format|
      format.html do
        # This is just a quick fix to avoid script barf if JavaScript is disabled
        flash[:error] = ts('Sorry, you need to have JavaScript enabled for this.')
        if request.env['HTTP_REFERER']
          redirect_to(request.env['HTTP_REFERER'] || root_path)
        else
          # else branch needed to deal with bots, which don't have a referer
          redirect_to '/'
        end
      end
      format.js
    end
  end

  # GET /tags/new
  def new
    @tag = Tag.new

    respond_to do |format|
      format.html # new.html.erb
    end
  end

  # POST /tags
  def create
    type = tag_params[:type] if params[:tag]

    unless type
      flash[:error] = ts("Please provide a category.")
      @tag = Tag.new(name: tag_params[:name])
      render(action: "new")
      return
    end

    raise "Redshirt: Attempted to constantize invalid class initialize create #{type.classify}" unless Tag::TYPES.include?(type.classify)

    model = begin
              type.classify.constantize
            rescue StandardError
              nil
            end
    @tag = model.find_or_create_by_name(tag_params[:name]) if model.is_a? Class

    unless @tag&.valid?
      render(action: "new")
      return
    end

    if @tag.id_previously_changed? # i.e. tag is new
      @tag.update_attribute(:canonical, tag_params[:canonical])
      flash[:notice] = ts("Tag was successfully created.")
    else
      flash[:notice] = ts("Tag already existed and was not modified.")
    end

    redirect_to edit_tag_path(@tag)
  end

  def edit
    @page_subtitle = ts('%{tag_name} - Edit', tag_name: @tag.name)

    if @tag.is_a?(Banned) && !logged_in_as_admin?
      flash[:error] = ts('Please log in as admin')

      redirect_to(tag_wranglings_path) && return
    end

    @counts = {}
    @uses = ['Works', 'Drafts', 'Bookmarks', 'Private Bookmarks', 'External Works', 'Taggings Count']
    @counts['Works'] = @tag.visible_works_count
    @counts['Drafts'] = @tag.works.unposted.count
    @counts['Bookmarks'] = @tag.visible_bookmarks_count
    @counts['Private Bookmarks'] = @tag.bookmarks.not_public.count
    @counts['External Works'] = @tag.visible_external_works_count
    @counts['Taggings Count'] = @tag.taggings_count

    @parents = @tag.parents.order(:name).group_by { |tag| tag[:type] }
    @parents['MetaTag'] = @tag.direct_meta_tags.by_name
    @children = @tag.children.order(:name).group_by { |tag| tag[:type] }
    @children['SubTag'] = @tag.direct_sub_tags.by_name
    @children['Merger'] = @tag.mergers.by_name

    if @tag.respond_to?(:wranglers)
      @wranglers = @tag.canonical ? @tag.wranglers : (@tag.merger ? @tag.merger.wranglers : [])
    elsif @tag.respond_to?(:fandoms) && !@tag.fandoms.empty?
      @wranglers = @tag.fandoms.collect(&:wranglers).flatten.uniq
    end
    @suggested_fandoms = @tag.suggested_parent_tags("Fandom") - @tag.fandoms if @tag.respond_to?(:fandoms)
  end

  def update
    # update everything except for the synonym,
    # so that the associations are there to move when the synonym is created
    syn_string = params[:tag].delete(:syn_string)
    new_tag_type = params[:tag].delete(:type)

    # Limiting the conditions under which you can update the tag type
    if @tag.can_change_type? && %w(Fandom Character Relationship Freeform UnsortedTag).include?(new_tag_type)
      @tag = @tag.recategorize(new_tag_type)
    end

    unless params[:tag].empty?
      @tag.attributes = tag_params
    end

    @tag.syn_string = syn_string if @tag.errors.empty? && @tag.save

    if @tag.errors.empty? && @tag.save
      flash[:notice] = ts('Tag was updated.')
      redirect_to edit_tag_path(@tag)
    else
      @parents = @tag.parents.order(:name).group_by { |tag| tag[:type] }
      @parents['MetaTag'] = @tag.direct_meta_tags.by_name
      @children = @tag.children.order(:name).group_by { |tag| tag[:type] }
      @children['SubTag'] = @tag.direct_sub_tags.by_name
      @children['Merger'] = @tag.mergers.by_name

      render :edit
    end
  end

  def wrangle
    @page_subtitle = ts('%{tag_name} - Wrangle', tag_name: @tag.name)
    @counts = {}
    @tag.child_types.map { |t| t.underscore.pluralize.to_sym }.each do |tag_type|
      @counts[tag_type] = @tag.send(tag_type).count
    end

    show = params[:show]
    if %w(fandoms characters relationships freeforms sub_tags mergers).include?(show)
      params[:sort_column] = 'name' unless valid_sort_column(params[:sort_column], 'tag')
      params[:sort_direction] = 'ASC' unless valid_sort_direction(params[:sort_direction])
      sort = params[:sort_column] + ' ' + params[:sort_direction]
      # add a secondary sorting key when the main one is not discerning enough
      if sort.include?('suggested') || sort.include?('taggings_count_cache')
        sort += ', name ASC'
      end
      # this makes sure params[:status] is safe
      status = params[:status]
      if %w(unfilterable canonical synonymous unwrangleable).include?(status)
        @tags = @tag.send(show).order(sort).send(status).paginate(page: params[:page], per_page: ArchiveConfig.ITEMS_PER_PAGE)
      elsif status == 'unwrangled'
        @tags = @tag.unwrangled_tags(
          params[:show].singularize.camelize,
          params.permit!.slice(:sort_column, :sort_direction, :page)
        )
      else
        @tags = @tag.send(show).order(sort).paginate(page: params[:page], per_page: ArchiveConfig.ITEMS_PER_PAGE)
      end
    end
  end

  def mass_update
    params[:page] = '1' if params[:page].blank?
    params[:sort_column] = 'name' unless valid_sort_column(params[:sort_column], 'tag')
    params[:sort_direction] = 'ASC' unless valid_sort_direction(params[:sort_direction])
    options = { show: params[:show], page: params[:page], sort_column: params[:sort_column], sort_direction: params[:sort_direction], status: params[:status] }

    error_messages = []
    notice_messages = []

    # make tags canonical if allowed
    if params[:canonicals].present? && params[:canonicals].is_a?(Array)
      saved_canonicals = []
      not_saved_canonicals = []
      tags = Tag.where(id: params[:canonicals])

      tags.each do |tag_to_canonicalize|
        if tag_to_canonicalize.update(canonical: true)
          saved_canonicals << tag_to_canonicalize
        else
          not_saved_canonicals << tag_to_canonicalize
        end
      end

      error_messages << ts('The following tags couldn\'t be made canonical: %{tags_not_saved}', tags_not_saved: not_saved_canonicals.collect(&:name).join(', ')) unless not_saved_canonicals.empty?
      notice_messages << ts('The following tags were successfully made canonical: %{tags_saved}', tags_saved: saved_canonicals.collect(&:name).join(', ')) unless saved_canonicals.empty?
    end

    # remove associated tags
    if params[:remove_associated].present? && params[:remove_associated].is_a?(Array)
      saved_removed_associateds = []
      not_saved_removed_associateds = []
      tags = Tag.where(id: params[:remove_associated])

      tags.each do |tag_to_remove|
        if @tag.remove_association(tag_to_remove.id)
          saved_removed_associateds << tag_to_remove
        else
          not_saved_removed_associateds << tag_to_remove
        end
      end

      error_messages << ts('The following tags couldn\'t be removed: %{tags_not_saved}', tags_not_saved: not_saved_removed_associateds.collect(&:name).join(', ')) unless not_saved_removed_associateds.empty?
      notice_messages << ts('The following tags were successfully removed: %{tags_saved}', tags_saved: saved_removed_associateds.collect(&:name).join(', ')) unless saved_removed_associateds.empty?
    end

    # wrangle to fandom(s)
    if params[:fandom_string].blank? && params[:selected_tags].is_a?(Array) && !params[:selected_tags].empty?
      error_messages << ts('There were no Fandom tags!')
    end
    if params[:fandom_string].present? && params[:selected_tags].is_a?(Array) && !params[:selected_tags].empty?
      canonical_fandoms = []
      noncanonical_fandom_names = []
      fandom_names = params[:fandom_string].split(',').map(&:squish)

      fandom_names.each do |fandom_name|
        if (fandom = Fandom.find_by_name(fandom_name)).try(:canonical?)
          canonical_fandoms << fandom
        else
          noncanonical_fandom_names << fandom_name
        end
      end

      if canonical_fandoms.present?
        saved_to_fandoms = Tag.where(id: params[:selected_tags])

        saved_to_fandoms.each do |tag_to_wrangle|
          canonical_fandoms.each do |fandom|
            tag_to_wrangle.add_association(fandom)
          end
        end

        canonical_fandom_names = canonical_fandoms.collect(&:name)
        options[:fandom_string] = canonical_fandom_names.join(',')
        notice_messages << ts('The following tags were successfully wrangled to %{canonical_fandoms}: %{tags_saved}', canonical_fandoms: canonical_fandom_names.join(', '), tags_saved: saved_to_fandoms.collect(&:name).join(', ')) unless saved_to_fandoms.empty?
      end

      if noncanonical_fandom_names.present?
        error_messages << ts('The following names are not canonical fandoms: %{noncanonical_fandom_names}.', noncanonical_fandom_names: noncanonical_fandom_names.join(', '))
      end
    end

    flash[:notice] = notice_messages.join('<br />').html_safe unless notice_messages.empty?
    flash[:error] = error_messages.join('<br />').html_safe unless error_messages.empty?

    redirect_to url_for({ controller: :tags, action: :wrangle, id: params[:id] }.merge(options))
  end

  private

  def tag_params
    params.require(:tag).permit(
      :name, :type, :canonical, :unwrangleable, :adult, :sortable_name,
      :meta_tag_string, :sub_tag_string, :merger_string, :syn_string,
      :media_string, :fandom_string, :character_string, :relationship_string,
      :freeform_string,
      associations_to_remove: []
    )
  end

  def tag_search_params
    params.require(:tag_search).permit(
      :query,
      :name,
      :fandoms,
      :type,
      :canonical,
      :created_at,
      :uses,
      :sort_column,
      :sort_direction
    )
  end
end