concord-consortium/rigse

View on GitHub
rails/app/controllers/report/learner_controller.rb

Summary

Maintainability
B
4 hrs
Test Coverage
class Report::LearnerController < ApplicationController

  require 'cgi'

  before_action :setup,
      :only => [
      :index,
      :update_learners
    ]

  protected

  def not_authorized_error_message
    super({resource_type: 'report learner'})
  end

  public

  def update_learners
    authorize Report::Learner
    Portal::Learner.all.each { |l|
      l.update_report_model_cache
    }
  end

  def index
    authorize Report::Learner
    render layout: ENV['RESEARCHER_REPORT_ONLY'] ? "minimal" : "application"
  end

  def updated_at
    # no authorization needed ...
    learner = Report::Learner.find_by_user_id_and_offering_id(current_visitor.id,params[:id])
    if learner
      last_run = learner.last_run
      status = 400
      hasnt_run_text  = I18n.t "StudentHasntRun"
      json_response = { error_msg: hasnt_run_text }
      text_response = hasnt_run_text
      if last_run
        status = 200
        modification_time = last_run.strftime("%s")
        json_response = {modification_time: modification_time }
        text_response = modification_time
      end
      respond_to do |format|
        format.html do
          render :plain => text_response, :status => status
        end
        format.json do
          render :json => json_response, :status => status
        end
      end
    else
      head :ok
    end
  end

  def report_only
    render layout: "minimal"
  end

  private

  def setup
    @button_texts = {
      :apply => 'Apply Filters',
      :usage => 'Usage Report',
      :details => 'Details Report',
      :arg_block => 'Arg Block Report',
      :log_manager => 'Open in Log Manager'
    }

    # commit"=>"update learners"
    if params[:commit] =~ /update learners/i
      update_learners
    end

    @no_log_manager = APP_CONFIG[:codap_url].nil? || APP_CONFIG[:log_manager_data_interactive_url].nil?

    if params.has_key?(:commit)
      # Selector makes a request to the Elasticsearch API and processes the results
      learner_selector = Report::Learner::Selector.new(params, current_visitor)
      # The learners we have selected:
      @select_learners  = learner_selector.learners
      url_helpers = Reports::UrlHelpers.new(:protocol => request.protocol, :host_with_port => request.host_with_port)
    end

    hide_names = params[:hide_names] == 'on'

    if params[:commit] == @button_texts[:usage]
      runnables =  learner_selector.runnables_to_report_on
      report = Reports::Usage.new(:runnables => runnables, :report_learners => @select_learners, :blobs_url => dataservice_blobs_url, :include_child_usage => params[:include_child_usage], :url_helpers => url_helpers, :hide_names => hide_names)
      book = report.run_report
      send_data(book.to_data_string, :type => book.mime_type, :filename => "usage.#{book.file_extension}" )
    elsif params[:commit] == @button_texts[:details]
      runnables =  learner_selector.runnables_to_report_on
      report = Reports::Detail.new(:runnables => runnables, :report_learners => @select_learners, :blobs_url => dataservice_blobs_url, :url_helpers => url_helpers, :hide_names => hide_names)
      book = report.run_report
      send_data(book.to_data_string, :type => book.mime_type, :filename => "details.#{book.file_extension}" )
    elsif params[:commit] == @button_texts[:arg_block]
      arg_block(@select_learners)
    elsif params[:commit] == @button_texts[:log_manager]
      log_manager
    end
  end

  def log_manager
    if @no_log_manager
      alert_and_reload "This portal is not configured to open the Log Manager"
      return
    end

    if @select_learners.length == 0
      alert_and_reload "No learners meet the criteria you selected"
      return
    end

    codap_url = APP_CONFIG[:codap_url]
    data_interactive_url = APP_CONFIG[:log_manager_data_interactive_url]

    # We'll let the Log Manager construct the query on run_remote_endpoint. In
    # order to do so, we provide the appropriate prefix for
    # run_remote_endpoints, and all the ids In the interest of URL-length
    # sanity, the ids are passed as a single string, joined by a delimiter
    # ('-') that is not percent encoded. (The practical URL length limit is
    # ~2k characters, so this should allow querying several hundred learners
    # before requests start failing because the URL is too long.)

    sample_learner_id = @select_learners.first.learner_id
    prefix = external_activity_return_url(sample_learner_id).match("(.*)" + sample_learner_id.to_s)[1]
    learner_ids = @select_learners.map { |l| l.learner_id }

    redirect_to(codap_url + "?moreGames=" + CGI::escape([{
      :name => "LogMgr",
      :dimensions => {
        :width => 600,
        :height => 800
      },
      :url => data_interactive_url + "?" + { :rep => prefix, :ids => learner_ids.join('-') }.to_param
    }].to_json))
  end

  def arg_block(learners)
    authoring_sites = learners.select { |l| l.runnable_type == "ExternalActivity" }.map do |learner|
      uri = URI(learner.runnable.url)
      "#{uri.scheme}://#{uri.host}:#{uri.port}"
    end

    learners = learners.reject { |l| l.permission_forms.strip.empty? }

    if learners.length == 0
      alert_and_reload "No learners with signed permission forms meet the criteria you selected"
      return
    end

    if authoring_sites.length == 0
      alert_and_reload "None of the selected learners performed external activities"
      return
    end

    # TODO: instead of refusing the request, modify the arg_block_bouncer to contain a list of the selected
    # authoring sites, with a submit button for each site (and disable the javascript auto-submit)
    if authoring_sites.uniq.count > 1
      alert_and_reload "The selected learners' arg block activity occurred on more than one authoring site. Try limiting your request to activities hosted on just one authoring site."
      return
    end

    @report_url = "#{authoring_sites.first}/c_rater/argumentation_blocks/report"
    @remote_endpoints = learners.map do |learner|
      learner.learner.remote_endpoint_url
    end

    # intentionally leave out student name - results should be semi-anonymized
    columns = [:permission_forms, :teachers_name, :school_name, :class_name, :class_id, :student_id, :remote_endpoint]
    sort_by_indices = [:teachers_name, :school_name, :class_id, :student_id].map { |key| columns.find_index(key) }

    rows = learners.map do |learner|
      columns.map do |column|
        # except for remote_endpoint, column names are just names of Report::Learner instance methods
        column == :remote_endpoint ? learner.learner.remote_endpoint_url : learner.send(column)
      end
    end

    remote_endpoint_index = columns.find_index(:remote_endpoint)

    # Several Report::Learners may correspond to the same (student, class, school) combination. We want to emit
    # one row per (student, class, school) combination, with the entry in the :remote_endpoint column being
    # an array of all remote_endpoints corresponding to that row. (remote_endpoints are 1:1 with Report::Learners)
    group_by_indices = columns.reject { |column| column == :remote_endpoint }.map { |key| columns.find_index(key) }
    remote_endpoints_by_row = rows.group_by { |row| row.values_at(* group_by_indices) }
    remote_endpoints_by_row.keys.each do |key|
      remote_endpoints_by_row[key] = remote_endpoints_by_row[key].map do |row|
        row[remote_endpoint_index]
      end
    end

    rows = remote_endpoints_by_row.keys.map {|key| key + [remote_endpoints_by_row[key]]}
    rows = rows.sort_by! { |row| row.values_at(* sort_by_indices) }

    @arg_block_buckets = {
      :columns => columns,
      :rows => rows
    }

    render :arg_block_bouncer, :layout => false
  end

  def alert_and_reload(message)
    flash['alert'] = message
    redirect_to request.GET.except(:commit)
  end

end