twitter/clockworkraven

View on GitHub
app/controllers/task_responses_controller.rb

Summary

Maintainability
B
6 hrs
Test Coverage
# Copyright 2012 Twitter, Inc. and others.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require 'csv'

class TaskResponsesController < ApplicationController
  before_filter :find_response, :except => [:index, :responses_csv]
  before_filter :require_priv, :except => [:index, :responses_csv]

  caches_action :index, :layout => false, :if => proc{request.format.html?}

  private

  def find_response
    @response = TaskResponse.find(params[:id])
  end

  def require_priv
    if @response.task.evaluation.prod? and !current_user.privileged?
      render :json => {:status => "Forbidden"}, :status => 403
    end
  end

  public

  # GET /evaluations/1/task_responses
  # GET /evaluations/1/task_responses.json
  def index
    @eval = Evaluation.includes({
      :task_responses => {:mc_question_responses => [:mc_question_option], :fr_question_responses => [], :task => [], :m_turk_user => []},
      :fr_questions => [:fr_question_responses],
      :mc_questions => {:mc_question_options => [:mc_question_responses => [:mc_question_option]]}
    }).find(params[:evaluation_id])

    @task_responses = @eval.task_responses

    # assemble JSON data to send to client for visualization. We send a data
    # Hash with for elements:
    #
    # @data[:mcQuestions]
    #   a hash of <id> => {:label => <label>. :options => [<option ids>]}
    #   with an element for each MCQuestion
    # @data[:mcQuestionOptions]
    #   a hash of
    #   <id> => {
    #     :label => <label>,
    #     :value => <value>
    #     :question => <mc question id>
    #   }
    #   with an element for each MCQuestionOption
    # @data[:frQuestions]
    #   a hash of <id> => {:label => <label>} with an element for each
    #   FRQuestion
    # @data[:responses]
    #   a hash of
    #   <id> => {
    #     :frQuestions => {
    #        <fr question 1 id> => <response text>,
    #        <fr question 2 id> => <response text>,
    #        ... entry for each FRQuestion ...
    #     },
    #     :mcQuestions => {
    #        <mc question 1 id> => <mc question option id>,
    #        <mc question 2 id> => <mc question option id>,
    #        ... entry for each MCQuestion ...
    #     }
    #     :approved => <false if rejected, true if approved or no decision yet>
    #   }
    #   with an element for each TaskResponse
    #
    # An sample @data is in TaskResponsesControllerTest#test_index
    mc_questions = {}
    mc_question_options = {}
    @eval.mc_questions.each do |mc_q|
      mc_questions[mc_q.id] = {
        :label => mc_q.label,
        :options => mc_q.mc_question_options.map{|opt| opt.id}
      }

      mc_q.mc_question_options.each do |opt|
        mc_question_options[opt.id] = {
          :label => opt.label,
          :value => opt.value,
          :question => opt.mc_question_id
        }
      end
    end

    fr_questions = {}
    @eval.fr_questions.each do |fr_q|
      fr_questions[fr_q.id] = {
        :label => fr_q.label
      }
    end

    responses = {}
    @task_responses.each do |resp|
      fr_question_responses = {}

      resp.fr_question_responses.each do |fr_resp|
        fr_question_responses[fr_resp.fr_question_id] = fr_resp.response
      end

      mc_question_responses = {}

      resp.mc_question_responses.each do |mc_resp|
        mc_question_responses[mc_resp.mc_question_option.mc_question_id] = mc_resp.mc_question_option_id
      end

      responses[resp.id] = {
        :frQuestions => fr_question_responses,
        :mcQuestions => mc_question_responses,
        :approved => (resp.approved.nil? or resp.approved?)
      }
    end

    @data = {
      :mcQuestions => mc_questions,
      :mcQuestionOptions => mc_question_options,
      :frQuestions => fr_questions,
      :responses => responses
    }


    respond_to do |format|
      format.html # index.html.erb
      format.json { render :json => @data }
      format.csv {
        render :text => responses_csv(','),  :type => 'text/csv',
                                             :filename => 'responses.csv',
                                             :disposition => 'attachment',
                                             :layout => false
      }
      format.tsv {
        render :text => responses_csv("\t"), :type => 'text/tab-separated-values',
                                             :filename => 'responses.tsv',
                                             :disposition => 'attachment',
                                             :layout => false
      }
    end
  end

  def approve
    @response.approve!
    expire_action(:action => 'index', :evaluation_id => @response.task.evaluation)
    render :json => {:status => "Approved"}
  end

  def reject
    @response.reject!
    expire_action(:action => 'index', :evaluation_id => @response.task.evaluation)
    render :json => {:status => "Rejected"}
  end

  def responses_csv(sep = ',')
    CSV.generate(:col_sep => sep) do |csv|
      # Pull out the fields that were uploaded with the original data,
      # so that we can output these along with the task responses.
      orig_fields_keys = if @task_responses.empty? 
                           []
                         else 
                           @task_responses.first.task.data.keys
                         end

      # As a slightly hacky way of dealing with metadata, Clockwork Raven stores
      # these metadata as MC questions in the eval. This pulls out the
      # _true_ MC questions (i.e., questions that aren't metadata), so that we
      # don't doubly output any fields that were uploaded with the original data.
      real_mc_questions = @eval.mc_questions.select{|mc_q| !mc_q.metadata }
                               
      # headers
      csv << orig_fields_keys + 
             (real_mc_questions + @eval.fr_questions).map{|q| q.label} +
             ["MTurk User", "Work Duration", "Approval"]

      @task_responses.each do |task_response|
        # build the row
        row = []

        # Fields from the original data file.
        orig_fields_keys.each do |k|
          row.push(task_response.task.data[k])
        end

        # MC Questions
        real_mc_questions.each do |mc_q|
          option_id = @data[:responses][task_response.id][:mcQuestions][mc_q.id]
          mc_q_resp = option_id.nil? ? nil : @data[:mcQuestionOptions][option_id][:label]
          row.push(mc_q_resp.nil? ? nil : mc_q_resp)
        end

        # FR Questions
        @eval.fr_questions.each do |fr_q|
          fr_q_resp = @data[:responses][task_response.id][:frQuestions][fr_q.id]
          row.push(fr_q_resp.nil? ? nil : fr_q_resp)
        end

        row.push(task_response.m_turk_user_id)
        row.push(task_response.work_duration)
        row.push(task_response.approved)

        csv << row
      end
    end
  end
end