otwcode/otwarchive

View on GitHub
app/controllers/works_controller.rb

Summary

Maintainability
F
1 wk
Test Coverage
# encoding=utf-8

class WorksController < ApplicationController
  # only registered users and NOT admin should be able to create new works
  before_action :load_collection
  before_action :load_owner, only: [:index]
  before_action :users_only, except: [:index, :show, :navigate, :search, :collected, :edit_tags, :update_tags, :drafts, :share]
  before_action :check_user_status, except: [:index, :edit, :edit_multiple, :confirm_delete_multiple, :delete_multiple, :confirm_delete, :destroy, :show, :show_multiple, :navigate, :search, :collected, :share]
  before_action :check_user_not_suspended, only: [:edit, :confirm_delete, :destroy, :show_multiple, :edit_multiple, :confirm_delete_multiple, :delete_multiple]
  before_action :load_work, except: [:new, :create, :import, :index, :show_multiple, :edit_multiple, :update_multiple, :delete_multiple, :search, :drafts, :collected]
  # this only works to check ownership of a SINGLE item and only if load_work has happened beforehand
  before_action :check_ownership, except: [:index, :show, :navigate, :new, :create, :import, :show_multiple, :edit_multiple, :edit_tags, :update_tags, :update_multiple, :delete_multiple, :search, :mark_for_later, :mark_as_read, :drafts, :collected, :share]
  # admins should have the ability to edit tags (:edit_tags, :update_tags) as per our ToS
  before_action :check_ownership_or_admin, only: [:edit_tags, :update_tags]
  before_action :log_admin_activity, only: [:update_tags]
  before_action :check_visibility, only: [:show, :navigate, :share]

  before_action :load_first_chapter, only: [:show, :edit, :update, :preview]

  cache_sweeper :collection_sweeper
  cache_sweeper :feed_sweeper

  skip_before_action :store_location, only: [:share]

  # we want to extract the countable params from work_search and move them into their fields
  def clean_work_search_params
    QueryCleaner.new(work_search_params || {}).clean
  end

  def search
    @languages = Language.default_order
    options = params[:work_search].present? ? clean_work_search_params : {}
    options[:page] = params[:page] if params[:page].present?
    options[:show_restricted] = current_user.present? || logged_in_as_admin?
    @search = WorkSearchForm.new(options)
    @page_subtitle = ts("Search Works")

    if params[:work_search].present? && params[:edit_search].blank?
      if @search.query.present?
        @page_subtitle = ts("Works Matching '%{query}'", query: @search.query)
      end

      @works = @search.search_results.scope(:for_blurb)
      set_own_works
      flash_search_warnings(@works)
      render 'search_results'
    end
  end

  # GET /works
  def index
    base_options = {
      page: params[:page] || 1,
      show_restricted: current_user.present? || logged_in_as_admin?
    }

    options = params[:work_search].present? ? clean_work_search_params : {}

    if params[:fandom_id] || (@collection.present? && @tag.present?)
      if params[:fandom_id].present?
        @fandom = Fandom.find_by(id: params[:fandom_id])
      end

      tag = @fandom || @tag
      options[:filter_ids] ||= []
      options[:filter_ids] << tag.id
    end

    if params[:include_work_search].present?
      params[:include_work_search].keys.each do |key|
        options[key] ||= []
        options[key] << params[:include_work_search][key]
        options[key].flatten!
      end
    end

    if params[:exclude_work_search].present?
      params[:exclude_work_search].keys.each do |key|
        options[:excluded_tag_ids] ||= []
        options[:excluded_tag_ids] << params[:exclude_work_search][key]
        options[:excluded_tag_ids].flatten!
      end
    end

    options.merge!(base_options)
    @page_subtitle = index_page_title

    if logged_in? && @tag
      @favorite_tag = @current_user.favorite_tags
                                   .where(tag_id: @tag.id).first ||
                      FavoriteTag
                      .new(tag_id: @tag.id, user_id: @current_user.id)
    end

    if @owner.present?
      @search = WorkSearchForm.new(options.merge(faceted: true, works_parent: @owner))
      # If we're using caching we'll try to get the results from cache
      # Note: we only cache some first initial number of pages since those are biggest bang for
      # the buck -- users don't often go past them
      if use_caching? && params[:work_search].blank? && params[:fandom_id].blank? &&
         params[:include_work_search].blank? && params[:exclude_work_search].blank? &&
         (params[:page].blank? || params[:page].to_i <= ArchiveConfig.PAGES_TO_CACHE)
        # the subtag is for eg collections/COLL/tags/TAG
        subtag = @tag.present? && @tag != @owner ? @tag : nil
        user = logged_in? || logged_in_as_admin? ? 'logged_in' : 'logged_out'
        @works = Rails.cache.fetch("#{@owner.works_index_cache_key(subtag)}_#{user}_page#{params[:page]}_true", expires_in: ArchiveConfig.SECONDS_UNTIL_WORK_INDEX_EXPIRE.seconds) do
          results = @search.search_results.scope(:for_blurb)
          # calling this here to avoid frozen object errors
          results.items
          results.facets
          results
        end
      else
        @works = @search.search_results.scope(:for_blurb)
      end

      flash_search_warnings(@works)

      @facets = @works.facets
      if @search.options[:excluded_tag_ids].present?
        tags = Tag.where(id: @search.options[:excluded_tag_ids])
        tags.each do |tag|
          @facets[tag.class.to_s.underscore] ||= []
          @facets[tag.class.to_s.underscore] << QueryFacet.new(tag.id, tag.name, 0)
        end
      end
    elsif use_caching?
      @works = Rails.cache.fetch('works/index/latest/v1', expires_in: ArchiveConfig.SECONDS_UNTIL_WORK_INDEX_EXPIRE.seconds) do
        Work.latest.for_blurb.to_a
      end
    else
      @works = Work.latest.for_blurb.to_a
    end
    set_own_works
  end

  def collected
    options = params[:work_search].present? ? clean_work_search_params : {}
    options[:page] = params[:page] || 1
    options[:show_restricted] = current_user.present? || logged_in_as_admin?

    @user = User.find_by!(login: params[:user_id])
    @search = WorkSearchForm.new(options.merge(works_parent: @user, collected: true))
    @works = @search.search_results.scope(:for_blurb)
    flash_search_warnings(@works)
    @facets = @works.facets
    set_own_works
    @page_subtitle = ts('%{username} - Collected Works', username: @user.login)
  end

  def drafts
    unless params[:user_id] && (@user = User.find_by(login: params[:user_id]))
      flash[:error] = ts('Whose drafts did you want to look at?')
      redirect_to users_path
      return
    end

    unless current_user == @user || logged_in_as_admin?
      flash[:error] = ts('You can only see your own drafts, sorry!')
      redirect_to logged_in? ? user_path(current_user) : new_user_session_path
      return
    end

    if params[:pseud_id]
      @pseud = @user.pseuds.find_by(name: params[:pseud_id])
      @works = @pseud.unposted_works.for_blurb.paginate(page: params[:page])
    else
      @works = @user.unposted_works.for_blurb.paginate(page: params[:page])
    end
  end

  # GET /works/1
  # GET /works/1.xml
  def show
    @tag_groups = @work.tag_groups
    if @work.unrevealed?
      @page_title = ts("Mystery Work")
    else
      page_creator = if @work.anonymous?
                       ts("Anonymous")
                     else
                       @work.pseuds.map(&:byline).sort.join(", ")
                     end
      fandoms = @tag_groups["Fandom"]
      page_title_inner = if fandoms.size > 3
                           ts("Multifandom")
                         else
                           fandoms.empty? ? ts("No fandom specified") : fandoms[0].name
                         end
      @page_title = get_page_title(page_title_inner, page_creator, @work.title)
    end

    # Users must explicitly okay viewing of adult content
    if params[:view_adult]
      cookies[:view_adult] = "true"
    elsif @work.adult? && !see_adult?
      render('_adult', layout: 'application') && return
    end

    # Users must explicitly okay viewing of entire work
    if @work.chaptered?
      if params[:view_full_work] || (logged_in? && current_user.preference.try(:view_full_works))
        @chapters = @work.chapters_in_order(
          include_drafts: (logged_in_as_admin? ||
                           @work.user_is_owner_or_invited?(current_user))
        )
      else
        flash.keep
        redirect_to([@work, @chapter, { only_path: true }]) && return
      end
    end

    @tag_categories_limited = Tag::VISIBLE - ['ArchiveWarning']
    @kudos = @work.kudos.with_user.includes(:user)

    if current_user.respond_to?(:subscriptions)
      @subscription = current_user.subscriptions.where(subscribable_id: @work.id,
                                                       subscribable_type: 'Work').first ||
                      current_user.subscriptions.build(subscribable: @work)
    end

    render :show
    Reading.update_or_create(@work, current_user) if current_user
  end

  # GET /works/1/share
  def share
    if request.xhr?
      if @work.unrevealed?
        render template: "errors/404", status: :not_found
      else
        render layout: false
      end
    else
      # Avoid getting an unstyled page 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 root_path
      end
    end
  end

  def navigate
    @chapters = @work.chapters_in_order(
      include_content: false,
      include_drafts: (logged_in_as_admin? ||
                       @work.user_is_owner_or_invited?(current_user))
    )
  end

  # GET /works/new
  def new
    @hide_dashboard = true
    @unposted = current_user.unposted_work

    if params[:load_unposted] && @unposted
      @work = @unposted
      @chapter = @work.first_chapter
    else
      @work = Work.new
      @chapter = @work.chapters.build
    end

    # for clarity, add the collection and recipient
    if params[:assignment_id] && (@challenge_assignment = ChallengeAssignment.find(params[:assignment_id])) && @challenge_assignment.offering_user == current_user
      @work.challenge_assignments << @challenge_assignment
    end

    if params[:claim_id] && (@challenge_claim = ChallengeClaim.find(params[:claim_id])) && User.find(@challenge_claim.claiming_user_id) == current_user
      @work.challenge_claims << @challenge_claim
    end

    if @collection
      @work.add_to_collection(@collection)
    end

    @work.set_challenge_info
    @work.set_challenge_claim_info
    set_work_form_fields

    if params[:import]
      @page_subtitle = ts("Import New Work")
      render(:new_import)
    elsif @work.persisted?
      render(:edit)
    else
      render(:new)
    end
  end

  # POST /works
  def create
    if params[:cancel_button]
      flash[:notice] = ts('New work posting canceled.')
      redirect_to current_user
      return
    end

    @work = Work.new(work_params)

    @chapter = @work.first_chapter
    @chapter.attributes = work_params[:chapter_attributes] if work_params[:chapter_attributes]
    @work.ip_address = request.remote_ip

    @work.set_challenge_info
    @work.set_challenge_claim_info
    set_work_form_fields

    # If Edit or Cancel is pressed, bail out and display relevant form
    if params[:edit_button] || work_cannot_be_saved?
      render :new
    else
      @work.posted = @chapter.posted = true if params[:post_button]
      @work.set_revised_at_by_chapter(@chapter)

      if @work.save
        if params[:preview_button]
          flash[:notice] = ts("Draft was successfully created. It will be <strong>scheduled for deletion</strong> on %{deletion_date}.", deletion_date: view_context.date_in_zone(@work.created_at + 29.days)).html_safe
          in_moderated_collection
          redirect_to preview_work_path(@work)
        else
          # We check here to see if we are attempting to post to moderated collection
          flash[:notice] = ts("Work was successfully posted. It should appear in work listings within the next few minutes.")
          in_moderated_collection
          redirect_to work_path(@work)
        end
      else
        render :new
      end
    end
  end

  # GET /works/1/edit
  def edit
    @hide_dashboard = true
    if @work.number_of_chapters > 1
      @chapters = @work.chapters_in_order(include_content: false,
                                          include_drafts: true)
    end
    set_work_form_fields

    return unless params['remove'] == 'me'

    pseuds_with_author_removed = @work.pseuds - current_user.pseuds

    if pseuds_with_author_removed.empty?
      redirect_to controller: 'orphans', action: 'new', work_id: @work.id
    else
      @work.remove_author(current_user)
      flash[:notice] = ts("You have been removed as a creator from the work.")
      redirect_to current_user
    end
  end

  # GET /works/1/edit_tags
  def edit_tags
    authorize @work if logged_in_as_admin?
    @page_subtitle = ts("Edit Work Tags")
  end

  # PUT /works/1
  def update
    if params[:cancel_button]
      return cancel_posting_and_redirect
    end

    @work.preview_mode = !!(params[:preview_button] || params[:edit_button])
    @work.attributes = work_params
    @chapter.attributes = work_params[:chapter_attributes] if work_params[:chapter_attributes]
    @work.ip_address = request.remote_ip
    @work.set_word_count(@work.preview_mode)

    @work.set_challenge_info
    @work.set_challenge_claim_info
    set_work_form_fields

    if params[:edit_button] || work_cannot_be_saved?
      render :edit
    elsif params[:preview_button]
      unless @work.posted?
        flash[:notice] = ts("Your changes have not been saved. Please post your work or save as draft if you want to keep them.")
      end

      in_moderated_collection
      @preview_mode = true
      render :preview
    else
      @work.posted = @chapter.posted = true if params[:post_button]
      @work.set_revised_at_by_chapter(@chapter)
      posted_changed = @work.posted_changed?

      @work.minor_version = @work.minor_version + 1
      if @chapter.save && @work.save
        flash[:notice] = ts("Work was successfully #{posted_changed ? 'posted' : 'updated'}.")
        if posted_changed
          flash[:notice] << ts(" It should appear in work listings within the next few minutes.")
        end
        in_moderated_collection
        redirect_to work_path(@work)
      else
        @chapter.errors.full_messages.each { |err| @work.errors.add(:base, err) }
        render :edit
      end
    end
  end

  def update_tags
    authorize @work if logged_in_as_admin?
    if params[:cancel_button]
      return cancel_posting_and_redirect
    end

    @work.preview_mode = !!(params[:preview_button] || params[:edit_button])
    @work.attributes = work_tag_params

    if params[:edit_button] || work_cannot_be_saved?
      render :edit_tags
    elsif params[:preview_button]
      @preview_mode = true
      render :preview_tags
    elsif params[:save_button]
      @work.save
      flash[:notice] = ts('Tags were successfully updated.')
      redirect_to(@work)
    else # Save As Draft
      @work.posted = true
      @work.minor_version = @work.minor_version + 1
      @work.save
      flash[:notice] = ts('Work was successfully updated.')
      redirect_to(@work)
    end
  end

  # GET /works/1/preview
  def preview
    @preview_mode = true
  end

  def preview_tags
    @preview_mode = true
  end

  def confirm_delete
  end

  # DELETE /works/1
  def destroy
    @work = Work.find(params[:id])

    begin
      was_draft = !@work.posted?
      title = @work.title
      @work.destroy
      flash[:notice] = ts("Your work %{title} was deleted.", title: title).html_safe
    rescue
      flash[:error] = ts("We couldn't delete that right now, sorry! Please try again later.")
    end

    if was_draft
      redirect_to drafts_user_works_path(current_user)
    else
      redirect_to user_works_path(current_user)
    end
  end

  # POST /works/import
  def import
    # check to make sure we have some urls to work with
    @urls = params[:urls].split
    if @urls.empty?
      flash.now[:error] = ts('Did you want to enter a URL?')
      render(:new_import) && return
    end

    @language_id = params[:language_id]
    if @language_id.empty?
      flash.now[:error] = ts("Language cannot be blank.")
      render(:new_import) && return
    end

    importing_for_others = params[:importing_for_others] != "false" && params[:importing_for_others]

    # is external author information entered when import for others is not checked?
    if (params[:external_author_name].present? || params[:external_author_email].present?) && !importing_for_others
      flash.now[:error] = ts('You have entered an external author name or e-mail address but did not select "Import for others." Please select the "Import for others" option or remove the external author information to continue.')
      render(:new_import) && return
    end

    # is this an archivist importing?
    if importing_for_others && !current_user.archivist
      flash.now[:error] = ts('You may not import stories by other users unless you are an approved archivist.')
      render(:new_import) && return
    end

    # make sure we're not importing too many at once
    if params[:import_multiple] == 'works' && (!current_user.archivist && @urls.length > ArchiveConfig.IMPORT_MAX_WORKS || @urls.length > ArchiveConfig.IMPORT_MAX_WORKS_BY_ARCHIVIST)
      flash.now[:error] = ts('You cannot import more than %{max} works at a time.', max: current_user.archivist ? ArchiveConfig.IMPORT_MAX_WORKS_BY_ARCHIVIST : ArchiveConfig.IMPORT_MAX_WORKS)
      render(:new_import) && return
    elsif params[:import_multiple] == 'chapters' && @urls.length > ArchiveConfig.IMPORT_MAX_CHAPTERS
      flash.now[:error] = ts('You cannot import more than %{max} chapters at a time.', max: ArchiveConfig.IMPORT_MAX_CHAPTERS)
      render(:new_import) && return
    end

    options = build_options(params)
    options[:ip_address] = request.remote_ip

    # now let's do the import
    if params[:import_multiple] == 'works' && @urls.length > 1
      import_multiple(@urls, options)
    else # a single work possibly with multiple chapters
      import_single(@urls, options)
    end
  end

  protected

  # import a single work (possibly with multiple chapters)
  def import_single(urls, options)
    # try the import
    storyparser = StoryParser.new

    begin
      @work = if urls.size == 1
                storyparser.download_and_parse_story(urls.first, options)
              else
                storyparser.download_and_parse_chapters_into_story(urls, options)
              end
    rescue Timeout::Error
      flash.now[:error] = ts('Import has timed out. This may be due to connectivity problems with the source site. Please try again in a few minutes, or check Known Issues to see if there are import problems with this site.')
      render(:new_import) && return
    rescue StoryParser::Error => exception
      flash.now[:error] = ts("We couldn't successfully import that work, sorry: %{message}", message: exception.message)
      render(:new_import) && return
    end

    unless @work && @work.save
      flash.now[:error] = ts("We were only partially able to import this work and couldn't save it. Please review below!")
      @chapter = @work.chapters.first
      @series = current_user.series.distinct
      render(:new) && return
    end

    # Otherwise, we have a saved work, go us
    send_external_invites([@work])
    @chapter = @work.first_chapter if @work
    if @work.posted
      redirect_to(work_path(@work)) && return
    else
      redirect_to(preview_work_path(@work)) && return
    end
  end

  # import multiple works
  def import_multiple(urls, options)
    # try a multiple import
    storyparser = StoryParser.new
    @works, failed_urls, errors = storyparser.import_from_urls(urls, options)

    # collect the errors neatly, matching each error to the failed url
    unless failed_urls.empty?
      error_msgs = 0.upto(failed_urls.length).map { |index| "<dt>#{failed_urls[index]}</dt><dd>#{errors[index]}</dd>" }.join("\n")
      flash.now[:error] = "<h3>#{ts('Failed Imports')}</h3><dl>#{error_msgs}</dl>".html_safe
    end

    # if EVERYTHING failed, boo. :( Go back to the import form.
    render(:new_import) && return if @works.empty?

    # if we got here, we have at least some successfully imported works
    flash[:notice] = ts('Importing completed successfully for the following works! (But please check the results over carefully!)')
    send_external_invites(@works)

    # fall through to import template
  end

  # if we are importing for others, we need to send invitations
  def send_external_invites(works)
    return unless params[:importing_for_others]

    @external_authors = works.collect(&:external_authors).flatten.uniq
    unless @external_authors.empty?
      @external_authors.each do |external_author|
        external_author.find_or_invite(current_user)
      end
      message = ' ' + ts('We have notified the author(s) you imported works for. If any were missed, you can also add co-authors manually.')
      flash[:notice] ? flash[:notice] += message : flash[:notice] = message
    end
  end

  # check to see if the work is being added / has been added to a moderated collection, then let user know that
  def in_moderated_collection
    moderated_collections = []
    @work.collections.each do |collection|
      next unless !collection.nil? && collection.moderated? && !collection.user_is_posting_participant?(current_user)
      next unless @work.collection_items.present?
      @work.collection_items.each do |collection_item|
        next unless collection_item.collection == collection
        if collection_item.approved_by_user? && collection_item.unreviewed_by_collection?
          moderated_collections << collection
        end
      end
    end
    if moderated_collections.present?
      flash[:notice] ||= ''
      flash[:notice] += ts(" You have submitted your work to #{moderated_collections.size > 1 ? 'moderated collections (%{all_collections}). It will not become a part of those collections' : "the moderated collection '%{all_collections}'. It will not become a part of the collection"} until it has been approved by a moderator.", all_collections: moderated_collections.map(&:title).join(', '))
    end
  end

  public

  def post_draft
    @user = current_user
    @work = Work.find(params[:id])

    unless @user.is_author_of?(@work)
      flash[:error] = ts('You can only post your own works.')
      redirect_to(current_user) && return
    end

    if @work.posted
      flash[:error] = ts('That work is already posted. Do you want to edit it instead?')
      redirect_to(edit_user_work_path(@user, @work)) && return
    end

    @work.posted = true
    @work.minor_version = @work.minor_version + 1

    unless @work.valid? && @work.save
      flash[:error] = ts('There were problems posting your work.')
      redirect_to(edit_user_work_path(@user, @work)) && return
    end

    # AO3-3498: since a work's word count is calculated in a before_save and the chapter is posted in an after_save,
    # work's word count needs to be updated with the chapter's word count after the chapter is posted
    @work.set_word_count
    @work.save

    if !@collection.nil? && @collection.moderated?
      redirect_to work_path(@work), notice: ts('Work was submitted to a moderated collection. It will show up in the collection once approved.')
    else
      flash[:notice] = ts('Your work was successfully posted.')
      redirect_to @work
    end
  end

  # WORK ON MULTIPLE WORKS

  def show_multiple
    @page_subtitle = ts("Edit Multiple Works")
    @user = current_user

    if params[:pseud_id]
      @works = Work.joins(:pseuds).where(pseud_id: params[:pseud_id])
    else
      @works = Work.joins(pseuds: :user).where('users.id = ?', @user.id)
    end

    @works = @works.where(id: params[:work_ids]) if params[:work_ids]

    @works_by_fandom = @works.joins(:taggings)
                             .joins("inner join tags on taggings.tagger_id = tags.id AND tags.type = 'Fandom'")
                             .select('distinct tags.name as fandom, works.id, works.title, works.posted').group_by(&:fandom)
  end

  def edit_multiple
    if params[:commit] == 'Orphan'
      redirect_to(new_orphan_path(work_ids: params[:work_ids])) && return
    end

    @page_subtitle = ts("Edit Multiple Works")
    @user = current_user
    @works = Work.select('distinct works.*').joins(pseuds: :user).where('users.id = ?', @user.id).where(id: params[:work_ids])

    render('confirm_delete_multiple') && return if params[:commit] == 'Delete'
  end

  def confirm_delete_multiple
    @user = current_user
    @works = Work.select('distinct works.*').joins(pseuds: :user).where('users.id = ?', @user.id).where(id: params[:work_ids])
  end

  def delete_multiple
    @user = current_user
    @works = Work.joins(pseuds: :user).where('users.id = ?', @user.id).where(id: params[:work_ids]).readonly(false)
    titles = @works.collect(&:title)

    @works.each(&:destroy)

    flash[:notice] = ts('Your works %{titles} were deleted.', titles: titles.join(', '))
    redirect_to show_multiple_user_works_path(@user)
  end

  def update_multiple
    @user = current_user
    @works = Work.joins(pseuds: :user).where('users.id = ?', @user.id).where(id: params[:work_ids]).readonly(false)
    @errors = []

    # To avoid overwriting, we entirely trash any blank fields.
    updated_work_params = work_params.reject { |_key, value| value.blank? }

    @works.each do |work|
      # now we can just update each work independently, woo!
      unless work.update(updated_work_params)
        @errors << ts('The work %{title} could not be edited: %{error}', title: work.title, error: work.errors.full_messages.join(" ")).html_safe
      end

      if params[:remove_me]
        if work.pseuds.where.not(user_id: current_user.id).exists?
          work.remove_author(current_user)
        else
          @errors << ts("You cannot remove yourself as co-creator of the work %{title} because you are the only listed creator. If you have invited another co-creator, you must wait for them to accept before you can remove yourself.", title: work.title)
        end
      end
    end

    if @errors.empty?
      flash[:notice] = ts('Your edits were put through! Please check over the works to make sure everything is right.')
    else
      flash[:error] = @errors
    end

    redirect_to show_multiple_user_works_path(@user, work_ids: @works.map(&:id))
  end

  # marks a work to read later
  def mark_for_later
    @work = Work.find(params[:id])
    Reading.mark_to_read_later(@work, current_user, true)
    read_later_path = user_readings_path(current_user, show: 'to-read')
    if @work.marked_for_later?(current_user)
      flash[:notice] = ts("This work was added to your #{view_context.link_to('Marked for Later list', read_later_path)}.").html_safe
    end
    redirect_to(request.env['HTTP_REFERER'] || root_path)
  end

  def mark_as_read
    @work = Work.find(params[:id])
    Reading.mark_to_read_later(@work, current_user, false)
    read_later_path = user_readings_path(current_user, show: 'to-read')
    unless @work.marked_for_later?(current_user)
      flash[:notice] = ts("This work was removed from your #{view_context.link_to('Marked for Later list', read_later_path)}.").html_safe
    end
    redirect_to(request.env['HTTP_REFERER'] || root_path)
  end

  protected

  def load_owner
    if params[:user_id].present?
      @user = User.find_by!(login: params[:user_id])
      if params[:pseud_id].present?
        @pseud = @user.pseuds.find_by(name: params[:pseud_id])
      end
    end
    if params[:tag_id]
      @tag = Tag.find_by_name(params[:tag_id])
      unless @tag && @tag.is_a?(Tag)
        raise ActiveRecord::RecordNotFound, "Couldn't find tag named '#{params[:tag_id]}'"
      end
      unless @tag.canonical?
        if @tag.merger.present?
          if @collection.present?
            redirect_to(collection_tag_works_path(@collection, @tag.merger)) && return
          else
            redirect_to(tag_works_path(@tag.merger)) && return
          end
        else
          redirect_to(tag_path(@tag)) && return
        end
      end
    end
    @language = Language.find_by(short: params[:language_id]) if params[:language_id].present?
    @owner = @pseud || @user || @collection || @tag || @language
  end

  def load_work
    @work = Work.find_by(id: params[:id])
    unless @work
      raise ActiveRecord::RecordNotFound, "Couldn't find work with id '#{params[:id]}'"
    end
    if @collection && !@work.collections.include?(@collection)
      redirect_to(@work) && return
    end

    @check_ownership_of = @work
    @check_visibility_of = @work
  end

  def load_first_chapter
    @chapter = if @work.user_is_owner_or_invited?(current_user) || logged_in_as_admin?
                 @work.first_chapter
               else
                 @work.chapters.in_order.posted.first
               end
  end

  # Check whether we should display :new or :edit instead of previewing or
  # saving the user's changes.
  def work_cannot_be_saved?
    !(@work.errors.empty? && @work.valid?)
  end

  def set_work_form_fields
    @work.reset_published_at(@chapter)
    @series = current_user.series.distinct
    @serial_works = @work.serial_works

    if @collection.nil?
      @collection = @work.approved_collections.first
    end

    if params[:claim_id]
      @posting_claim = ChallengeClaim.find_by(id: params[:claim_id])
    end
  end

  def set_own_works
    return unless @works
    @own_works = []
    if current_user.is_a?(User)
      pseud_ids = current_user.pseuds.pluck(:id)
      @own_works = @works.select do |work|
        (pseud_ids & work.pseuds.pluck(:id)).present?
      end
    end
  end

  def cancel_posting_and_redirect
    if @work && @work.posted
      flash[:notice] = ts('The work was not updated.')
      redirect_to user_works_path(current_user)
    else
      flash[:notice] = ts('The work was not posted. It will be saved here in your drafts for one month, then deleted from the Archive.')
      redirect_to drafts_user_works_path(current_user)
    end
  end

  def index_page_title
    if @owner.present?
      owner_name =
        case @owner.class.to_s
        when 'Pseud'
          @owner.name
        when 'User'
          @owner.login
        when 'Collection'
          @owner.title
        else
          @owner.try(:name)
        end

      "#{owner_name} - Works".html_safe
    else
      'Latest Works'
    end
  end

  def log_admin_activity
    if logged_in_as_admin?
      options = { action: params[:action] }

      if params[:action] == 'update_tags'
        summary = "Old tags: #{@work.tags.pluck(:name).join(', ')}"
      end

      AdminActivity.log_action(current_admin, @work, action: params[:action], summary: summary)
    end
  end

  private

  def build_options(params)
    pseuds_to_apply =
      (Pseud.find_by(name: params[:pseuds_to_apply]) if params[:pseuds_to_apply])

    {
      pseuds: pseuds_to_apply,
      post_without_preview: params[:post_without_preview],
      importing_for_others: params[:importing_for_others],
      restricted: params[:restricted],
      moderated_commenting_enabled: params[:moderated_commenting_enabled],
      comment_permissions: params[:comment_permissions],
      override_tags: params[:override_tags],
      detect_tags: params[:detect_tags] == "true",
      fandom: params[:work][:fandom_string],
      archive_warning: params[:work][:archive_warning_strings],
      character: params[:work][:character_string],
      rating: params[:work][:rating_string],
      relationship: params[:work][:relationship_string],
      category: params[:work][:category_strings],
      freeform: params[:work][:freeform_string],
      notes: params[:notes],
      encoding: params[:encoding],
      external_author_name: params[:external_author_name],
      external_author_email: params[:external_author_email],
      external_coauthor_name: params[:external_coauthor_name],
      external_coauthor_email: params[:external_coauthor_email],
      language_id: params[:language_id]
    }
  end

  def work_params
    params.require(:work).permit(
      :rating_string, :fandom_string, :relationship_string, :character_string,
      :archive_warning_string, :category_string,
      :freeform_string, :summary, :notes, :endnotes, :collection_names, :recipients, :wip_length,
      :backdate, :language_id, :work_skin_id, :restricted, :comment_permissions,
      :moderated_commenting_enabled, :title, :pseuds_to_add, :collections_to_add,
      current_user_pseud_ids: [],
      collections_to_remove: [],
      challenge_assignment_ids: [],
      challenge_claim_ids: [],
      category_strings: [],
      archive_warning_strings: [],
      author_attributes: [:byline, ids: [], coauthors: []],
      series_attributes: [:id, :title],
      parent_work_relationships_attributes: [
        :url, :title, :author, :language_id, :translation
      ],
      chapter_attributes: [
        :title, :"published_at(3i)", :"published_at(2i)", :"published_at(1i)",
        :published_at, :content
      ]
    )
  end

  def work_tag_params
    params.require(:work).permit(
      :rating_string, :fandom_string, :relationship_string, :character_string,
      :archive_warning_string, :category_string, :freeform_string, :language_id,
      category_strings: [],
      archive_warning_strings: []
    )
  end

  def work_search_params
    params.require(:work_search).permit(
      :query,
      :title,
      :creators,
      :revised_at,
      :complete,
      :single_chapter,
      :word_count,
      :language_id,
      :fandom_names,
      :rating_ids,
      :character_names,
      :relationship_names,
      :freeform_names,
      :hits,
      :kudos_count,
      :comments_count,
      :bookmarks_count,
      :sort_column,
      :sort_direction,
      :other_tag_names,
      :excluded_tag_names,
      :crossover,
      :date_from,
      :date_to,
      :words_from,
      :words_to,

      archive_warning_ids: [],
      warning_ids: [], # backwards compatibility
      category_ids: [],
      rating_ids: [],
      fandom_ids: [],
      character_ids: [],
      relationship_ids: [],
      freeform_ids: [],

      collection_ids: []
    )
  end

end