ucberkeley/moocchat

View on GitHub
app/models/task/task_sequencer.rb

Summary

Maintainability
A
1 hr
Test Coverage
class Task < ActiveRecord::Base

  class Sequencer
    # Provides sequencing logic for a +Condition+.
    # Automatically initialized as part of a +Task+, and used by
    # +TaskController+ to sequence the pages.

    # Returns one of +:in_prologue+, +:in_body+, +:in_epilogue+,
    # or +:finished+ indicating where we are in the sequence.  (NOTE: If the
    # prologue is empty, the return value from this method will still
    # be +:in_prologue+ until the first call to +current_page+, but the
    # sequence of returned pages will be correct.)
    attr_reader :where

    # Current value of monotonically increasing sequence counter, which
    # starts from 0
    attr_reader :counter

    # Current index into which question from +ActivitySchema+ will be
    # served next.  Advanced by {#next_question}.  Pins when it reaches
    # the last question in +ActivitySchema+.
    attr_reader :question_counter

    # Current position within prologue, body, or epilogue (starts at 0)
    attr_reader :subcounter

    def initialize(args={})     # :nodoc:
      @body_reps = args[:body_repeat_count] || 1
      @num_questions = args[:num_questions] || 1
      @counter = 0
      @subcounter = 0
      @question_counter = 0
      @where = :in_prologue
    end

    public

    # Return +Template+ object to be rendered next for +condition+,
    # or +nil+ if end of sequence.  (The task's +condition+ must be
    # passed in so we don't have to serialize it to the database.)
    def current_page(condition=nil)
      return nil if @where == :finished
      @condition ||= condition
      array = array_for_current_subsequence
      # boundary condition: zero body iterations => skip to epilogue
      unless (@where == :in_body && @body_reps.zero?)
        # try to return an element
        if (elt = array[@subcounter])
          return elt
        end
        # elt is nil: we must be at end of subsequence.
        # if in body, iterate again
        if @where == :in_body && @body_reps > 1
          start_new_body_iteration
          return current_page
        end
      end
      # else we're not in body, must be in prologue or epilogue
      @where = next_subsequence
      # if we are all out of subsequences, this is the end of the flow
      return nil if @where.nil?
      # else we still have a subsequence to try, so reset subsequence counter,
      # and tail-recurse
      @subcounter = 0
      return current_page
    end

    # Advance to next page.
    def next_page
      @subcounter += 1
      @counter += 1
    end

    # Advance to next question, but pin if no more questions.
    def next_question
      @question_counter = (@question_counter + 1) % @num_questions
    end

    private

    def start_new_body_iteration
      @body_reps -= 1
      @subcounter = 0
    end

    def array_for_current_subsequence
      @where = next_subsequence if @where == :in_prologue && @condition.prologue_pages.empty?
      case @where
      when :in_prologue then @condition.prologue_pages
      when :in_body then @condition.body_pages
      when :in_epilogue then @condition.epilogue_pages
      else raise RuntimeError, "Unknown sequence state #{@where}"
      end
    end

    def next_subsequence
      case @where
      when :in_prologue then :in_body
      when :in_body then :in_epilogue
      when :in_epilogue then :finished
      else raise RuntimeError, "Unknown sequence state #{@where}"
      end
    end
  end

end