ucberkeley/moocchat

View on GitHub
app/models/task.rb

Summary

Maintainability
A
0 mins
Test Coverage
class Task < ActiveRecord::Base

  # Ties together a +Learner+, and +Condition+ into
  # a task that steps the learner through a sequence of pages.

  belongs_to :learner
  belongs_to :condition
  validates_associated :learner
  validates_associated :condition

  # In most experiments, at some point a +Task+ will be added to a
  # +WaitingRoom+, which collects learners and forms groups from them.
  belongs_to :waiting_room

  # The sequence state is tracked by an internal private class
  # +Task::Sequencer+, certain elements of which are exposed via
  # delegation as read-only attributes of this class.

  require_relative './task/task_sequencer'
  serialize :sequence_state, Sequencer

  serialize :turk_params, Hash

  # Tasks log interesting events to an +EventLog+.
  has_one :event_log    

  attr_accessible :condition, :learner, :completed, :original_chat_group, :chat_group, :sequence_state, :turk_params

  # Exception raised when learner tries to create task for an activity that
  # isn't open yet
  class ActivityNotOpenError < RuntimeError ; end
  # Exception raised when +learner_index+ is called but this task's learner
  # isn't in the specified chat group channel
  class LearnerNotInGroupError < RuntimeError ; end
  
  serialize :user_state, Hash

  # For testing purposes, an admin or "Test learner" can bypass the
  # timer on the welcome page and force group formation to happen RIGHT NOW.
  delegate :force_group_formation_now!, :to => :waiting_room

  # Create a new task from a hash that includes a +condition_id+,
  # +activity_schema_id+, and +learner_name+.
  #
  # +condition_id+ and +activity_schema_id+ must be the primary keys
  # of an existing valid +Condition+ and +ActivitySchema+ respectively.
  #
  # +learner_name+ is the learner's nym; if it doesn't exist, an instance
  # of +Learner+ will be created.
 
  def self.create_from_params(params)
    cond = params[:condition_id]
    if(cond.is_a?(Hash)) #suggested by github to handle collection select(which returns {:id => value} rather than value)
      cond = cond[:id]
    end
    condition = Condition.find  cond
    learner = Learner.find_or_create_by_name! params[:learner_name]

    raise ActivityNotOpenError unless condition.primary_activity_schema.enabled?
    
    @t = Task.create!(
      :condition => condition,
      :learner => learner,
      :completed => false,
      :original_chat_group => nil,
      :chat_group => nil,
      :sequence_state => Sequencer.new(:body_repeat_count => condition.body_repeat_count, :num_questions => condition.primary_activity_schema.num_questions),
      :turk_params => params[:turk_params]
      )
  end

  # The counter starts at 0 on the first page of the task and
  # counts by 1 as each new page is visited.
  delegate :counter, :to => :sequence_state

  # Which question from the +ActivitySchema+ is to be served next (0-based)
  delegate :question_counter, :to => :sequence_state

  # Where we are in the condition flow (prologue, body, epilogue)
  delegate :where, :to => :sequence_state

  # Subcounter of where we are within the prologue/body/etc.
  delegate :subcounter, :to => :sequence_state
  
  # Form chat group ("channel") name from tasks associated with this group
  def self.chat_group_name_from_tasks(tasks)
    tasks.map(&:id).sort.map(&:to_s).join(',')
  end

  # Given a chat group channel name (string), return numeric index (0, 1, ...)
  # of which learner is represented by THIS task.
  def learner_index
    group_tasks.index(self.id.to_i) ||
      raise(LearnerNotInGroupError,
      "Chat group #{chat_group} does not include task id #{self.id}")
  end

  # Retrieve user state for all tasks in my chat group, including my state
  def user_state_for_all
    begin
      group_tasks.map { |task_id| Task.find(task_id).user_state }
    rescue ActiveRecord::RecordNotFound => e
      raise LearnerNotInGroupError, "Can't find user state for task: #{e.message}"
    end
  end

  def self.parse_group_tasks(group_tasks)
    group_tasks.to_s.split(',').map(&:to_i)
  end

  def group_tasks # :nodoc:
    case chat_group
    when blank?
      raise LearnerNotInGroupError, "Learner was never assigned to a group"
    when WaitingRoom::CHAT_GROUP_NONE
      raise LearnerNotInGroupError, "Learner was kicked out of Waiting Room"
    else
      Task.parse_group_tasks(chat_group)
    end
  end

  # Returns the +Template+ object that should be rendered for the
  # current page in the task sequence.
  def current_page
    page = sequence_state.current_page(self.condition)
    # the above call may modify sequence_state's internal state,
    # so we have to save the task to serialize it
    save!
    page
  end

  # Advance to the next page of the task.  Returns that page, or +nil+
  # if end of task has been reached.
  def next_page!
    sequence_state.next_page
    save!
    self.reload.current_page
  end

  # Advance to next question
  def next_question!
    sequence_state.next_question
    save!
    reload
  end

  def remove_from_chat_group(task_id)
    chat_group_ids = self.group_tasks
    if chat_group_ids.include? task_id   # Ignore if already removed
      chat_group_ids.delete(task_id)
      chat_group = chat_group_ids.map { |task_id| Task.find(task_id) }
      new_group_name = Task.chat_group_name_from_tasks(chat_group)
      self.assign_to_chat_group(new_group_name, false)
    end
  end

  # Assign this task to a particular chat group.  As a side effect, this removes the task
  # from its waiting room.
  def assign_to_chat_group(group, original_assignment)
    self.chat_group = group
    if original_assignment then self.original_chat_group = self.chat_group end
    self.waiting_room = nil
    self.save!
  end

  # Returns the next question to be consumed for the task.
  def current_question
    condition.primary_activity_schema.questions[question_counter]
  end

  # Log an interesting event related to this task. Denormalize the various
  # foreign key fields - see README.rdoc for details.  Some event names
  # require a value; enforcing that is left to the +EventLog+ validations.
  def log(name, value='')
    EventLog.create!(
      :name => name.to_sym,
      :value => value,
      :task => self,
      :learner => self.learner,
      :activity_schema => self.condition.primary_activity_schema,
      :condition => self.condition,
      :counter => self.counter,
      :subcounter => self.subcounter,
      :question_counter => self.question_counter,
      :question => self.current_question,
      :chat_group => self.chat_group)
  end

end