Skip to content

Instantly share code, notes, and snippets.

@zeroeth
Created November 11, 2010 02:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zeroeth/671898 to your computer and use it in GitHub Desktop.
Save zeroeth/671898 to your computer and use it in GitHub Desktop.
class ArrayExpander
attr_accessor :replacements
attr_accessor :column_order
def initialize
self.replacements = {}
end
### Process unquoted single items, or quoted evaluatable strings
def from_string(string)
if string.blank?
[]
elsif string.match(",") or string.match(":")
eval("[#{string}]")
else
[string]
end
end
### Wrap single items in an array for expander
def normalize(array)
return array if array.flatten.size < 2
#return array unless array.any?{|element| element.kind_of?(Array)}
array.map do |element|
if element.kind_of?(Array)
element
else
[element]
end
end
end
def cell_expand(array)
return array if array.flatten.size < 2
return array.first if array.size == 1
expanded = []
recursor( array, expanded)
expanded
end
### Iterate a 2D array, permuting the values of each sub array with one another.
def recursor(source, destination, in_stack = [], index = 0)
source[index].each do |element|
stack = in_stack.clone.push element
if source[index+1]
recursor source, destination, stack.clone, index+1
else
destination.push stack
end
end
end
### Store replacement values
def set_replacement(key, value)
replacements[key] = value
end
### Substitute any stored values into the array
def replace_tokens(array)
array.map do |element|
replacements[element] || element
end
end
### what order to unwravel the arrays
def set_column_order(array)
self.column_order = array
end
def row_expand(hash)
expanded_rows = []
pair_expander expanded_rows, hash, column_order, 0, Hash.new
expanded_rows
end
def add_to_destination(destination, output)
destination.push output.clone
end
# give it a hash like {:a => [1], :b => [2,3], :c => [:x,:y], and an iterator like [:a, [:b, c]]
# and you get back [{:a => 1, :b => 2, :c => :x}, {:a => 1, :b => 3, :c => :y}]
def pair_expander(destination, hash, iterators, index = 0, output = {})
iterator = iterators[index]
# puts "|"*(2*index+1) + iterator.inspect
if iterator.kind_of? Array
hash[iterator.first].size.times do |sub_index|
iterator.each do |sub_iterator|
output[sub_iterator] = hash[sub_iterator][sub_index]
end
if iterators[index+1]
pair_expander(destination, hash, iterators, index+1, output.clone)
else
add_to_destination destination, output
end
end
else
if hash[iterator].empty?
output[iterator] = nil
if iterators[index+1]
pair_expander(destination, hash, iterators, index+1, output.clone)
else
add_to_destination destination, output
end
else
hash[iterator].each do |value|
output[iterator] = value
if iterators[index+1]
pair_expander(destination, hash, iterators, index+1, output.clone)
else
add_to_destination destination, output
end
end
end
end
end
end
require File.dirname(__FILE__) + '/../test_helper'
class ArrayExpanderTest < ActiveSupport::TestCase
context "Expander" do
setup do
@expander = ArrayExpander.new
end
# IN "A, B, [C,D]" => OUT [A, B, [C, D]]
should "return array from string" do
array = @expander.from_string %{"A", "B", ["C", :D]}
assert_equal ["A", "B", ["C", :D]], array
array = @expander.from_string %{"A", "B", "C"}
assert_equal ["A", "B", "C"], array
array = @expander.from_string %{"1011", :all_programs}
assert_equal ["1011", :all_programs], array
end
should "return single items" do
array = @expander.from_string %{Some Name}
assert_equal ["Some Name"], array
end
should "return single items with commas" do
array = @expander.from_string %{"Some, Name"}
assert_equal ["Some, Name"], array
end
# IN "A, B, ["C,C",D]" => OUT [A, B, ["C, C", D]]
should "return from string with separator in name" do
array = @expander.from_string %{"A", "B", ["C, C", "D"]}
assert_equal ["A", "B", ["C, C", "D"]], array
end
# :all_regions.. register handler to return stuff (from outside world)
should "replace tokens" do
@expander.set_replacement :all_programs, ['P1', 'P2']
array = @expander.replace_tokens [:all_programs, :A, :B]
assert_equal [['P1','P2'], :A, :B], array
end
# IN [A, B, [C, D]] => OUT [[A],[B],[C,D]]
should "wrap single first level items in arrays" do
array = @expander.normalize [:A, :B, [:C, :D]]
assert_equal [[:A], [:B], [:C,:D]], array
end
should "not wrap if no sub arrays" do
array = @expander.normalize [:A, :B, :C]
assert_equal [[:A], [:B], [:C]], array
array = @expander.normalize [[:A, :B, :C]]
assert_equal [[:A,:B,:C]], array
end
# IN [[A],[B],[C,D]] => OUT [[A,B,C],[A,B,D]]
should "expand cell array" do
array = @expander.cell_expand [[:A,:B], [:Q], [:Y, :Z]]
assert_equal [[:A, :Q, :Y], [:A, :Q, :Z], [:B, :Q, :Y], [:B, :Q, :Z]], array
end
should "expand single array" do
array = @expander.cell_expand [[:A,:B,:C]]
assert_equal [:A,:B,:C], array
end
should "build hash from csv row"
should "expand row hash" do
@expander.set_column_order [:other, :one, :empty, :two, :three, [:four, :five]]
source_row = {:other => ['other'], :one => [:a,:b], :empty => [], :two => [1], :three => [:q], :four => [:x,:y,:z], :five => [:X, :Y]}
rows = @expander.row_expand source_row
expected_rows = [{:other => 'other', :one => :a, :empty => nil, :two => 1, :three => :q, :four => :x, :five => :X},
{:other => 'other', :one => :a, :empty => nil, :two => 1, :three => :q, :four => :y, :five => :Y},
{:other => 'other', :one => :a, :empty => nil, :two => 1, :three => :q, :four => :z, :five => nil},
{:other => 'other', :one => :b, :empty => nil, :two => 1, :three => :q, :four => :x, :five => :X},
{:other => 'other', :one => :b, :empty => nil, :two => 1, :three => :q, :four => :y, :five => :Y},
{:other => 'other', :one => :b, :empty => nil, :two => 1, :three => :q, :four => :z, :five => nil}
]
assert_equal 6, expected_rows.size
assert_equal expected_rows, rows
end
should "expand from inline order"
should "expand evaluation row" do
non_expanding_fields = ['Survey ID', 'Survey Name', 'Deployment Type', 'Supervisor', 'Take']
expanding_fields = ['Shared Supervisor And Eval', 'Supervisor Group', 'Shared Eval', 'Evaluators', 'Evaluatees', ['Start On', 'End On', 'Deployment Name']]
@expander.set_column_order non_expanding_fields + expanding_fields
row ={ "Supervisor Group"=>["Assistant/Associate Directors"],
"Shared Eval"=>[],
"Survey ID"=>["1497"],
"Survey Name"=>["Faculty Evaluation"],
"Deployment Type"=>["Evaluation"],
"Supervisor"=>[],
"Take"=>[],
"Shared Supervisor And Eval"=>[["1011", "AZ"], ["1011", "BR"]],# ["1011", "PR"]],
"Evaluators"=>["Resident"],
"Evaluatees"=>["Faculty"],
"Deployment Name"=>[["First", "Trimester"], ["Second", "Trimester"], ["Third", "Trimester"]],
"End On"=>["2011-03-01", "2011-06-01", "2011-07-15"],
"Start On"=>["2010-11-01", "2011-03-01", "2011-06-01"]
}
rows = @expander.row_expand row
assert_equal 6, rows.size
end
end
end
class InvalidEvaluatee < RuntimeError; end
class InvalidSigner < RuntimeError; end
class InvalidCloser < RuntimeError; end
class InvalidCompleter < RuntimeError; end
class AlreadySignedOff < RuntimeError; end
class EvaluationPortfolio < ActiveRecord::Base
belongs_to :evaluation
has_one :survey, :through => :evaluation
belongs_to :evaluation_deployment
has_one :evaluator_group, :through => :evaluation
has_one :evaluatee_group, :through => :evaluation
has_one :supervisor, :through => :evaluation
belongs_to :evaluatee, :class_name => 'User'
has_many :evaluation_survey_responses, :dependent => :destroy
has_many :comments, :as => :commentable, :dependent => :destroy
delegate :name, :to => :evaluation, :prefix => true
def evaluatee_name
evaluatee.name(:formal)
end
def review?
evaluation.discussion?
end
### ASSOCIATIONS ########################################
# FIXME: wish this could be a has_many
def evaluators
evaluation_deployment.group.users
end
# FIXME: wish this could be a has_many
def responded_evaluators
evaluation_survey_responses.map(&:user)
end
def pending_evaluators
evaluators - responded_evaluators - [evaluatee]
end
### STATE MACHINE #######################################
state_machine :state, :initial => :responding do
# NOTE put state based permission checks like.. can comment? for user and supervisor?
# if common patterns evolve out of the controllers
state :responding
state :reviewing
state :commenting
state :signing
state :complete
on :end_responding do
transition :responding => :complete, :if => :no_pending_and_review_false?
transition :responding => :reviewing, :if => :no_pending_and_review_true?
end
on :supervisor_force_end_responding do
transition :responding => :complete, :unless => :review?
transition :responding => :reviewing, :if => :review?
end
on :start_commenting do
transition :reviewing => :commenting, :if => :supervisor_commented?
end
on :sign_advance do
transition :signing => :complete, :if => :both_signed?
transition :commenting => :complete, :if => :both_signed?
transition :commenting => :signing, :if => :one_signed?
end
before_transition :update_state_timestamp
after_transition :to => :reviewing, :do => :reviewing_notification
after_transition :to => :commenting, :do => :commenting_notification
after_transition :to => :signing, :do => :signing_notification
after_transition :to => :complete, :do => :complete_notification
# TODO email/comment transition events here!
end
## TRANSITION GUARDS ####################################
def no_pending_and_review_true?
review? && pending_evaluators.empty?
end
def no_pending_and_review_false?
!review? && pending_evaluators.empty?
end
def supervisor_commented?
comments.any?{|comment| comment.user && is_supervisor?(comment.user) }
end
def none_signed?
!supervisor_signed? && !evaluatee_signed?
end
def one_signed?
supervisor_signed? ^ evaluatee_signed?
end
def both_signed?
supervisor_signed? && evaluatee_signed?
end
def supervisor_signed?
!supervisor_signed_at.nil?
end
def evaluatee_signed?
!evaluatee_signed_at.nil?
end
## TRANSITION EVENTS ####################################
def update_state_timestamp
self.state_started_at = Time.now
end
def reviewing_notification
comment "Supervisor is reviewing Evaluation."
Notifier.deliver_portfolio_start_reviewing self
end
def commenting_notification
comment "Open for Comments."
Notifier.deliver_portfolio_start_commenting self
end
def signing_notification
comment "Awaiting signature from #{needed_signatures}."
Notifier.deliver_portfolio_start_signing self
end
def complete_notification
comment "Evaluation for #{evaluatee.name(:proper)} is complete."
Notifier.deliver_portfolio_complete self
end
## METHODS ##############################################
# TODO this and the controllers could benefit from a single authorization matrix
# that echos the person being superuser/eval admin/supervisor/evaluatee based on state
# Only supervisors and evaluatee's can sign off.
def sign_off( person )
if is_supervisor?(person)
raise AlreadySignedOff if supervisor_signed?
self.supervisor_signed_at = Time.now
comments.create! :from_system => true, :body => "Supervisor #{person.name(:formal)} signed off."
return self.supervisor_signed_at
elsif person.is?(evaluatee)
raise AlreadySignedOff if evaluatee_signed?
self.evaluatee_signed_at = Time.now
comments.create! :from_system => true, :body => "Evaluatee #{person.name(:formal)} signed off."
return self.evaluatee_signed_at
else
raise InvalidSigner
end
end
def needed_signatures
needed = []
needed << "Evaluatee" unless evaluatee_signed?
needed << "Supervisor" unless supervisor_signed?
needed.join ' and '
end
def name
"#{evaluation.name} #{evaluation_deployment.name} of #{evaluatee.name}"
end
def comment(body)
comments.create! :from_system => true, :skip_email => true, :body => body
end
### USER HELPERS ########################################
def is_supervisor?(person)
person.is?(supervisor) || person.in_group?(EVALUATIONS_ADMIN_GROUP_NAME)
end
def supervisors
[supervisor, survey.account.evaluation_admins].flatten
end
def evaluatee_and_supervisors
[evaluatee, supervisors].flatten
end
def comment_recipients
evaluatee_and_supervisors
end
end
require File.dirname(__FILE__) + '/../test_helper'
class EvaluationPortfolioTest < ActiveSupport::TestCase
fixtures :all
preload_factory_data :evaluations, :users, :evaluation_portfolios
should_belong_to :evaluation
should_belong_to :evaluatee
should_have_many :evaluation_survey_responses
should_have_many :comments
should_have_one :evaluator_group
should_have_one :evaluatee_group
should_belong_to :evaluation_deployment
should_have_one :supervisor
context "Portfolio" do
setup do
@portfolio = preload :evaluation_portfolio
@evaluation = @portfolio.evaluation
@supervisor = @portfolio.supervisor
admin_a = preload_next :user
admin_b = preload_next :user
Account.any_instance.stubs(:evaluation_admins).returns([admin_a, admin_b])
@evaluation_admins = @evaluation.survey.account.evaluation_admins
should_receive_emails
end
should "start in responding state" do
assert_equal 'responding', @portfolio.state
end
should "update timestamp on state advance" do
assert_nil @portfolio.state_started_at
future = Time.now + 3.days
Delorean.time_travel_to future
@portfolio.supervisor_force_end_responding
assert_equal future.to_date, @portfolio.state_started_at.to_date
@portfolio = EvaluationPortfolio.find @portfolio
assert_equal future.to_date, @portfolio.state_started_at.to_date
end
should "force to review" do
@evaluation.update_attribute :discussion, true
@portfolio.reload
assert @portfolio.supervisor_force_end_responding
assert_equal 'reviewing', @portfolio.state
end
should "force to complete" do
@evaluation.update_attribute :discussion, false
@portfolio.reload
assert @portfolio.supervisor_force_end_responding
assert_equal 'complete', @portfolio.state
end
should "move to reviewing if no pending" do
@evaluation.update_attribute :discussion, true
@portfolio.reload
assert !@portfolio.pending_evaluators.empty?
assert !@portfolio.can_end_responding?
@portfolio.pending_evaluators.each{|evaluator| EvaluationSurveyResponse.factory(:portfolio => @portfolio, :user => evaluator) }
@portfolio.reload
assert @portfolio.pending_evaluators.empty?
assert @portfolio.end_responding
assert_equal 'reviewing', @portfolio.state
end
should "move to complete if no pending" do
@evaluation.update_attribute :discussion, false
@portfolio.reload
assert !@portfolio.pending_evaluators.empty?
assert !@portfolio.can_end_responding?
@portfolio.pending_evaluators.each{|evaluator| EvaluationSurveyResponse.factory(:portfolio => @portfolio, :user => evaluator) }
@portfolio.reload
assert @portfolio.end_responding
assert_equal 'complete', @portfolio.state
end
should "start commenting if one supervisor comment" do
@portfolio.update_attribute :state, 'reviewing'
assert @portfolio.comments.empty?
assert !@portfolio.can_start_commenting?
# comment by user
Factory :comment, :user => @portfolio.evaluatee, :commentable => @portfolio
@portfolio.reload
assert !@portfolio.can_start_commenting?
# comment by system
Factory :comment, :from_system => true, :commentable => @portfolio
@portfolio.reload
assert !@portfolio.can_start_commenting?
# comment by supervisor
Factory :comment, :user => @portfolio.supervisor, :commentable => @portfolio
@portfolio.reload
assert @portfolio.start_commenting
assert_equal 'commenting', @portfolio.state
end
should "move from commenting to sign if supervisor sign" do
@portfolio.update_attribute :state, 'commenting'
assert !@portfolio.supervisor_signed?
assert !@portfolio.can_sign_advance?
@portfolio.update_attribute :supervisor_signed_at, Time.now
assert @portfolio.supervisor_signed?
assert @portfolio.sign_advance
assert_equal 'signing', @portfolio.state
end
should "move from commenting to sign if evaluatee sign" do
@portfolio.update_attribute :state, 'commenting'
assert !@portfolio.evaluatee_signed?
assert !@portfolio.can_sign_advance?
@portfolio.update_attribute :evaluatee_signed_at, Time.now
assert @portfolio.evaluatee_signed?
assert @portfolio.sign_advance
assert_equal 'signing', @portfolio.state
end
should "move from commenting to complete if both signatures" do
@portfolio.update_attribute :state, 'commenting'
assert !@portfolio.supervisor_signed?
assert !@portfolio.evaluatee_signed?
assert !@portfolio.can_sign_advance?
@portfolio.update_attribute :evaluatee_signed_at, Time.now
@portfolio.update_attribute :supervisor_signed_at, Time.now
assert @portfolio.supervisor_signed?
assert @portfolio.evaluatee_signed?
assert @portfolio.sign_advance
assert_equal 'complete', @portfolio.state
end
should "not move from signing to complete if just supervisor signature" do
@portfolio.update_attribute :state, 'signing'
@portfolio.update_attribute :supervisor_signed_at, Time.now
assert @portfolio.supervisor_signed?
assert !@portfolio.evaluatee_signed?
assert !@portfolio.can_sign_advance?
end
should "not move from signing to complete if just evaluatee signature" do
@portfolio.update_attribute :state, 'signing'
@portfolio.update_attribute :evaluatee_signed_at, Time.now
assert !@portfolio.supervisor_signed?
assert @portfolio.evaluatee_signed?
assert !@portfolio.can_sign_advance?
end
should "move from signing to complete if both signatures" do
@portfolio.update_attribute :state, 'signing'
@portfolio.update_attribute :evaluatee_signed_at, Time.now
assert !@portfolio.supervisor_signed?
assert @portfolio.evaluatee_signed?
assert !@portfolio.can_sign_advance?
@portfolio.update_attribute :supervisor_signed_at, Time.now
assert @portfolio.supervisor_signed?
assert @portfolio.evaluatee_signed?
assert @portfolio.sign_advance
assert_equal 'complete', @portfolio.state
end
context "Notification Email and Comment" do
teardown do
assert_from @portfolio.evaluation.account.notifier_email
assert_match @portfolio.evaluation.account.host, last_email.body
assert_two_part_email
end
should "send on review start" do
@portfolio.state = 'responding'
@portfolio.stubs(:no_pending_and_review_false?).returns(false)
@portfolio.stubs(:no_pending_and_review_true? ).returns(true)
assert_delivers_email do
assert_creates_comment do
assert @portfolio.end_responding
end
end
assert_bcc_to @portfolio.supervisors
end
should "send on commenting start" do
@portfolio.state = 'reviewing'
@portfolio.stubs(:supervisor_commented?).returns(true)
assert_delivers_email do
assert_creates_comment do
assert @portfolio.start_commenting
end
end
assert_bcc_to @portfolio.evaluatee_and_supervisors
end
should "send on signing start" do
@portfolio.state = 'commenting'
@portfolio.stubs(:both_signed?).returns(false)
@portfolio.stubs(:one_signed? ).returns(true)
assert_delivers_email do
assert_creates_comment do
assert @portfolio.sign_advance
end
end
assert_bcc_to @portfolio.evaluatee_and_supervisors
assert_match @portfolio.needed_signatures, last_email.body
end
should "send on complete" do
@portfolio.state = 'signing'
@portfolio.stubs(:both_signed?).returns(true)
assert_delivers_email do
assert_creates_comment do
assert @portfolio.sign_advance
end
end
assert_bcc_to @portfolio.evaluatee_and_supervisors
end
end
end
context "Sign Off" do
setup do
@portfolio = preload :evaluation_portfolio
end
should "set sign date for supervisor" do
assert_nil @portfolio.supervisor_signed_at
now = Date.today
Delorean.jump 3.days
assert_difference 'Comment.count' do
@portfolio.sign_off @portfolio.supervisor
end
assert_equal (now + 3.days), @portfolio.supervisor_signed_at.to_date
assert_nil @portfolio.evaluatee_signed_at
end
should "set sign date for admin" do
@evaluation_admin = preload :user
@evaluation_admin.expects(:in_group?).with(EVALUATIONS_ADMIN_GROUP_NAME).returns(true)
now = Date.today
Delorean.jump 3.days
assert_nil @portfolio.supervisor_signed_at
assert_difference 'Comment.count' do
@portfolio.sign_off @evaluation_admin
end
assert_equal (now + 3.days), @portfolio.supervisor_signed_at.to_date
assert_nil @portfolio.evaluatee_signed_at
end
should "set sign date for evaluatee" do
assert_nil @portfolio.evaluatee_signed_at
now = Date.today
Delorean.jump 3.days
assert_difference 'Comment.count' do
@portfolio.sign_off @portfolio.evaluatee
end
assert_equal (now + 3.days), @portfolio.evaluatee_signed_at.to_date
assert_nil @portfolio.supervisor_signed_at
end
should "not sign if not authorized" do
other_user = preload :user
assert_raise InvalidSigner do
@portfolio.sign_off other_user
end
assert_nil @portfolio.evaluatee_signed_at
assert_nil @portfolio.supervisor_signed_at
end
end
context "pending evaluators" do
setup do
@portfolio = preload :evaluation_portfolio
@evaluatee = @portfolio.evaluatee
@evaluators = 3.times.collect{ preload_next :user }
@portfolio.evaluation_deployment.group.users = @evaluators
end
should "list all with no responses" do
assert_equal @evaluators, @portfolio.pending_evaluators
end
should "list all minus self if same group" do
@portfolio.evaluation_deployment.group.users << @evaluatee
assert_equal @evaluators, @portfolio.pending_evaluators
end
should "list all minus responses" do
@response = SurveyResponse.create! :survey_deployment => @portfolio.evaluation_deployment, :survey => @portfolio.survey, :user => @evaluators.first, :evaluatee => @evaluatee
@portfolio.reload
assert_equal (@evaluators - [@evaluators.first]).map(&:id).sort, @portfolio.pending_evaluators.map(&:id).sort
end
end
should "return comment recipients" do
@portfolio = preload :evaluation_portfolio
should_receive_emails
user_a = preload_next :user
user_b = preload_next :user
Account.any_instance.stubs(:evaluation_admins).returns([user_a, user_b])
assert_equal [@portfolio.evaluatee, @portfolio.supervisor, user_a, user_b], @portfolio.comment_recipients
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment