fastruby/points

View on GitHub
app/controllers/stories_controller.rb

Summary

Maintainability
A
45 mins
Test Coverage
require "csv"
class StoriesController < ApplicationController
  before_action :authenticate_user!
  before_action :find_project, except: [:bulk_destroy, :render_markdown, :edit, :update, :destroy, :show, :move, :approve, :reject, :pending]
  before_action :find_story, only: [:edit, :update, :destroy, :show, :move, :approve, :reject, :pending]
  before_action :validate_url_product_id, only: [:edit, :update, :destroy, :show, :move]
  before_action :ensure_unarchived!, except: [:show, :bulk_destroy, :render_markdown, :move]

  include ApplicationHelper

  CSV_HEADERS = %w[id title description position]

  def new
    @story = Story.where(id: params[:story_id]).first_or_initialize.dup
  end

  def edit
  end

  def create
    @story = Story.new(stories_params)
    @story.project_id = @project.id

    if @story.save
      flash[:success] = "Story created!"
      redirect_to project_path(@project.id)
    else
      render :new
    end
  end

  def destroy
    @story.destroy
    respond_to do |format|
      format.html { redirect_to project_path(@project), notice: "Story was successfully destroyed." }
      format.json { head :no_content }
    end
  end

  def bulk_destroy
    Story.where(id: params[:ids]).destroy_all
    respond_to do |format|
      format.json { render json: :ok }
    end
  end

  def show
    @estimate = Estimate.find_by(story: @story, user: current_user)
    @comments = @story.comments.includes(:user).order(:created_at)
    @comment = Comment.new
  end

  def update
    if @story.update(stories_params)
      flash[:success] = "Story updated!"
      redirect_to project_path(@project.id)
    else
      flash[:error] = @story.errors.full_messages
      render :edit
    end
  end

  def import
    if !params[:file].original_filename.ends_with?(".csv")
      flash[:error] = "Invalid File: Must be CSV"
      redirect_to(@project) && return
    end
    file = begin
      CSV.parse(params[:file].read, headers: true)
    rescue
      []
    end
    if file.empty?
      flash[:error] = "CSV Error: File is empty or could not be parsed"
      redirect_to(@project) && return
    elsif !expected_csv_headers?(file)
      flash[:error] = "Invalid CSV: Must have headers title,description,position"
      redirect_to(@project) && return
    else
      file.each do |story_csv|
        story = @project.stories.find_by(id: story_csv["id"]) || @project.stories.new
        story.update(title: story_csv["title"], description: story_csv["description"], position: story_csv["position"])
      end
      flash[:success] = "CSV import was successful"
      redirect_to project_path(@project)
    end
  end

  def export
    csv = if params[:export_with_comments] == "1"
      CSV.generate(headers: true) do |csv|
        csv << CSV_HEADERS + ["comment"]
        @project.stories.includes(:comments).approved.by_position.each do |story|
          comments = []
          story.comments.each do |comment|
            comments << "#{display_name(comment.user)}: #{comment.body}"
          end
          csv << [story.id, story.title, story.description, story.position] + comments
        end
      end
    else
      CSV.generate(headers: true) do |csv|
        csv << CSV_HEADERS
        @project.stories.approved.by_position.each do |story|
          csv << story.attributes.slice(*CSV_HEADERS)
        end
      end
    end
    filename = "#{@project.title.gsub(/[^\w]/, "_")}-#{Time.now.to_formatted_s(:short).tr(" ", "_")}.csv"
    send_data csv, filename: filename
  end

  def render_markdown
    render plain: markdown(params[:markdown])
  end

  def move
    @new_project = @project.siblings.find_by(id: params[:to_project])

    if @new_project
      @story.update_attribute(:project_id, @new_project.id)
      flash[:success] = "Story moved!"
    else
      flash[:error] = "Selected project does not exist or is not a sibling."
    end

    redirect_to @project
  end

  def approve
    @story.approved!
    respond_to do |format|
      format.js { render "shared/update_status" }
    end
  end

  def reject
    @story.rejected!
    respond_to do |format|
      format.js { render "shared/update_status" }
    end
  end

  def pending
    @story.pending!
    respond_to do |format|
      format.js { render "shared/update_status" }
    end
  end

  private

  def find_project
    @project = Project.find(params[:project_id])
  end

  def find_story
    @story = Story.find(params[:id] || params[:story_id])
    @project = @story.project
  end

  def validate_url_product_id
    raise ActionController::RoutingError.new("This story was not found for this project") unless params[:project_id] == @project.id.to_s
  end

  def stories_params
    params.require(:story).permit(:title, :description, :extra_info, :project_id, :status)
  end

  def expected_csv_headers?(file)
    ["title", "description", "position"].to_set.subset?(file.headers.to_set)
  end
end