osbridge/openconferenceware

View on GitHub
app/controllers/open_conference_ware/proposals_controller.rb

Summary

Maintainability
D
2 days
Test Coverage
module OpenConferenceWare
  class ProposalsController < ApplicationController

    before_filter :authentication_required, only: [:edit, :update, :destroy, :speaker_confirm, :speaker_decline, :proposal_login_required]
    before_filter :assert_current_event_or_redirect
    before_filter :assert_proposal_status_published, only: [:sessions_index, :sessions_index_terse, :session_show]
    before_filter :assert_schedule_published, only: [:schedule]
    before_filter :normalize_event_path_or_redirect, only: [:index, :sessions_index, :schedule]
    before_filter :assert_anonymous_proposals, only: [:new, :create]
    before_filter :assert_accepting_proposals, only: [:new, :create]
    before_filter :assign_proposal_and_event, only: [:show, :session_show, :edit, :update, :destroy, :speaker_confirm, :speaker_decline]
    before_filter :assert_record_ownership, only: [:edit, :update, :destroy, :speaker_confirm, :speaker_decline]
    before_filter :assert_user_complete_profile, only: [:new, :edit, :update]
    before_filter :assign_proposals_breadcrumb

    MAX_FEED_ITEMS = 50
    SESSION_RELATED_ACTIONS = ['sessions_index', 'session_show', 'schedule']

    def proposals_or_sessions
      if @event && @event.proposal_status_published?
        redirect_to event_sessions_path(@event)
      else
        redirect_to event_proposals_path(@event)
      end
    end

    # GET /proposals
    # GET /proposals.xml
    def index
      warn_about_incomplete_event

      @kind = :proposals

      assign_prefetched_hashes
      @proposals = Defer { @proposals_hash.values }

      unless params[:sort]
        if selector? 
          params[:sort] = "random"
        else
          params[:sort] = "submitted_at"
          params[:dir] = "desc"
        end
      end

      respond_to do |format|
        format.html {
          add_breadcrumb @event.title, event_proposals_path(@event)
        }
        format.xml  {
          render xml: @proposals
        }
        format.json {
          render json: @proposals
        }
        format.atom {
          # index.atom.builder
          if @event_assignment == :assigned_to_param
            @cache_key = "proposals_atom,event_#{@event.id}"
            @proposals = Defer { @event.populated_proposals(:proposals).reorder("submitted_at desc").limit(MAX_FEED_ITEMS) }
          else
            @cache_key = "proposals_atom,all"
            @proposals = Defer { Proposal.populated.reorder("submitted_at desc").limit(MAX_FEED_ITEMS) }
          end
        }
        format.csv {
          records = @event.populated_proposals(@kind).includes(:comments)
          if admin?
            render csv: records, style: :admin
          else
            if schedule_visible?
              render csv: records, style: :schedule
            else
              render csv: records
            end
          end
        }
      end
    end

    def sessions_index
      @kind = :sessions

      assign_prefetched_hashes
      @proposals = Defer { @sessions_hash.values }

      params[:sort] ||= "track"

      respond_to do |format|
        format.html {
          add_breadcrumb @event.title, event_proposals_path(@event)
          render action: "index"
        }
        format.xml  {
          render xml: @proposals
        }
        format.json {
          render json: @proposals
        }
      end
    end

    def sessions_index_terse
      assign_prefetched_hashes
    end

    def schedule
      page_title 'Schedule'

      @schedule = Defer { Schedule.new(@event, admin?) }
      assign_prefetched_hashes

      respond_to do |format|
        format.html {
          @view_cache_key = "schedule,event_#{@event.id},admin_#{admin?}"
        }
        format.json {
          render json: @schedule
        }
        format.xml {
          ### render xml: @schedule
          render xml: {error: "XML output of schedule not yet supported"}, status: :unprocessable_entity
        }
        format.ics {
          view_cache_key = "schedule,event_#{@event.id}.ics"
          data = Rails.cache.fetch(view_cache_key) {
            Proposal.to_icalendar(
              @schedule.items,
              title: "#{@event.title}",
              url_helper: lambda {|item| session_url(item)})
          }
          render text: data
        }
      end
    end

    def session_show
      # @proposal and @event set via #assign_proposal_and_event filter
      @kind = :session
      unless @proposal.confirmed?
        @redirector = lambda {
          flash[:failure] = "This proposal is not a session."
          return redirect_to( proposal_path(@proposal) )
        }
      end
      return base_show
    end

    # GET /proposals/1
    # GET /proposals/1.xml
    def show
      # @proposal and @event set via #assign_proposal_and_event filter
      @kind = :proposal
      if @event.proposal_status_published? && @proposal.confirmed?
        @redirector = lambda {
          # flash[:notice] = "This proposal has been accepted as a session."
          flash.keep
          return redirect_to( session_path(@proposal) )
        }
      end
      return base_show
    end

    # GET /proposals/new
    # GET /proposals/new.xml
    def new
      add_breadcrumb @event.title, event_proposals_path(@event)
      add_breadcrumb "Create a proposal", new_event_proposal_path(@event)

      @proposal = Proposal.new(agreement: false)

      # Set default track if only one was found.
      if event_tracks? && @event.tracks.size == 1
        @proposal.track = @event.tracks.first
      end

      # Set default session_type if only one was found.
      if event_session_types? && @event.session_types.size == 1
        @proposal.session_type = @event.session_types.first
      end

      if logged_in?
        @proposal.presenter = current_user.fullname
        @proposal.add_user(current_user)
      end
      @proposal.email = current_email

      respond_to do |format|
        format.html # new.html.erb
        format.xml  { render xml: @proposal }
        format.json { render json: @proposal }
      end
    end

    # GET /proposals/1/edit
    def edit
      # @proposal set via #assign_proposal filter

      @event = @proposal.event
      add_breadcrumb @event.title, event_proposals_path(@event)
      add_breadcrumb @proposal.title, proposal_path(@proposal)
    end

    # POST /proposals
    # POST /proposals.xml
    def create
      @proposal = @event.proposals.new(proposal_params)
      @proposal.add_user(current_user) if logged_in?
      @proposal.transition = transition_from_params if admin?

      manage_speakers_on_submit

      respond_to do |format|
        if params[:speaker_submit].blank? && params[:preview].nil? && @proposal.save
          format.html {
            # Display a page thanking users for submitting a proposal and telling them what to do next.
            page_title "Thank You!"
          }
          format.xml  { render xml: @proposal, status: :created, location: @proposal }
          format.json { render json: @proposal, status: :created, location: @proposal }
        else
          @proposal.valid? if params[:preview]
          format.html { render action: "new" }
          format.xml  { render xml: @proposal.errors, status: :unprocessable_entity }
          format.json { render json: @proposal.errors, status: :unprocessable_entity }
        end
      end
    end

    # PUT /proposals/1
    # PUT /proposals/1.xml
    def update
      # @proposal and @event set via #assign_proposal_and_event filter

      # If proposal title editing is locked, prevent non-admin from modifying title.
      if params[:proposal] && @event.proposal_titles_locked? && ! admin?
        params[:proposal].delete(:title)
      end

      if params[:proposal][:start_time] && admin?
        if params[:proposal][:start_time][:date].blank? || params[:proposal][:start_time][:hour].blank? || params[:proposal][:start_time][:minute].blank?
          @proposal.start_time = nil
        else
          @proposal.start_time = "#{params[:proposal][:start_time][:date]} #{params[:proposal][:start_time][:hour]}:#{params[:proposal][:start_time][:minute]}"
        end
      end

      @proposal.assign_attributes(proposal_params)

      add_breadcrumb @event.title, event_proposals_path(@event)
      add_breadcrumb @proposal.title, proposal_path(@proposal)

      manage_speakers_on_submit

      respond_to do |format|
        if params[:speaker_submit].blank? && params[:preview].nil? && @proposal.save
          @proposal.transition = transition_from_params if admin?
          format.html {
            flash[:success] = 'Updated proposal.'
            redirect_to(@proposal)
          }
          format.xml  { head :ok }
          format.json {
            render(
              json: {
                proposal_status: @proposal.status,
                _transition_control_html: render_to_string(partial: '/open_conference_ware/proposals/transition_control', formats: [:html])
              },
              status: :ok
            )
          }
        else
          @proposal.valid? if params[:preview]

          format.html { render action: "edit" }
          format.xml  { render xml: @proposal.errors, status: :unprocessable_entity }
          format.json { render json: @proposal.errors, status: :unprocessable_entity }
        end
      end
    end

    # DELETE /proposals/1
    # DELETE /proposals/1.xml
    def destroy
      # @proposal and @event set via #assign_proposal_and_event filter

      @proposal.destroy
      flash[:success] = "Destroyed proposal: #{@proposal.title}"

      respond_to do |format|
        format.html { redirect_to(event_proposals_path(@proposal.event)) }
        format.xml  { head :ok }
        format.json { head :ok }
      end
    end

    def manage_speakers
      @proposal = get_proposal_for_speaker_manager(params[:id], params[:speakers])

      if params[:add]
        user = User.find(params[:add])
        @proposal.add_user(user)
      elsif params[:remove]
        user = User.find(params[:remove])
        @proposal.remove_user(user)
      end

      respond_to do |format|
        format.html { render partial: "manage_speakers", layout: false }
      end
    end

    def search_speakers
      @proposal = get_proposal_for_speaker_manager(params[:id], params[:speakers])
      @matches = get_speaker_matches(params[:search])

      respond_to do |format|
        format.html { render partial: "search_speakers", layout: false }
      end
    end

    def stats
      # Uses @event
    end

    # GET /proposals/1/login
    def proposal_login_required
      return redirect_to(proposal_path(params[:proposal_id]))
    end

    def speaker_confirm
      # @proposal and @event set via #assign_proposal_and_event filter
      if current_user_is_proposal_speaker? and @proposal.confirm!
        flash[:success] = 'Updated proposal. Thank you for confirming!'
      end
      redirect_to(@proposal)
    end

    def speaker_decline
      # @proposal and @event set via #assign_proposal_and_event filter
      if current_user_is_proposal_speaker? and @proposal.decline!
        flash[:success] = 'Updated proposal.'
      end
      redirect_to(@proposal)
    end

  protected

    def proposal_params
      permitted = [
        :presenter,
        :affiliation,
        :email,
        :website,
        :biography,
        :title,
        :description,
        :excerpt,
        :agreement,
        :note_to_organizers,
        :track_id,
        :session_type_id,
        :speaking_experience,
        :audience_level,
        :tag_list]

      permitted += [:status, :room_id, :start_time, :audio_url] if admin?

      params.require(:proposal).permit(permitted)
    end

    # Is this event accepting proposals? If not, redirect with a warning.
    def assert_accepting_proposals
      unless accepting_proposals? || admin?
        flash[:failure] = Snippet.content_for(:proposals_not_accepted_error)
        redirect_to @event ? event_proposals_path(@event) : proposals_path
      end
    end

    # Ensure that anonymous users are allowed to add proposals
    def assert_anonymous_proposals
      if logged_in?
        return false # Logged in users can always create
      else
        if anonymous_proposals?
          return false # Anonymous proposals are allowed
        else
          flash[:notice] = "Please sign in so you can create and manage proposals."
          store_location
          return redirect_to(sign_in_path)
        end
      end
    end

    # Return the proposal and its assignment status for this request. The status
    # can be:
    # * :assigned_via_param
    # * :invalid_proposal
    # * :invalid_event
    def get_proposal_and_assignment_status
      if proposal = Proposal.find(params[:id].to_i) rescue nil
        if proposal.event
          return [proposal, :assigned_via_param]
        else
          return [proposal, :invalid_event]
        end
      else
        return [proposal, :invalid_proposal]
      end
    end

    def get_proposal_for_speaker_manager(proposal_id, speaker_ids_string)
      if proposal_id.blank? || proposal_id == "new_record"
        proposal = Proposal.new
        speaker_ids_string.split(',').each do |speaker|
          proposal.add_user(speaker)
        end
      else
        proposal = Proposal.find(proposal_id)
      end
      return proposal
    end

    # Assign @proposal and @event from parameters, or redirect with warnings.
    def assign_proposal_and_event
      @proposal, @proposal_assignment_status = get_proposal_and_assignment_status()
      case @proposal_assignment_status
      when :assigned_via_param
        @event = @proposal.event
        return false # Successfully found both @event and @proposal
      when :invalid_proposal
        flash[:failure] = "Sorry, that presentation proposal doesn't exist or has been deleted."
        return redirect_to(action: :index)
      when :invalid_event
        flash[:failure] = "Sorry, no event was associated with proposal ##{@proposal.id}"
        return redirect_to(action: :index)
      end
    end

    def assert_user_complete_profile
      if user_profiles? && logged_in? && !current_user.admin? && !current_user.complete_profile?
        current_user.complete_profile = true
        if current_user.valid?
          current_user.save
        else
          flash[:notice] = "Please complete your profile before creating a proposal."
          store_location
          return redirect_to(edit_user_path(current_user, require_complete_profile: true))
        end
      end
    end

    def assign_proposals_breadcrumb
      add_breadcrumb "Proposals", proposals_path
    end

    def manage_speakers_on_submit
      if params[:speaker_ids].present?
        speakers = params[:speaker_ids].map(&:first)
        unless speakers.blank?
          speakers.each do |speaker|
            @proposal.add_user(speaker)
          end
        end
      end
    end

    # Return the name of a transition (e.g., "accept") from a Proposal's params.
    def transition_from_params
      return params[:proposal].present? ? params[:proposal][:transition] : nil
    end

    # Return a sanitized Regexp for matching a speaker by name from the +query+ string.
    def get_speaker_matcher(query)
      string = query.gsub(/[[:punct:]]/, ' ').gsub(/\s{2,}/, ' ').strip
      return Regexp.new(Regexp.escape(string), Regexp::IGNORECASE)
    end

    # Return an array of speakers (User records) matching the +query+ string.
    def get_speaker_matches(query)
      if query.blank? || ! query.match(/\w+/)
        return []
      else
        matcher = get_speaker_matcher(query)
        return(User.complete_profiles.select{|u| u.fullname.try(:match, matcher)} - @proposal.users)
      end
    end

    # Base method used for #show and #session_show
    def base_show
      if selector? && @event.accept_selector_votes?
        @selector_vote = @proposal.selector_votes.find_or_initialize_by(user_id: current_user.id)
      end

      unless defined?(@redirector)
        add_breadcrumb @event.title, event_proposals_path(@event)
        add_breadcrumb @proposal.title, proposal_path(@proposal)

        @profile = @proposal.profile
        @comment = @proposal.comments.new(email: current_email)
        @display_comment_form = \
          # Admin can always leave comments
          admin? || (
            # Display comment form if the event is accepting proposals
            accepting_proposals? ||
            # or if the settings provide a toggle and the event is accepting comments
            (event_proposal_comments_after_deadline? && @event.accept_proposal_comments_after_deadline?)
          )
        @focus_comment = false
      end

      respond_to do |format|
        format.html {
          if defined?(@redirector)
            @redirector.call
          else
            render template: "open_conference_ware/proposals/show"
          end
        }
        format.xml  { render xml: @proposal }
        format.json { render json: @proposal }
      end
    end

  end
end