Last active
August 29, 2015 14:06
-
-
Save aguestuser/22d2bff3003059f304c5 to your computer and use it in GitHub Desktop.
Routine for batch shift reassignment with automated scheduling obstacle detection (and override) for BK Shift on Rails
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ********************* | |
# *****CONTROLLERS***** | |
# ********************* | |
# ************************************* | |
# SHIFTS CONTROLLER | |
# app/controllers/shifts_controller.rb | |
# ************************************* | |
def batch_edit | |
if params[:ids] | |
load_shift_batch # loads @shifts | |
route_batch_edit params[:commit] | |
else | |
flash[:error] = "Oops! Looks like you didn't select any shifts to batch edit." | |
load_shifts | |
load_table | |
render 'index' | |
end | |
end | |
def route_batch_edit commit | |
query = params.extract!(:ids, :base_path).to_query | |
case commit | |
when 'Batch Edit' | |
@errors = [] | |
render 'batch_edit' | |
when 'Batch Assign' | |
redirect_to "/assignment/batch_edit?#{query}" | |
when 'Uniform Assign' | |
redirect_to "/assignment/batch_edit_uniform?#{query}" | |
end | |
end | |
def batch_update | |
old_shifts = old_shifts_from params # loads @shifts | |
new_shifts = new_shifts_from params | |
@errors = Shift.batch_update(old_shifts, new_shifts) | |
if @errors.empty? | |
flash[:success] = "Shifts successfully batch edited" | |
redirect_to @base_path | |
else | |
render "batch_edit" | |
end | |
end | |
# ***************************** | |
# ASSIGNMENTS CONTROLLER | |
# app/controllers/assignments_controller.rb | |
# ***************************** | |
#public | |
def batch_edit | |
@errors = [] | |
load_shift_batch # loads @shifts (Arr of Shifts) | |
load_assignment_batch #loads @assignments (Assignments Obj) | |
render 'batch_edit' | |
end | |
def batch_update | |
assignments = Assignments.from_params params[:wrapped_assignments] # | |
get_savable assignments | |
end | |
def batch_edit_uniform | |
@errors = [] | |
load_shift_batch # loads @shifts (Arr of Shifts) | |
load_assignment_batch #loads @assignments (Assignments Obj) | |
render 'batch_edit_uniform' | |
end | |
def batch_update_uniform | |
assignments = Assignments.new( | |
{ | |
fresh: new_uniform_assignments_from(params), | |
old: old_uniform_assignments_from(params) | |
} | |
) | |
get_savable assignments | |
end | |
# GET SAVABLE RECURSION CASE 1 | |
def resolve_obstacles | |
decisions = Assignments.decisions_from params[:decisions] | |
assignments = Assignments.from_params( JSON.parse(params[:assignments_json] ) ) | |
assignments = assignments.resolve_obstacles_with decisions | |
get_savable assignments # RECURSE | |
end | |
# GET SAVABLE RECURSION CASE 2 | |
def batch_reassign | |
assignments = Assignments.from_params params[:wrapped_assignments] | |
get_savable assignments # RECURSE | |
end | |
#private | |
# *** BATCH SAVE ROUTINE *** | |
def get_savable assignments # RECURSION HOOK | |
#input: Assignments Obj | |
#output: Assignments Obj w/ empty .with_obstacles and .requiring_reassignment Arrays | |
assignments = assignments.find_obstacles if assignments.fresh.any? | |
if assignments.with_obstacles.any? | |
request_obstacle_decisions_for assignments # WILL RECURSE | |
nil | |
elsif assignments.requiring_reassignment.any? | |
request_reassignments_for assignments # WILL RECURSE | |
nil | |
else # BASE CASE (BREAKS RECURSION) | |
old_assignments = assignments.unwrap_old | |
new_assignments = assignments.savable | |
update_savable old_assignments, new_assignments | |
end | |
end | |
# RECURSION CASE 1 | |
def request_obstacle_decisions_for assignments | |
@assignments = assignments | |
render "resolve_obstacles" # view posts to '/assignment/resolve_obstacles' | |
end | |
# RECURSION CASE 2 | |
def request_reassignments_for assignments | |
@errors = [] | |
@assignments = assignments | |
render "batch_reassign" # view posts to '/assignment/batch_reassign' | |
end | |
# *** SAVE HOOK *** | |
def update_savable old_assignments, new_assignments | |
now = now_unless_test | |
new_assignments.each{ |a| a.shift.refresh_urgency now } # will update weekly shifts to emergency and extra as appropriate | |
if batch_save? old_assignments, new_assignments | |
message = success_message_from old_assignments.count | |
email_alert = send_batch_emails new_assignments, old_assignments, current_account | |
flash[:success] = message << email_alert | |
redirect_to @base_path | |
else | |
request_batch_error_fixes old_assignments, new_assignments | |
end | |
end | |
def batch_save? old_assignments, new_assignments | |
@errors = Assignment.batch_update(old_assignments, new_assignments) | |
@errors.empty? | |
end | |
def success_message_from count | |
if count > 1 | |
"Assignments successfully batch edited" | |
else | |
"Assignment successfully edited" | |
end | |
end | |
def send_batch_emails new_assignments, old_assignments, current_account | |
email_count = Assignment.send_emails new_assignments, old_assignments, current_account | |
if email_count == 0 | |
"" | |
else | |
" -- #{email_count} emails sent" | |
end | |
end | |
def request_batch_error_fixes old_assignments, new_assignments | |
@assignments = Assignments.new({ | |
fresh: Assignments.wrap(new_assignments), | |
}) | |
render "batch_edit" | |
end | |
# BATCH PARAM PARSERS | |
def load_shift_batch | |
@shifts = Shift.where("id IN (:ids)", { ids: params[:ids] } ).order(:start).to_a | |
end | |
def load_assignment_batch | |
assignments = @shifts.map(&:assignment) | |
wrapped_assignments = Assignments.wrap assignments | |
@assignments = Assignments.new( { fresh: wrapped_assignments } ) | |
end | |
def old_assignments_from new_assignments | |
if params[:old_assignments_json] | |
load_old_assignments | |
else | |
new_assignments.map{ |ass| Assignment.find(ass.id) } | |
end | |
end | |
def load_old_assignments | |
Assignments.from_params JSON.parse(params[:old_assignments_json]) | |
end | |
def new_uniform_assignments_from params | |
attrs = Assignment.attributes_from(params[:assignment]) # Hash | |
shift_ids = params[:shift_ids].map(&:to_i) # Array | |
assignments = shift_ids.map do |shift_id| | |
attrs['shift_id'] = shift_id | |
Assignment.new(attrs) | |
end | |
Assignments.wrap(assignments) | |
end | |
def old_uniform_assignments_from params | |
assignments = params[:shift_ids].map do |shift_id| | |
attrs = Shift.find(shift_id).assignment.attributes #.reject{ |k,v| k == 'id' } | |
Assignment.new(attrs) | |
end | |
Assignments.wrap(assignments) | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ******************************* | |
# ***** CONTROLLER HELPERS ****** | |
# ******************************* | |
# ********************* | |
# ASSIGNMENTS CLASS | |
# app/helpers/assignments.rb | |
# *************************** | |
class Assignments | |
include Hashable | |
attr_accessor :fresh, :old, :with_conflicts, :with_double_bookings, :with_obstacles, :without_obstacles, :requiring_reassignment | |
def initialize options={} | |
@old = options[:old] || options[:fresh].clone # Array of WrappedAssignments | |
# NOTE: above will clone fresh options on first iteration, retain initial value of @old on subsequent (recursive) iterations | |
@fresh = options[:fresh] || [] # Array of WrapedAssignments | |
@with_conflicts = options[:with_conflicts] || [] # Arr of WrapedAssignments | |
@with_double_bookings = options[:with_double_bookings] || [] # Arr of WrapedAssignments | |
@without_obstacles = options[:without_obstacles] || [] # Arr of WrapedAssignments | |
@requiring_reassignment = options[:requiring_reassignment] || [] #Arr of WrapedAssignments | |
end | |
def with_obstacles | |
@with_conflicts + @with_double_bookings | |
end | |
def find_obstacles | |
#input: @fresh (implicit - must be loaded) Arr of Assignments | |
#does: | |
# sorts assignments from fresh into 3 Arrays: | |
# (1) @with_conflicts: Arr of Assignments with conflicts | |
# (2) @with_double_bookings: Arr of Assignmens with double bookings | |
# (3) @without_obstacles: Arr of Assignments with neither conflicts nor double bookings | |
# clears @fresh | |
# output: Assignments Obj | |
@fresh.each do |wrapped_assignment| | |
assignment = wrapped_assignment.assignment | |
if assignment.conflicts.any? | |
@with_conflicts.push wrapped_assignment | |
elsif assignment.double_bookings.any? | |
@with_double_bookings.push wrapped_assignment | |
else | |
@without_obstacles.push wrapped_assignment | |
end | |
end | |
@fresh = [] | |
self | |
end | |
def resolve_obstacles_with decisions | |
#input: @with_conflicts (implicit) Array of Assignments, @with_double_bookings (implicit) Array of Assignments | |
#does: | |
# builds array of assignments with obstacles | |
# based on user decisions, sorts them into either | |
# (1) assignments @requiring_reassignment | |
# (2) assignments @without_obstacles (after clearing obstacles from assignment object) | |
# clears @with_conflicts, @with_double_bookings, returns new state of Assignments Object | |
with_obstacles = self.with_obstacles | |
with_obstacles.each_with_index do |wrapped_assignment, i| | |
case decisions[i] | |
when 'Accept' # move to @requiring_reassignment | |
self.requiring_reassignment.push wrapped_assignment | |
when 'Override' # resolve obstacle and move to @without_obstacles | |
wrapped_assignment.assignment.resolve_obstacle | |
self.without_obstacles.push wrapped_assignment | |
end | |
end | |
self.with_conflicts = [] | |
self.with_double_bookings = [] | |
self | |
end | |
def savable | |
#input: self (implicit) Assignments Obj, @without_obstacles (implicit) Array of WrappedAssignments | |
#does: restores Arr of WrappedAssignments without obstacles to original sort and returns unwrapped Arr of Assignments | |
#output: Array of Assignments | |
savable = @without_obstacles.sort_by{ |wrapped_assignment| wrapped_assignment.index } | |
savable.map(&:assignment) | |
end | |
def unwrap_old | |
#input: self (implicit), @old (implicit) Array of WrappedAssignments | |
#output: Array of Assignments | |
@old.map(&:assignment) | |
end | |
# def to_params | |
# self.to_json.to_query 'assignments' | |
# end | |
# CLASS METHODS | |
def Assignments.wrap assignments | |
assignments.each_with_index.map { |assignment, i| WrappedAssignment.new(assignment, i) } | |
end | |
def Assignments.wrap_with_indexes assignments, indexes | |
assignments.each_with_index.map { |assignment, i| WrappedAssignment.new(assignment, indexes[i]) } | |
end | |
def Assignments.from_params param_hash | |
#input Hash of type | |
# { 'fresh': [ | |
# { | |
# id: Num, | |
# assignment:{ | |
# 'id': Num, | |
# 'rider_id': Num, | |
# 'shift_id': Num, | |
# ...(other Assignment attributes) | |
# } | |
# } | |
# 'id': Num | |
# ], | |
# 'old': [ | |
# { | |
# 'id': Num, | |
# 'assignment':{ | |
# ...(Assignment attributes)... | |
# } | |
# } | |
# ].... (other Arrays of WrappedAssignment attributes) | |
# } | |
#does: parses params hash into WrappedAssignments that can be passed as options to initialize an Assignments object | |
#output: Assignments Obj | |
options = {} | |
param_hash.each do |key, wrapped_attr_arr| | |
index_arr = wrapped_attr_arr.map{ |wrapped_attrs| wrapped_attrs['index'] } | |
attr_arr = wrapped_attr_arr.map{ |wrapped_attrs| wrapped_attrs['assignment'] } | |
assignments = attr_arr.map{ |attrs| Assignment.new(attrs) } | |
options[key.to_sym] = Assignments.wrap_with_indexes assignments, index_arr | |
end | |
Assignments.new(options) | |
end | |
def Assignments.decisions_from params | |
#input params[:decisions] (must be present) | |
decisions = [] | |
params.each { |k,v| decisions[k.to_i] = v } | |
decisions | |
end | |
class WrappedAssignment | |
attr_accessor :assignment, :index | |
def initialize assignment, index | |
@assignment = assignment | |
@index = index | |
end | |
end | |
end | |
# ****************************************** | |
# RIDER-SHIFTS CLASS | |
# app/helpers/rider_shifts.rb | |
# ************************************************ | |
class RiderShifts | |
attr_reader :hash | |
URGENCIES = [ :emergency, :extra, :weekly ] | |
def initialize assignments | |
@hash = hash_from assignments | |
# puts ">>>> @hash" | |
# pp @hash | |
end | |
private | |
def hash_from assignments | |
#input: Arr of assignments | |
#output: Hash of Hashes of type: | |
# { Num<rider_id>: | |
# { rider: Rider, | |
# emergency_ shifts: { | |
# shifts: Arr of Shifts, | |
# restaurants: Arr of Restaurants | |
# } | |
# extra_shifts: { | |
# shifts: Arr of Shifts | |
# restaurants: Arr of Restaurants | |
# } | |
# } | |
grouped_by_rider = group_by_rider assignments | |
with_parsed_rider_and_shift = parse_rider_and_shifts grouped_by_rider | |
grouped_by_urgency = group_by_urgency with_parsed_rider_and_shift | |
with_restaurants = insert_restaurants grouped_by_urgency | |
# sorted_by_date = sort_by_date grouped_by_urgency | |
# with_restaurants = insert_restaurants sorted_by_date | |
end | |
def group_by_rider assignments | |
#input: Array of type: [ Assignment, Assignment, ...] | |
#output: Hash of type: { Num(rider_id): Arr of Assignments } | |
assignments.group_by{ |a| a.rider.id } | |
end | |
def parse_rider_and_shifts assignments | |
#input: Hash of type: { Num(rider_id): Arr of Assignments } | |
#output: Hash of Hashes of type: { Num<rider_id>: { rider: Rider, shifts: Arr of Shifts } } | |
hash = {} | |
assignments.each do |id,assignments| | |
hash[id] = { rider: assignments.first.rider, shifts: assignments.map(&:shift) } | |
end | |
hash | |
end | |
def group_by_urgency assignments | |
#input: Hash of Hashes of type: { Num<rider_id>: { rider: Rider, shifts: Arr of Shifts } } | |
#output: Hash of Hashes of type: | |
# { Num<rider_id>: | |
# { rider: Rider, emergency: Arr of Shifts, extra: Arr of Shifts, weekly: Arr of Shifts } | |
# } | |
hash = {} | |
assignments.each do |id, rider_hash| | |
sorted_hash = rider_hash[:shifts].group_by{ |s| s.urgency.text.downcase.to_sym } | |
hash[id] = { rider: rider_hash[:rider] } | |
URGENCIES.each { |urgency| hash[id][urgency] = sorted_hash[urgency] } | |
end | |
hash | |
end | |
def sort_by_date assignments | |
hash = {} | |
assignments.each do |id, rider_hash| | |
URGENCIES.each do |urgency| | |
rider_hash[urgency].sort_by!{ |shift| shift.start } if rider_hash[urgency] | |
end | |
end | |
hash | |
end | |
def insert_restaurants assignments | |
#input: Hash of Hashes of type: | |
# { Num<rider_id>: | |
# { rider: Rider, emergency: Arr of Shifts, extra: Arr of Shifts } | |
# } | |
#output: Hash of Hashes of type: | |
# { Num<rider_id>: | |
# { rider: Rider, | |
# emergency_ shifts: { | |
# shifts: Arr of Shifts, | |
# restaurants: Arr of Restaurants | |
# } | |
# extra_shifts: { | |
# shifts: Arr of Shifts | |
# restaurants: Arr of Restaurants | |
# } | |
# } | |
hash = {} | |
assignments.each do |id, rider_hash| | |
hash[id] = { rider: rider_hash[:rider] } | |
URGENCIES.each do |urgency| | |
shifts = rider_hash[urgency] || [] | |
restaurants = parse_restaurants shifts | |
hash[id][urgency] = urgency_hash_from shifts, restaurants | |
end | |
end | |
hash | |
end | |
def urgency_hash_from shifts, restaurants | |
{ shifts: shifts , restaurants: restaurants } | |
end | |
def parse_restaurants shifts | |
shifts.map{ |shift| shift.restaurant }.uniq | |
end | |
end | |
# ******************************* | |
# ******** MAILER HELPERS ******* | |
# ******************************* | |
# ****************************************** | |
# DELEGATION EMAIL HELPER | |
# app/helpers/delegation_email_helper.rb | |
# ****************************************** | |
class DelegationEmailHelper | |
attr_accessor :subject, :offering, :confirmation_request | |
def initialize shifts, type | |
plural = shifts.count > 1 | |
adj = type.to_s | |
noun = noun_from type, plural | |
@subject = subject_from adj, noun, shifts, type | |
@offering = offering_from adj, noun, type | |
@confirmation_request = conf_req_from noun, type | |
end | |
private | |
def noun_from type, plural | |
str = type == :weekly ? "schedule" : "shift" | |
str << "s" if plural && type != :weekly | |
str | |
end | |
def subject_from adj, noun, shifts, type | |
"[" << adj.upcase << " " << noun.upcase << "] " << shift_descr_from(shifts, type) | |
end | |
def shift_descr_from shifts, type | |
case type | |
when :weekly | |
"-- PLEASE CONFIRM BY SUNDAY" | |
when :extra | |
'-- CONFIRMATION REQUIRED' | |
when :emergency | |
"-- SHIFT DETAILS ENCLOSED" | |
end | |
end | |
def offering_from adj, noun, type | |
offer_prefix = offer_prefix_from type | |
"#{offer_prefix} #{adj} #{noun}:" | |
end | |
def offer_prefix_from type | |
if type == :emergency | |
"As per our conversation, you are confirmed for the following" | |
else | |
"We'd like to offer you the following" | |
end | |
end | |
def conf_req_from noun, type | |
if type == :emergency | |
"Have a great shift!" | |
else | |
conf_time = conf_time_from type | |
"Please confirm whether you can work the #{noun} by #{conf_time}" | |
end | |
end | |
def conf_time_from type | |
type == :weekly ? "12pm this Sunday" : "2pm tomorrow" | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ************************** | |
# RIDER MAILER | |
# app/mailers/rider_mailer.rb | |
class RiderMailer < ActionMailer::Base | |
default from: "[email protected]" | |
helper_method :protect_against_forgery? | |
def delegation_email rider, shifts, restaurants, account, type | |
require 'delegation_email_helper' | |
@rider = rider | |
@shifts = shifts | |
@restaurants = restaurants | |
@staffer = account.user #, staffer_from account | |
helper = DelegationEmailHelper.new shifts, type | |
@salutation = "Dear #{rider.first_name}:" | |
@offering = helper.offering | |
@confirmation_request = helper.confirmation_request | |
mail(to: rider.email, subject: helper.subject) | |
end | |
# ... | |
private | |
def protect_against_forgery? | |
false | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/ ******************************** | |
/ ****** MAILER TEMPLATES ******* | |
/ ******************************** | |
/ **************************************** | |
/ RIDER MAILER LAYOUT | |
/ app/views/layouts/rider_mailer.html.haml | |
/ **************************************** | |
!!! | |
%html | |
%head | |
%meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/ | |
:css | |
table { | |
border-collapse:collapse; | |
margin-left: 2em; | |
} | |
th { | |
background-color: lightgray; | |
} | |
th, td { | |
border: 1px solid black; | |
margin: 0px; | |
padding: .5em; | |
} | |
.underline { | |
text-decoration: underline; | |
} | |
%body | |
%p | |
= @salutation | |
= yield | |
/ **************************************** | |
/ DELEGATION EMAIL TEMPLATE | |
/ app/views/layouts/rider_mailer.html.haml | |
/ **************************************** | |
%p | |
= @offering | |
%table | |
%tr | |
%th | |
Time | |
%th | |
Restaurant | |
- @shifts.each do |shift| | |
%tr | |
%td | |
= shift.table_time | |
%td | |
= shift.restaurant.name | |
%p | |
= @confirmation_request | |
= render 'mailer/signature' | |
= render 'briefs' | |
= render 'reminders' | |
/ **************************************** | |
/ SIGNATURE PARTIAL | |
/ app/views/mailer/_signature.html.haml | |
/ **************************************** | |
- #Args: @staffer | |
%p | |
%p | |
Best, | |
%p | |
= @staffer.name | |
%br/ | |
= @staffer.title | |
%br/ | |
BK Shift, LLC | |
%br/ | |
= @staffer.phone | |
%br/ | |
= mail_to @staffer.email | |
/ **************************************** | |
/ BRIEFS PARTIAL | |
/ app/views/rider_mailer/_briefs.html.haml | |
/ **************************************** | |
%strong.underline | |
Restaurant Briefs: | |
- @shifts.each do |shift| | |
- r = shift.restaurant | |
%p | |
%strong | |
#{r.name}: | |
= r.brief | |
%br/ | |
%strong | |
Location: | |
= r.location.full_address | |
/ **************************************** | |
/ REMINDERS PARTIAL | |
/ app/views/rider_mailer/_reminders.html.haml | |
/ **************************************** | |
%strong.underline | |
Reminders: | |
%ul | |
%li | |
Don’t forget to text 347-460-6484 2 hrs before your shift | |
%li | |
Please arrive 15 minutes before your scheduled shift | |
%li | |
Please note that the DOT requires the use of helmets, front white light, back red light and a bell and/or whistle. | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ******************* | |
# ***** MODELS ****** | |
# ******************* | |
# ************************ | |
# SHIFT MODEL | |
# app/models/shift.rb | |
# ************************ | |
# == Schema Information | |
# | |
# Table name: shifts | |
# | |
# id :integer not null, primary key | |
# restaurant_id :integer | |
# start :datetime | |
# end :datetime | |
# period :string(255) | |
# urgency :string(255) | |
# billing_rate :string(255) | |
# notes :text | |
# created_at :datetime | |
# updated_at :datetime | |
# | |
class Shift < ActiveRecord::Base | |
include Timeboxable, BatchUpdatable | |
belongs_to :restaurant | |
has_one :assignment, dependent: :destroy #inverse_of: :shift | |
accepts_nested_attributes_for :assignment | |
classy_enum_attr :billing_rate | |
classy_enum_attr :urgency | |
validates :restaurant_id, :billing_rate, :urgency, | |
presence: true | |
def build_associations | |
self.assignment = Assignment.new | |
end | |
def rider | |
self.assignment.rider | |
end | |
def assigned? #output: bool | |
!self.assignment.rider.nil? | |
end | |
def assign_to(rider, status=:proposed) | |
#input: Rider, AssignmentStatus(Symbol) | |
#output: self.Assignment | |
params = { rider_id: rider.id, status: status } | |
if self.assigned? | |
self.assignment.update params | |
else | |
self.assignment = Assignment.create! params | |
end | |
end | |
def unassign | |
self.assignment.update(rider_id: nil, status: :unassigned) if self.assigned? | |
end | |
def conflicts_with? conflict | |
( conflict.end >= self.end && conflict.start < self.end ) || | |
( conflict.start <= self.start && conflict.end > self.start ) | |
# ie: if the conflict under examination overlaps with this conflict | |
end | |
def double_books_with? shift | |
( shift.end >= self.end && shift.start < self.end ) || | |
( shift.start <= self.start && shift.end > self.start ) | |
# ie: if the shift under examination overlaps with this shift | |
end | |
def refresh_urgency now | |
#input self (implicit), DateTime Obj | |
#side-effects: updates shift's urgency attribute | |
#output: self | |
start = self.start | |
send_urgency( parse_urgency( now, start ) ) if start > now | |
self | |
end | |
private | |
def parse_urgency now, start | |
#input: Datetime, Datetime | |
#output: Symbol | |
next_week = start.beginning_of_week != now.beginning_of_week | |
time_gap = start - now | |
if next_week | |
:weekly | |
elsif time_gap <= 36.hours | |
:emergency | |
else | |
:extra | |
end | |
end | |
def send_urgency urgency | |
#input: Symbol | |
self.update(urgency: urgency) | |
end | |
end | |
# ************************ | |
# RIDER MODEL | |
# app/models/rider.rb | |
# ************************ | |
# == Schema Information | |
# | |
# Table name: riders | |
# | |
# id :integer not null, primary key | |
# active :boolean | |
# created_at :datetime | |
# updated_at :datetime | |
# | |
class Rider < ActiveRecord::Base | |
include User, Contactable, Equipable, Locatable # app/models/concerns/ | |
#nested attributes | |
has_one :qualification_set, dependent: :destroy | |
accepts_nested_attributes_for :qualification_set | |
has_one :skill_set, dependent: :destroy | |
accepts_nested_attributes_for :skill_set | |
has_one :rider_rating, dependent: :destroy | |
accepts_nested_attributes_for :rider_rating | |
#associations | |
has_many :assignments | |
has_many :shifts, through: :assignments | |
has_many :conflicts | |
validates :active, inclusion: { in: [ true, false ] } | |
scope :testy, -> { joins(:contact).where("contacts.email = ?", "[email protected]").first } | |
scope :active, -> { joins(:contact).where(active: true).order("contacts.name asc") } | |
scope :inactive, -> { joins(:contact).where(active: false).order("contacts.name asc") } | |
#public methods | |
def name | |
self.contact.name | |
end | |
def shifts_on(date) #input: date obj, #output Arr of Assignments (possibly empty) | |
self.shifts.where( start: (date.beginning_of_day..date.end_of_day) ) | |
end | |
def conflicts_on(date) #input: date obj, #output Arr of Conflicts (possibly empty) | |
self.conflicts.where( start: (date.beginning_of_day..date.end_of_day) ) | |
end | |
def conflicts_between start_t, end_t | |
#input: Rider(self/implicit), Datetiem, Datetime | |
#does: builds an array of conflicts belonging to rider within date range btw/ start_t and end_t | |
#output: Arr | |
conflicts = self.conflicts | |
.where( "start > :start AND start < :end", { start: start_t, :end => end_t } ) | |
.order("start asc") | |
end | |
#class methods | |
def Rider.select_options | |
Rider.all.joins(:contact).order("contacts.name asc").map{ |r| [ r.name, r.id ] } | |
end | |
def Rider.email_conflict_requests rider_conflicts, week_start, account | |
#input: RiderConflicts, Datetime, Account | |
#output: Str (empty if no emails sent, email alert if emails sent) | |
count = 0 | |
rider_conflicts.arr.each do |hash| | |
RiderMailer.request_conflicts(hash[:rider], hash[:conflicts], week_start, account).deliver | |
count += 1 | |
end | |
alert = count > 0 ? "#{count} conflict requests successfully sent" : "" | |
end | |
end | |
# ************************ | |
# ASSIGNMENT MODEL | |
# app/models/rider.rb | |
# ************************ | |
# == Schema Information | |
# | |
# Table name: assignments | |
# | |
# id :integer not null, primary key | |
# shift_id :integer | |
# rider_id :integer | |
# status :string(255) | |
# created_at :datetime | |
# updated_at :datetime | |
# override_conflict :boolean | |
# override_double_booking :boolean | |
# | |
class Assignment < ActiveRecord::Base | |
include BatchUpdatable | |
belongs_to :shift #, inverse_of: :assignment | |
belongs_to :rider | |
classy_enum_attr :status, allow_nil: true, enum: 'AssignmentStatus' | |
before_validation :set_status, if: :status_nil? | |
validates :status, presence: true | |
validate :no_emergency_shift_delegation | |
#instance methods | |
def no_emergency_shift_delegation | |
if self.shift | |
if self.shift.urgency == :emergency | |
errors.add(:base, 'Emergency shifts cannot be delegated') unless self.status != :delegated | |
end | |
end | |
end | |
def conflicts | |
#input: self (implicit) | |
#output: Arr of Conflicts | |
if self.rider.nil? | |
[] | |
else | |
rider_conflicts = get_rider_conflicts | |
rider_conflicts.select { |conflict| self.shift.conflicts_with? conflict } | |
end | |
end | |
def double_bookings | |
rider_shifts = get_rider_shifts | |
if self.rider.nil? | |
[] | |
else | |
rider_shifts.select { |shift| self.shift.double_books_with? shift } | |
end | |
end | |
def resolve_obstacle | |
self.conflicts.each(&:destroy) if self.conflicts.any? | |
self | |
end | |
def save_success_message | |
self.rider.nil? ? "Assignment updated (currently unassigned)." : "Assignment updated (Rider: #{self.rider.contact.name}, Status: #{self.status.text})" | |
end | |
def try_send_email old_assignment, sender_account | |
if self.status == :delegated && ( old_assignment.status != :delegated || old_assignment.rider != self.rider ) | |
send_email_from sender_account | |
true | |
else | |
false | |
end | |
end | |
#Class Methods | |
def Assignment.send_emails new_assignments, old_assignments, sender_account | |
#input: assignments <Arr of Assignments>, old_assignments <Arr of Assignments>, Account | |
#does: | |
# (1) constructs array of newly delegated shifts | |
# (2) parses list of shifts into sublists for each rider | |
# (3) parses list of shifts for restaurants | |
# (4) [ SIDE EFFECT ] sends batch shift delegation email to each rider using params built through (1), (2), and (3) | |
#output: Int (count of emails sent) | |
# delegations = Assignment.delegations_from new_assignments, old_assignments # (1) | |
# rider_shifts = RiderShifts.new(delegations).hash #(2), (3) | |
emailable_shifts = Assignment.emailable new_assignments, old_assignments | |
rider_shifts = RiderShifts.new(emailable_shifts).hash #(2), (3) | |
count = 0 | |
rider_shifts.values.each do |rider_hash| # (4) | |
[:emergency, :extra, :weekly].each do |urgency| | |
if rider_hash[urgency][:shifts].any? | |
Assignment.send_email rider_hash, urgency, sender_account | |
count += 1 | |
end | |
end | |
end | |
count | |
end | |
def Assignment.send_email rider_hash, urgency, sender_account | |
RiderMailer.delegation_email( | |
rider_hash[:rider], | |
rider_hash[urgency][:shifts], | |
rider_hash[urgency][:restaurants], | |
sender_account, | |
urgency | |
).deliver | |
end | |
def Assignment.delegations_from new_assignments, old_assignments | |
#input: Arr of Assignments, Arr of Assignments | |
#does: builds array of assignments that were newly delegated when being updated from second argument to first | |
#output: Arr of Assignments | |
new_assignments.select.with_index do |a, i| | |
a.status == :delegated && ( old_assignments[i].status != :delegated || old_assignments[i].rider != a.rider ) | |
end | |
end | |
def Assignment.emailable new_assignments, old_assignments | |
#input: Arr of Assignments, Arr of Assignments | |
#does: builds array of assignments that were newly delegated when being updated from second argument to first | |
#output: Arr of Assignments | |
# raise ( "NEW ASSIGNMENTS: " + new_assignments.inspect + "OLD ASSIGNMENTS: " + old_assignments.inspect ) | |
new_assignments.select.with_index do |a, i| | |
if a.status == :delegated | |
old_assignments[i].status != :delegated || old_assignments[i].rider != a.rider | |
elsif a.status == :confirmed | |
# raise ( old_assignments[i].rider != a.rider ).inspect | |
val = ( a.shift.urgency == :emergency && ( old_assignments[i].status != :confirmed || old_assignments[i].rider != a.rider ) ) | |
# raise val.inspect | |
else | |
false | |
end | |
# a.status == :delegated && ( old_assignments[i].status != :delegated || old_assignments[i].rider != a.rider ) || | |
# a.status == :confirmed && ( old_assignments[i].status != :confirmed || old_assignments[i].rider != a.rider ) | |
end | |
end | |
private | |
#instance method helpers | |
def status_nil? | |
self.status.nil? | |
end | |
def set_status | |
self.status = :unassigned | |
end | |
def get_rider_conflicts | |
self.rider.conflicts_on self.shift.start | |
end | |
def get_rider_shifts | |
if self.rider | |
self.rider.shifts_on(self.shift.start).reject{ |s| s.id == self.shift.id } | |
else | |
[] | |
end | |
end | |
def send_email_from sender_account | |
RiderMailer.delegation_email(self.rider, [ self.shift ], [ self.shift.restaurant ], sender_account).deliver | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ********************** | |
# ROUTES | |
# app/config/routes.rb | |
# ********************** | |
BksOnRails::Application.routes.draw do | |
# ... | |
get 'shift/batch_edit' => 'shifts#batch_edit' | |
post 'shift/batch_edit' => 'shifts#batch_update' | |
get 'assignment/batch_edit' => 'assignments#batch_edit' | |
post 'assignment/batch_edit' => 'assignments#batch_update' | |
get 'assignment/batch_edit_uniform' => 'assignments#batch_edit_uniform' | |
post 'assignment/batch_edit_uniform' => 'assignments#batch_update_uniform' | |
get 'assignment/resolve_obstacles' => 'assignments#request_obstacle_decisions' | |
post 'assignment/resolve_obstacles' => 'assignments#resolve_obstacles' | |
post 'assignment/batch_reassign' => 'assignments#batch_reassign' | |
# ... | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ********************************* | |
# ********** SPEC MACROS ********** | |
# ********************************* | |
# ************************************************ | |
# BATCH ASSIGNMENT SPEC MACROS | |
# app/spec/support/shift_request_macros.rb | |
# ************************************************ | |
module ShiftRequestMacros | |
# ... | |
def filter_shifts_by_time_inclusively | |
#set start filter | |
select '2011', from: 'filter_start_year' | |
select 'January', from: 'filter_start_month' | |
select '1', from: 'filter_start_day' | |
#set end filter | |
select '2017', from: 'filter_end_year' | |
select 'January', from: 'filter_end_month' | |
select '1', from: 'filter_end_day' | |
click_button 'Filter' | |
end | |
# ... | |
def check_batch_assign_uri | |
expect(current_path).to eq "/assignment/batch_edit" | |
expect(URI.parse(current_url).to_s).to include("&ids[]=#{batch[0].id}&ids[]=#{batch[1].id}&ids[]=#{batch[2].id}") | |
end | |
def check_uniform_assign_uri | |
expect(current_path).to eq "/assignment/batch_edit_uniform" | |
expect(URI.parse(current_url).to_s).to include("&ids[]=#{batch[0].id}&ids[]=#{batch[1].id}&ids[]=#{batch[2].id}") | |
end | |
def check_batch_assign_select_values rider, status | |
rider_id_selector = "#wrapped_assignments_fresh__assignment_rider_id" | |
status_selector = "#wrapped_assignments_fresh__assignment_status" | |
expect(page.within("#assignments_fresh_0"){ find(rider_id_selector).find("option[selected]").text }).to eq rider.name | |
expect(page.within("#assignments_fresh_1"){ find(rider_id_selector).find("option[selected]").text }).to eq rider.name | |
expect(page.within("#assignments_fresh_2"){ find(rider_id_selector).find("option[selected]").text }).to eq rider.name | |
expect(page.within("#assignments_fresh_0"){ find(status_selector).find("option[selected]").text }).to eq status | |
expect(page.within("#assignments_fresh_1"){ find(status_selector).find("option[selected]").text }).to eq status | |
expect(page.within("#assignments_fresh_2"){ find(status_selector).find("option[selected]").text }).to eq status | |
end | |
def assign_batch_to rider, status | |
3.times do |n| | |
page.within("#assignments_fresh_#{n}") do | |
find("#wrapped_assignments_fresh__assignment_rider_id").select rider.name | |
find("#wrapped_assignments_fresh__assignment_status").select status | |
end | |
end | |
click_button 'Save changes' | |
end | |
def check_uniform_assign_shift_list rider, status | |
expect(page.within("#shifts"){ find("h3").text }).to eq "Shifts" | |
expect(page.all("#shifts_0 .shift_box")[0].text).to eq "#{batch[0].table_time} @ #{restaurant.name}" | |
expect(page.all("#shifts_0 .shift_box")[1].text).to eq "Assigned to: #{rider.name} [#{status}]" | |
expect(page.all("#shifts_1 .shift_box")[0].text).to eq "#{batch[1].table_time} @ #{restaurant.name}" | |
expect(page.all("#shifts_1 .shift_box")[1].text).to eq "Assigned to: #{rider.name} [#{status}]" | |
expect(page.all("#shifts_2 .shift_box")[0].text).to eq "#{batch[2].table_time} @ #{restaurant.name}" | |
expect(page.all("#shifts_2 .shift_box")[1].text).to eq "Assigned to: #{rider.name} [#{status}]" | |
end | |
def check_uniform_assign_select_values | |
expect(page.within("#assignment_form"){ find("#assignment_rider_id").has_css?("option[selected]") } ).to eq false | |
expect(page.within("#assignment_form"){ find("#assignment_status").find("option[selected]").text }).to eq 'Proposed' | |
end | |
def uniform_assign_batch_to rider, status | |
page.find("#assignment_rider_id").select rider.name | |
page.find("#assignment_status").select status | |
click_button 'Save changes' | |
end | |
def check_assignments_with_conflicts_list conflict_list_indices, batch_indices | |
expect(page.within("#assignments_with_conflicts"){ find("h3").text }).to eq "Assignments With Conflicts" | |
conflict_list_indices.each_with_index do |i,j| | |
batch_index = batch_indices[j] | |
expect(page.all("#assignments_with_conflicts_#{i} .shift_box")[0].text).to eq "#{batch[batch_index].table_time} @ #{batch[batch_index].restaurant.name}" | |
expect(page.all("#assignments_with_conflicts_#{i} .shift_box")[1].text).to eq "Assigned to: #{other_rider.name} [Proposed]" | |
expect(page.all("#assignments_with_conflicts_#{i} .shift_box")[2].text).to eq conflicts[batch_index].table_time | |
expect(page.find("#decisions_#{i}_Accept")).to be_checked | |
expect(page.find("#decisions_#{i}_Override")).to_not be_checked | |
end | |
end | |
def check_assignments_with_double_booking_list double_booking_list_indices, batch_indices | |
expect(page.within("#assignments_with_double_bookings"){ find("h3").text }).to eq "Assignments With Double Bookings" | |
double_booking_list_indices.each_with_index do |i,j| | |
batch_index = batch_indices[j] | |
expect(page.all("#assignments_with_double_bookings_#{i} .shift_box")[0].text).to eq "#{batch[batch_index].table_time} @ #{batch[batch_index].restaurant.name}" | |
expect(page.all("#assignments_with_double_bookings_#{i} .shift_box")[1].text).to eq "Assigned to: #{other_rider.name} [Proposed]" | |
expect(page.all("#assignments_with_double_bookings_#{i} .shift_box")[2].text).to eq "#{double_bookings[batch_index].table_time} @ #{double_bookings[batch_index].restaurant.name}" | |
expect(page.find("#decisions_0_Accept")).to be_checked | |
expect(page.find("#decisions_0_Override")).to_not be_checked | |
end | |
end | |
def check_without_obstacles_list list_indices, batch_indices | |
#input: Array of Nums (indices of assignments_without_obstacles Arr to check for), Array of Nums (indices of batch Shifts Arr to retrieve values from) | |
expect(page.within("#assignments_without_obstacles"){ find("h3").text }).to eq "Assignments Without Obstacles" | |
list_indices.each_with_index do |i,j| | |
batch_index = batch_indices[j] | |
expect(page.all("#assignments_without_obstacles_#{i} .shift_box")[0].text).to eq "#{batch[batch_index].table_time} @ #{restaurant.name}" | |
expect(page.all("#assignments_without_obstacles_#{i} .shift_box")[1].text).to eq "Assigned to: #{other_rider.name} [Proposed]" | |
end | |
end | |
def check_reassign_single_shift_list rider, status, batch_index | |
expect(page.within("#assignments_requiring_reassignment"){ find("h3").text }).to eq "Assignments Requiring Reassignment" | |
expect(page.find("#assignments_requiring_reassignment_0 .shift_box").text).to eq "#{batch[batch_index].table_time} @ #{batch[batch_index].restaurant.name}" | |
expect(page.within("#assignments_requiring_reassignment_0"){ | |
find("#wrapped_assignments_fresh__assignment_rider_id").find("option[selected]").text | |
}).to eq rider.name | |
expect(page.within("#assignments_requiring_reassignment_0"){ | |
find("#wrapped_assignments_fresh__assignment_status").find("option[selected]").text | |
}).to eq status | |
end | |
def reassign_single_shift_to rider, status | |
page.within("#assignments_requiring_reassignment_0") { find("#wrapped_assignments_fresh__assignment_rider_id").select rider.name } | |
page.within("#assignments_requiring_reassignment_0") { find("#wrapped_assignments_fresh__assignment_status").select status } | |
click_button 'Save changes' | |
end | |
def check_reassigned_shift_values rider, status | |
expect(page.find("#row_1_col_3").text).to eq rider.name | |
expect(page.find("#row_2_col_3").text).to eq rider.name | |
expect(page.find("#row_3_col_3").text).to eq rider.name | |
expect(page.find("#row_1_col_4").text).to eq status | |
expect(page.find("#row_2_col_4").text).to eq status | |
expect(page.find("#row_3_col_4").text).to eq status | |
end | |
def check_reassigned_shift_values_after_accepting_obstacle rider_1, rider_2, status | |
expect(page.find("#row_1_col_3").text).to eq rider_2.name | |
expect(page.find("#row_2_col_3").text).to eq rider_1.name | |
expect(page.find("#row_3_col_3").text).to eq rider_1.name | |
expect(page.find("#row_1_col_4").text).to eq status | |
expect(page.find("#row_2_col_4").text).to eq status | |
expect(page.find("#row_3_col_4").text).to eq status | |
end | |
def select_batch_assign_shifts_from_grid | |
page.within("#row_1_col_6"){ find("#ids_").set true } | |
page.within("#row_1_col_8"){ find("#ids_").set true } | |
page.within("#row_1_col_10"){ find("#ids_").set true } | |
end | |
def check_reassigned_shift_values_in_grid rider, status_code | |
expect(page.find("#row_1_col_6").text).to eq "#{rider.short_name} #{status_code}" | |
expect(page.find("#row_1_col_8").text).to eq "#{rider.short_name} #{status_code}" | |
expect(page.find("#row_1_col_10").text).to eq "#{rider.short_name} #{status_code}" | |
end | |
def load_batch | |
let(:start_t){ Time.zone.local(2014,1,1,12) } | |
let(:end_t){ Time.zone.local(2014,1,1,18) } | |
let!(:batch)do | |
3.times.map do |n| | |
FactoryGirl.build(:shift, :with_restaurant, restaurant: restaurant, start: start_t + n.days, :end => end_t + n.days) | |
end | |
end | |
end | |
def load_conflicts | |
let(:conflicts) do | |
3.times.map do |n| | |
FactoryGirl.build(:conflict, :with_rider, rider: other_rider, start: batch[n].start, :end => batch[n].end) | |
end | |
end | |
end | |
def load_double_bookings | |
let(:double_bookings) do | |
3.times.map do |n| | |
FactoryGirl.build(:shift, :with_restaurant, restaurant: restaurant, start: batch[n].start, :end => batch[n].end) | |
end | |
end | |
end | |
def load_free_rider | |
let!(:free_rider){ FactoryGirl.create(:rider) } | |
end | |
end | |
# ************************************************ | |
# RIDER MAILER SPEC MACROS | |
# app/spec/support/rider_mailer_macros.rb | |
# ************************************************ | |
module RiderMailerMacros | |
def load_staffers | |
let(:tess){ FactoryGirl.create(:staffer, :tess) } | |
let(:justin){ FactoryGirl.create(:staffer, :justin) } | |
end | |
def load_delegation_scenario | |
let!(:rider){ FactoryGirl.create(:rider) } | |
let!(:restaurant){ FactoryGirl.create(:restaurant) } | |
let(:now){ Time.zone.local(2014,1,6,11) } | |
let(:start_t){ now + 1.hour } | |
let(:end_t){ now + 7.hours } | |
# let(:start_t){ Time.zone.now.beginning_of_day + 12.hours } | |
# let(:end_t){ Time.zone.now.beginning_of_day + 18.hours } | |
let(:extra_shift){ FactoryGirl.create(:shift, :with_restaurant, restaurant: restaurant, start: start_t + 3.days, :end => end_t + 3.days) } | |
let(:emergency_shift){ FactoryGirl.create(:shift, :with_restaurant, restaurant: restaurant, start: start_t + 1.day, :end => end_t + 1.day) } | |
before do | |
rider.contact.update(name: 'A'*10) | |
restaurant.mini_contact.update(name: 'A'*10) | |
end | |
end | |
def load_batch_delegation_scenario | |
let!(:other_rider){ FactoryGirl.create(:rider) } | |
let!(:other_restaurant){ FactoryGirl.create(:restaurant) } | |
let(:extra_shifts) do | |
4.times.map do |n| | |
this_restaurant = is_even?(n) ? restaurant : other_restaurant #even shifts belong to restaurant, odd to other_restaurant | |
FactoryGirl.create(:shift, :with_restaurant, restaurant: this_restaurant, start: start_t + (n+3).days, :end => end_t + (n+3).days) | |
end | |
end | |
let(:emergency_shifts) do | |
4.times.map do |n| | |
m = n < 2 ? n*6 : (6*n+12) | |
this_restaurant = is_even?(n) ? restaurant : other_restaurant #even shifts belong to restaurant, odd to other_restaurant | |
FactoryGirl.create(:shift, :with_restaurant, restaurant: this_restaurant, start: start_t + m.hours, :end => end_t + m.hours ) | |
# FactoryGirl.create(:shift, :with_restaurant, restaurant: this_restaurant, start: start_t + ((n+2)/2).days + (n/2*6).hours, :end => end_t + ((n+2)/2).days + (n/2*6).hours) | |
end | |
end | |
let(:mixed_batch) do | |
4.times.map{ |n| n < 2 ? extra_shifts[n] : emergency_shifts[n] } | |
end | |
# let(:mixed_shifts){ [ emergency_shift, extra_shift ] } | |
# change to include 4 shifts so can be passed to batch delegate | |
before do | |
other_rider.contact.update(name: 'A'*9+'a') | |
other_restaurant.mini_contact.update(name: 'A'*9+'a') | |
end | |
end | |
def load_conflict_request_scenario | |
let!(:riders){ 3.times.map{ FactoryGirl.create(:rider) } } | |
let(:week_start){ Time.zone.local(2014,1,6) } | |
let(:week_end){ Time.zone.local(2014,1,12) } | |
let(:start_t){ week_start + 12.hours } | |
let(:end_t){ week_start + 18.hours } | |
let!(:conflicts) do | |
arr_1 = 7.times.map do |n| | |
if n!= 5 | |
start_ = start_t + n.days | |
end_ = start_ + 6.hours | |
FactoryGirl.create(:conflict, :with_rider, rider: riders[0], start: start_, :end => end_ ) | |
end | |
end | |
arr_2 = 7.times.map do |n| | |
if n != 6 | |
start_ = end_t + n.days | |
end_ = start_ + 6.hours | |
FactoryGirl.create(:conflict, :with_rider, rider: riders[0], start: start_, :end => end_ ) | |
end | |
end | |
arr_1 + arr_2 | |
end | |
let!(:mail_count){ ActionMailer::Base.deliveries.count } | |
end # load_conflict_request_scenario | |
def is_even? n | |
(n+2)%2 == 0 | |
end | |
def load_schedule_email_scenario | |
let(:restaurants){ 7.times.map{ |n| FactoryGirl.create(:restaurant) } } | |
let(:schedule) do | |
7.times.map { |n| FactoryGirl.create(:shift, :with_restaurant, restaurant: restaurants[n], start: start_t + 12.hours + (7+n).days, :end => start_t + 18.hours + (7+n).days) } | |
end | |
end | |
# def load_delegation_email_scenario | |
# let(:mail){ RiderMailer.delegation_email rider, shift } | |
# end | |
def assign shift, status | |
visit edit_shift_assignment_path(shift, shift.assignment) | |
page.find("#assignment_rider_id").select rider.name | |
page.find("#assignment_status").select status | |
click_button 'Save changes' | |
end | |
def batch_delegate shifts, type | |
visit shifts_path | |
#set time filters inclusively | |
select Time.zone.local(2013).year, from: 'filter_start_year' | |
select Time.zone.local(2015).year, from: 'filter_end_year' | |
#filter out all restaurants but test restaurants | |
Restaurant.all.each { |r| unselect r.name, from: 'filter_restaurants' } | |
select restaurant.name, from: "filter_restaurants" | |
select other_restaurant.name, from: "filter_restaurants" | |
click_button 'Filter' | |
# sort by restaurant | |
click_link 'Restaurant' | |
#select and submit test restaurants' shifts for batch assignment | |
page.within("#row_1"){ find("#ids_").set true } | |
page.within("#row_2"){ find("#ids_").set true } | |
page.within("#row_3"){ find("#ids_").set true } | |
page.within("#row_4"){ find("#ids_").set true } | |
click_button 'Batch Assign', match: :first | |
#assign shifts | |
assign_extra if type == :extra | |
assign_emergency if type == :emergency | |
delegate_emergency if type == :emergency_delegation | |
assign_mixed if type == :mixed | |
click_button 'Save changes' | |
end | |
def assign_extra | |
#batch delegate shifts: first two shifts to rider, second two to other_rider | |
4.times do |n| | |
the_rider = n < 2 ? rider : other_rider | |
page.within("#assignments_fresh_#{n}") do | |
find("#wrapped_assignments_fresh__assignment_rider_id").select the_rider.name | |
find("#wrapped_assignments_fresh__assignment_status").select 'Delegated' | |
end | |
end | |
end | |
def assign_emergency | |
# batch confirm shifts: 0 & 1 to rider, 2 & 3 to other_rider | |
4.times do |n| | |
the_rider = n < 2 ? rider : other_rider | |
page.within("#assignments_fresh_#{n}") do | |
find("#wrapped_assignments_fresh__assignment_rider_id").select the_rider.name | |
find("#wrapped_assignments_fresh__assignment_status").select 'Confirmed' | |
end | |
end | |
end | |
def delegate_emergency | |
# batch confirm shifts: 0 & 1 to rider, 2 & 3 to other_rider | |
4.times do |n| | |
the_rider = n < 2 ? rider : other_rider | |
page.within("#assignments_fresh_#{n}") do | |
find("#wrapped_assignments_fresh__assignment_rider_id").select the_rider.name | |
find("#wrapped_assignments_fresh__assignment_status").select 'Delegated' | |
end | |
end | |
end | |
def assign_mixed | |
# batch assign: 0 (confirmed), 1 (delegated) to rider ; 2 (confirmed), 3 (delegated) to other_rider | |
4.times do |n| | |
the_rider = is_even?(n) ? rider : other_rider | |
status = n < 2 ? 'Confirmed' : 'Delegated' | |
page.within("#assignments_fresh_#{n}") do | |
find("#wrapped_assignments_fresh__assignment_rider_id").select the_rider.name | |
find("#wrapped_assignments_fresh__assignment_status").select status | |
end | |
end | |
end | |
# SINGLE EMAIL MACROS | |
def check_delegation_email_metadata mail, staffer, type | |
expect(mail.to).to eq [ rider.email ] | |
expect(mail.subject).to eq subject_from type | |
expect(mail.from).to eq [ "[email protected]" ] | |
end | |
def subject_from type | |
case type | |
when :extra | |
'[EXTRA SHIFT] -- CONFIRMATION REQUIRED' | |
when :emergency | |
"[EMERGENCY SHIFT] -- SHIFT DETAILS ENCLOSED" | |
end | |
end | |
def check_delegation_email_body mail, staffer, type | |
actual_body = parse_body_from mail | |
expected_body = File.read("spec/mailers/sample_emails/single_#{staffer}_#{type}.html") | |
expect(actual_body).to eq expected_body | |
end | |
# BATCH EMAIL MACROS | |
def check_batch_delegation_email_metadata mails, type | |
from = [ "[email protected]" ] | |
emails = [ rider.email, other_rider.email ] | |
subject = batch_subject_from type | |
mails.each_with_index do |mail, i| | |
expect(mail.from).to eq from | |
expect(mail.to).to eq [ emails[i] ] | |
expect(mail.subject).to eq subject | |
end | |
end | |
def check_mixed_batch_delegation_email_metadata mails | |
from = [ "[email protected]" ] | |
emails = [ rider.email, rider.email, other_rider.email, other_rider.email ] | |
subjects = [ subject_from(:emergency), subject_from(:extra), subject_from(:emergency), subject_from(:extra) ] | |
mails.each_with_index do |mail, i| | |
expect(mail.from).to eq from | |
expect(mail.to).to eq [ emails[i] ] | |
expect(mail.subject).to eq subjects[i] | |
# puts ">>>> MAIL #{i} SUBJECT" | |
# puts mail.subject | |
# puts ">>>> MAIL #{i} TO" | |
# puts mail.to | |
end | |
end | |
def batch_subject_from type | |
case type | |
when :weekly | |
"[WEEKLY SCHEDULE] -- PLEASE CONFIRM BY SUNDAY" | |
when :extra | |
'[EXTRA SHIFTS] -- CONFIRMATION REQUIRED' | |
when :emergency | |
"[EMERGENCY SHIFTS] -- SHIFT DETAILS ENCLOSED" | |
end | |
end | |
def check_batch_delegation_email_body mails, staffer, type | |
mails.each_with_index do |mail, i| | |
# puts ">>>>>> MAIL #{i}" | |
# print mail.body | |
actual_body = parse_body_from mail | |
expected_body = File.read( "spec/mailers/sample_emails/batch_#{staffer}_#{type}_#{i}.html" ) | |
expect(actual_body).to eq expected_body | |
end | |
end | |
def check_conflict_request_email_bodies mails, riders | |
mails.each_with_index do |mail, i| | |
puts ">>>>>> MAIL #{i}" | |
print mail.body | |
actual_body = parse_body_from mail | |
expected_body = expected_conflict_request_body_for riders[i], i | |
expect(actual_body).to eq expected_body | |
end | |
end | |
def check_conflict_request_metadata mails, riders | |
from = [ "[email protected]" ] | |
subject = "[SCHEDULING CONFLICT REQUEST] 1/13 - 1/19" | |
mails.each_with_index do |mail, i| | |
expect(mail.from).to eq from | |
expect(mail.to).to eq [ riders[i].email ] | |
expect(mail.subject).to eq subject | |
end | |
end | |
# HELPERS | |
def parse_body_from mail | |
mail.body.encoded.gsub("\r\n", "\n") | |
end | |
def expected_conflict_request_body_for rider, i | |
str = File.read( "spec/mailers/sample_emails/conflicts_request_#{i}.html" ) | |
str.gsub('<RIDER_ID>', "#{rider.id}") | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# *************************** | |
# ********** SPECS ********** | |
# *************************** | |
# ************************************************ | |
# BATCH ASSIGNMENT SPECS | |
# app/spec/requests/shift_pages_spec.rb | |
# ************************************************ | |
require 'spec_helper' | |
include CustomMatchers, RequestSpecMacros, ShiftRequestMacros, GridRequestMacros | |
describe "Shift Requests" do | |
let!(:restaurant) { FactoryGirl.create(:restaurant) } | |
let!(:other_restaurant) { FactoryGirl.create(:restaurant) } | |
let!(:rider){ FactoryGirl.create(:rider) } | |
let!(:other_rider){ FactoryGirl.create(:rider) } | |
let(:shift) { FactoryGirl.build(:shift, :with_restaurant, restaurant: restaurant) } | |
let(:shifts) { 31.times.map { FactoryGirl.create(:shift, :without_restaurant) } } | |
let(:staffer) { FactoryGirl.create(:staffer) } | |
before { mock_sign_in staffer } | |
subject { page } | |
# ... | |
describe "BATCH REQUESTS" do | |
before { restaurant } | |
let!(:old_count){ Shift.count } | |
load_batch | |
# ... | |
describe "BATCH ASSIGN" do | |
before do | |
# initialize rider & shifts, assign shifts to rider | |
other_rider | |
batch.each(&:save) | |
batch.each { |s| s.assignment.update(rider_id: rider.id, status: :confirmed) } | |
end | |
describe "from SHIFTS INDEX" do | |
before do | |
# select shifts for batch assignment | |
visit shifts_path | |
filter_shifts_by_time_inclusively | |
page.within("#row_1"){ find("#ids_").set true } | |
page.within("#row_2"){ find("#ids_").set true } | |
page.within("#row_3"){ find("#ids_").set true } | |
end | |
describe "with STANDARD batch edit" do | |
before { click_button 'Batch Assign', match: :first } | |
describe "batch edit assignment page" do | |
it "should have correct URI" do | |
check_batch_assign_uri | |
end | |
it { should have_h1 'Batch Assign Shifts' } | |
it { should have_content(restaurant.name) } | |
it "should have correct select values" do | |
check_batch_assign_select_values rider, 'Confirmed' | |
end | |
end | |
describe "EXECUTING batch assignment" do | |
describe "WITHOUT OBSTACLES" do | |
before { assign_batch_to other_rider, 'Proposed' } | |
describe "after editing" do | |
it "should redirect to the correct page" do | |
expect(current_path).to eq "/shifts/" | |
end | |
describe "index page" do | |
before { filter_shifts_by_time_inclusively } | |
it "should show new values of edited shifts" do | |
check_reassigned_shift_values other_rider, 'Proposed' | |
end | |
end | |
end # "after editing" | |
end # "WITHOUT OBSTACLES" | |
describe "WITH CONFLICT" do | |
load_conflicts | |
before do | |
conflicts[0].save | |
assign_batch_to other_rider, 'Proposed' | |
end | |
describe "Resolve Obstacles page" do | |
describe "CONTENTS" do | |
it "should be the Resolve Obstacles page" do | |
expect(current_path).to eq "/assignment/batch_edit" | |
expect(page).to have_h1 'Resolve Scheduling Obstacles' | |
end | |
it "should correctly list Assignments With Conflicts" do | |
check_assignments_with_conflicts_list [0], [0] | |
end | |
it "should not list Assignments With Double Bookings" do | |
expect(page).not_to have_selector("#assignments_with_double_bookings") | |
end | |
it "should correctly list Assignments Without Obstacles" do | |
check_without_obstacles_list [0,1], [1,2] | |
end | |
end # "CONTENTS" | |
describe "OVERRIDING" do | |
before do | |
choose "decisions_0_Override" | |
click_button 'Submit' | |
end | |
describe "after submission" do | |
before { filter_shifts_by_time_inclusively } | |
it "should redirect to the index page" do | |
expect(current_path).to eq "/shifts/" | |
expect(page).to have_h1 'Shifts' | |
end | |
it "should show new values for reassigned shifts" do | |
check_reassigned_shift_values other_rider, 'Proposed' | |
end | |
end # "after submission (on shifts index)" | |
end # "OVERRIDING" | |
describe "ACCEPTING" do | |
load_free_rider | |
before do | |
choose 'decisions_0_Accept' | |
click_button 'Submit' | |
end | |
describe "after submission" do | |
describe "batch reassign page" do | |
it "should be the batch reassign page" do | |
expect(current_path).to eq '/assignment/resolve_obstacles' | |
expect(page).to have_h1 'Batch Reassign Shifts' | |
end | |
it "should correctly list Assignements Requiring Reassignment" do | |
check_reassign_single_shift_list other_rider, 'Proposed', 0 | |
end | |
it "should not list Assignments With Double Bookings" do | |
expect(page).not_to have_selector("#assignments_with_double_bookings") | |
end | |
it "should correctly list Assignments Without Obstacles" do | |
check_without_obstacles_list [0,1], [1,2] | |
end | |
end | |
describe "executing REASSIGNMENT TO FREE RIDER" do | |
before { reassign_single_shift_to free_rider, 'Proposed' } | |
describe "after submission" do | |
it "should redirect to the correct page" do | |
expect(current_path).to eq "/shifts/" | |
expect(page).to have_h1 'Shifts' | |
end | |
describe "index page" do | |
before { filter_shifts_by_time_inclusively } | |
it "shoud show new values for reassigned shifts" do | |
check_reassigned_shift_values_after_accepting_obstacle other_rider, free_rider, 'Proposed' | |
end | |
end #"index page" | |
end # "after submission" | |
end # "executing REASSIGNMENT TO FREE RIDER" | |
describe "executing REASSIGNMENT TO RIDER WITH CONFLICT" do | |
before{ click_button 'Save changes' } | |
it "should redirect to resolve obstacles page" do | |
expect(current_path).to eq "/assignment/batch_reassign" | |
expect(page).to have_h1 'Resolve Scheduling Obstacles' | |
end | |
end #"executing REASSIGNMENT TO RIDER WITH CONFLICT" | |
describe "executing REASSIGNMENT TO RIDER WITH DOUBLE BOOKING" do | |
load_double_bookings | |
before do | |
double_bookings[0].save | |
double_bookings[0].assign_to free_rider | |
reassign_single_shift_to free_rider, 'Confirmed' | |
end | |
it "should redirect to resolve obstacles page" do | |
expect(current_path).to eq "/assignment/batch_reassign" | |
expect(page).to have_h1 'Resolve Scheduling Obstacles' | |
end | |
end #"executing REASSIGNMENT TO RIDER WITH CONFLICT" | |
end # "after submission" | |
end # "ACCEPTING" | |
end # "Resove Obstacles Page" | |
end # "WITH CONFLICT" | |
describe "WITH 2 CONFLICTS" do | |
load_conflicts | |
before do | |
conflicts[0..1].each(&:save) | |
assign_batch_to other_rider, 'Proposed' | |
end | |
describe "Resolve Obstacles page" do | |
describe "CONTENTS" do | |
it "should be the Resolve Obstacles page" do | |
expect(current_path).to eq "/assignment/batch_edit" | |
expect(page).to have_h1 'Resolve Scheduling Obstacles' | |
end | |
it "should correctly list Assignments With Conflicts" do | |
check_assignments_with_conflicts_list [0,1], [0,1] | |
end | |
it "should not list Assignments With Double Bookings" do | |
expect(page).not_to have_selector("#assignments_with_double_bookings") | |
end | |
it "should correctly list Assignments Without Obstacles" do | |
check_without_obstacles_list [0], [2] | |
end | |
end # "CONTENTS" | |
end # "Resolve Obstacles page" | |
end # "WITH 2 CONFLICTS" | |
describe "WITH 3 CONFLICTS" do | |
load_conflicts | |
before do | |
conflicts.each(&:save) | |
assign_batch_to other_rider, 'Proposed' | |
end | |
describe "Resolve Obstacles page" do | |
describe "CONTENTS" do | |
it "should be the Resolve Obstacles page" do | |
expect(current_path).to eq "/assignment/batch_edit" | |
expect(page).to have_h1 'Resolve Scheduling Obstacles' | |
end | |
it "should correctly list Assignments With Conflicts" do | |
check_assignments_with_conflicts_list [0,1,2], [0,1,2] | |
end | |
it "should not list Assignments With Double Bookings" do | |
expect(page).not_to have_selector("#assignments_with_double_bookings") | |
end | |
it "should not list Assignments Without Obstacles" do | |
expect(page).not_to have_selector("#assignments_without_obstacles") | |
end | |
end # "CONTENTS" | |
end # "Resolve Obstacles page" | |
end # "WITH 3 CONFLICTS" | |
describe "WITH DOUBLE BOOKING" do | |
load_double_bookings | |
before do | |
double_bookings[0].save | |
double_bookings[0].assign_to other_rider | |
assign_batch_to other_rider, 'Proposed' | |
end | |
describe "Resolve Obstacles page" do | |
describe "CONTENTS" do | |
it "should be the Resolve Obstacles page" do | |
expect(current_path).to eq "/assignment/batch_edit" | |
expect(page).to have_h1 'Resolve Scheduling Obstacles' | |
end | |
it "should not list Assignments With Conflicts" do | |
expect(page).not_to have_selector("#assignments_with_conflicts") | |
end | |
it "should correctly list Assignments With Double Bookings" do | |
check_assignments_with_double_booking_list [0], [0] | |
end | |
it "should correctly list Assignments Without Obstacles" do | |
check_without_obstacles_list [0,1], [1,2] | |
end | |
end # "CONTENTS" | |
describe "OVERRIDING" do | |
before do | |
choose "decisions_0_Override" | |
click_button 'Submit' | |
end | |
describe "after submission" do | |
it "should redirect to the correct page" do | |
expect(current_path).to eq "/shifts/" | |
expect(page).to have_h1 'Shifts' | |
end | |
describe "index page" do | |
before { filter_shifts_by_time_inclusively } | |
it "shoud show new values for reassigned shifts" do | |
check_reassigned_shift_values other_rider, 'Proposed' | |
end | |
end # "index page" | |
end # "after submission" | |
end # "OVERRIDING" | |
describe "ACCEPTING" do | |
load_free_rider | |
before do | |
choose 'decisions_0_Accept' | |
click_button 'Submit' | |
end | |
describe "after submission" do | |
describe "batch reassign page" do | |
it "should redirect to the correct page" do | |
expect(current_path).to eq '/assignment/resolve_obstacles' | |
expect(page).to have_h1 'Batch Reassign Shifts' | |
end | |
it "should correctly list Assignements Requiring Reassignment" do | |
check_reassign_single_shift_list other_rider, 'Proposed', 0 | |
end | |
it "should not list Assignments With Double Bookings" do | |
expect(page).not_to have_selector("#assignments_with_double_bookings") | |
end | |
it "should correctly list Assignemnts Without Obstacles" do | |
check_without_obstacles_list [0,1], [1,2] | |
end | |
end | |
describe "executing REASSIGNMENT TO FREE RIDER" do | |
before { reassign_single_shift_to free_rider, 'Proposed' } | |
describe "after submission" do | |
it "should redirect to the correct page" do | |
expect(current_path).to eq "/shifts/" | |
expect(page).to have_h1 'Shifts' | |
end | |
describe "index page" do | |
before { filter_shifts_by_time_inclusively } | |
it "shoud show new values for reassigned shifts" do | |
check_reassigned_shift_values_after_accepting_obstacle other_rider, free_rider, 'Proposed' | |
end | |
end #"index page" | |
end # "after submission" | |
end # "executing REASSIGNMENT TO FREE RIDER" | |
end # "after submission" | |
end # "ACCEPTING" | |
end # "Resolve Obstacles page" | |
end # "WITH DOUBLE BOOKING" | |
describe "WITH 2 DOUBLE BOOKINGS" do | |
load_double_bookings | |
before do | |
double_bookings[0..1].each do |shift| | |
shift.save | |
shift.assign_to other_rider | |
end | |
assign_batch_to other_rider, 'Proposed' | |
end | |
describe "Resolve Obstacles page" do | |
describe "CONTENTS" do | |
it "should be the Resolve Obstacles page" do | |
expect(current_path).to eq "/assignment/batch_edit" | |
expect(page).to have_h1 'Resolve Scheduling Obstacles' | |
end | |
it "should not list Assignments With Conflicts" do | |
expect(page).not_to have_selector("#assignments_with_conflicts") | |
end | |
it "should correctly list Assignments With Double Bookings" do | |
check_assignments_with_double_booking_list [0,1], [0,1] | |
end | |
it "should correctly list Assignments Without Obstacles" do | |
check_without_obstacles_list [0], [2] | |
end | |
end # "CONTENTS" | |
end # "Resolve Obstacles page" | |
end # "WITH 2 DOUBLE BOOKINGS" | |
describe "WITH 3 DOUBLE BOOKINGS" do | |
load_double_bookings | |
before do | |
double_bookings.each do |shift| | |
shift.save | |
shift.assign_to other_rider | |
end | |
assign_batch_to other_rider, 'Proposed' | |
end | |
describe "Resolve Obstacles page" do | |
describe "CONTENTS" do | |
it "should be the Resolve Obstacles page" do | |
expect(current_path).to eq "/assignment/batch_edit" | |
expect(page).to have_h1 'Resolve Scheduling Obstacles' | |
end | |
it "should not list Assignments With Conflicts" do | |
expect(page).not_to have_selector("#assignments_with_conflicts") | |
end | |
it "should correctly list Assignments With Double Bookings" do | |
check_assignments_with_double_booking_list [0,1,2], [0,1,2] | |
end | |
it "should not list Assignments Without Obstacles" do | |
expect(page).not_to have_selector("#assignments_without_obstacles") | |
end | |
end # "CONTENTS" | |
end # "Resolve Obstacles page" | |
end # "WITH 2 DOUBLE BOOKINGS" | |
describe "WITH CONFLICT AND DOUBLE BOOKING" do | |
load_conflicts | |
load_double_bookings | |
before do | |
conflicts[0].save | |
double_bookings[1].save | |
double_bookings[1].assign_to other_rider | |
assign_batch_to other_rider, 'Proposed' | |
end | |
describe "Resolve Obstacles Page" do | |
describe "CONTENTS" do | |
it "should be the Resolve Obstacles page" do | |
expect(current_path).to eq "/assignment/batch_edit" | |
expect(page).to have_h1 'Resolve Scheduling Obstacles' | |
end | |
it "should correctly list Assignments With Conflicts" do | |
check_assignments_with_conflicts_list [0], [0] | |
end | |
it "should correctly list Assignments With Double Bookings" do | |
check_assignments_with_double_booking_list [0], [1] | |
end | |
it "should correctly list Assignments Without Obstacles" do | |
check_without_obstacles_list [0], [2] | |
end | |
end # "CONTENTS" | |
describe "OVERRIDING BOTH" do | |
before do | |
choose "decisions_0_Override" | |
choose "decisions_1_Override" | |
click_button 'Submit' | |
end | |
describe "after submission" do | |
before { filter_shifts_by_time_inclusively } | |
it "should redirect to the index page" do | |
expect(current_path).to eq "/shifts/" | |
expect(page).to have_h1 'Shifts' | |
end | |
it "should show new values for reassigned shifts" do | |
check_reassigned_shift_values other_rider, 'Proposed' | |
end | |
end # "after submission (on shifts index)" | |
end # "OVERRIDING BOTH" | |
describe "OVERRIDING CONFLICT / ACCEPTING DOUBLE BOOKING" do | |
before do | |
choose "decisions_0_Override" | |
choose "decisions_1_Accept" | |
click_button 'Submit' | |
end | |
describe "after submission" do | |
describe "batch reassign page" do | |
it "should be the batch reassign page" do | |
expect(current_path).to eq '/assignment/resolve_obstacles' | |
expect(page).to have_h1 'Batch Reassign Shifts' | |
end | |
it "should correctly list Assignments Requiring Reassignment" do | |
check_reassign_single_shift_list other_rider, 'Proposed', 1 | |
end | |
it "should correctly list Assignments Without Obstacles" do | |
check_without_obstacles_list [0,1], [2,0] | |
end | |
end | |
end # "after submission" | |
end # "OVERRIDING CONFLICT / ACCEPTING DOUBLE BOOKING" | |
describe "ACCEPTING CONFLICT / OVERRIDING DOUBLE BOOKING" do | |
before do | |
choose "decisions_0_Accept" | |
choose "decisions_1_Override" | |
click_button 'Submit' | |
end | |
describe "after submission" do | |
describe "batch reassign page" do | |
it "should be the batch reassign page" do | |
expect(current_path).to eq '/assignment/resolve_obstacles' | |
expect(page).to have_h1 'Batch Reassign Shifts' | |
end | |
it "should correctly list Assignments Requiring Reassignment" do | |
check_reassign_single_shift_list other_rider, 'Proposed', 0 | |
end | |
it "should correctly list Assignments Without Obstacles" do | |
check_without_obstacles_list [0,1], [2,1] | |
end | |
end # "batch reassign page" | |
end # "after submission" | |
end # "OVERRIDING CONFLICT / ACCEPTING DOUBLE BOOKING" | |
end # "Resolve Obstacles Page" | |
end # "WITH CONFLICT AND DOUBLE BOOKING" | |
end # "EXECUTING batch assignment" | |
end # "with STANDARD batch edit" | |
describe "with UNIFORM batch edit" do | |
before { click_button 'Uniform Assign', match: :first } | |
describe "Uniform Assign Shifts page" do | |
it "should have correct URI and Header" do | |
check_uniform_assign_uri | |
expect(page).to have_h1 "Uniform Assign Shifts" | |
end | |
it "should list Shifts correctly" do | |
check_uniform_assign_shift_list rider, 'Confirmed' | |
end | |
it "should have correct form values" do | |
check_uniform_assign_select_values | |
end | |
end | |
describe "EXECUTING batch assignment" do | |
describe "WITHOUT OBSTACLES" do | |
before { uniform_assign_batch_to other_rider, 'Cancelled (Rider)' } | |
describe "after editing" do | |
it "should redirect to the correct page" do | |
expect(current_path).to eq "/shifts/" | |
expect(page).to have_h1 'Shifts' | |
end | |
describe "index page" do | |
before { filter_shifts_by_time_inclusively } | |
it "should show new values for re-assigned shifts" do | |
check_reassigned_shift_values other_rider, 'Cancelled (Rider)' | |
end | |
end # "index page" | |
end # "after editing" | |
end # "WITHOUT OBSTACLES" | |
describe "WITH CONFLICT" do | |
load_conflicts | |
before do | |
conflicts[0].save | |
uniform_assign_batch_to other_rider, 'Proposed' | |
end | |
it "should redirect to the Resolve Obstacles page" do | |
expect(current_path).to eq "/assignment/batch_edit_uniform" | |
expect(page).to have_h1 'Resolve Scheduling Obstacles' | |
end | |
end # "WITH CONFLICT" | |
describe "WITH DOUBLE BOOKING" do | |
load_double_bookings | |
before do | |
double_bookings[0].save | |
double_bookings[0].assign_to other_rider | |
uniform_assign_batch_to other_rider, 'Proposed' | |
end | |
it "should be the Resolve Obstacles page" do | |
expect(current_path).to eq "/assignment/batch_edit_uniform" | |
expect(page).to have_h1 'Resolve Scheduling Obstacles' | |
end | |
end # "WITH DOUBLE BOOKING" | |
end # "EXECUTING batch assignment" | |
end # "Uniform Assign Shifts page" | |
end # "with UNIFORM batch edit" | |
describe "from GRID" do | |
before do | |
restaurant.mini_contact.update(name: 'A'*10) | |
visit shift_grid_path | |
filter_grid_for_jan_2014 | |
end | |
describe "page contents" do | |
describe "batch edit form" do | |
it { should have_button 'Batch Assign' } | |
it "should have correct form action" do | |
expect(page.find("form.batch")['action']).to eq '/shift/batch_edit' | |
end | |
end | |
describe "grid rows" do | |
it "should have correct cells in first row" do | |
expect(page.find("#row_1_col_1").text).to eq 'A'*10 | |
expect(page.find("#row_1_col_6").text).to eq rider.short_name + " [c]" | |
expect(page.find("#row_1_col_8").text).to eq rider.short_name + " [c]" | |
expect(page.find("#row_1_col_10").text).to eq rider.short_name + " [c]" | |
end | |
end | |
end | |
describe "STANDARD batch assignment" do | |
before do | |
select_batch_assign_shifts_from_grid | |
click_button 'Batch Assign' | |
end | |
describe "batch assign page" do | |
it "should have correct URI" do | |
check_batch_assign_uri | |
end | |
it "should have correct assignment values" do | |
check_batch_assign_select_values rider, 'Confirmed' | |
end | |
end | |
describe "executing batch assignment" do | |
before { assign_batch_to rider, 'Proposed' } | |
describe "after editing" do | |
it "should redirect to the correct page" do | |
expect(current_path).to eq "/grid/shifts" | |
end | |
describe "page contents" do | |
before { filter_grid_for_jan_2014 } | |
it "should have new assignment values" do | |
check_reassigned_shift_values_in_grid other_rider, '[p]' | |
end | |
end | |
end | |
end | |
end | |
describe "UNIFORM batch assignment" do | |
before do | |
select_batch_assign_shifts_from_grid | |
click_button 'Uniform Assign' | |
end | |
describe "uniform assign page" do | |
it "should have correct uri" do | |
check_uniform_assign_uri | |
end | |
it { should have_h1 'Uniform Assign Shifts' } | |
it { should have_content restaurant.name } | |
it "should have correct form values" do | |
check_uniform_assign_select_values | |
end | |
end | |
describe "executing batch edit" do | |
before { uniform_assign_batch_to other_rider, 'Cancelled (Rider)' } | |
describe "after editing" do | |
it "should redirect to the correct page" do | |
expect(current_path).to eq "/grid/shifts" | |
end | |
describe "index page" do | |
before { filter_grid_for_jan_2014 } | |
it "should show new values for re-assigned shifts" do | |
check_reassigned_shift_values_in_grid other_rider, '[xf]' | |
end | |
end | |
end | |
end | |
end | |
end | |
end | |
end | |
end | |
# ************************************************ | |
# RIDER MAILER SPECS | |
# app/spec/mailers/rider_mailer_spec.rb | |
# ************************************************ | |
require 'spec_helper' | |
include RequestSpecMacros, RiderMailerMacros | |
describe "Rider Mailer Requests" do | |
load_staffers | |
describe "DELEGATION EMAIL" do | |
load_delegation_scenario | |
describe "as Tess" do | |
before { mock_sign_in tess } | |
describe "for extra shift" do | |
let!(:mail_count){ ActionMailer::Base.deliveries.count } | |
before { assign extra_shift, 'Delegated' } | |
let(:mail){ ActionMailer::Base.deliveries.last } | |
it "should send an email" do | |
expect(ActionMailer::Base.deliveries.count).to eq (mail_count + 1) | |
end | |
it "should render correct email metadata" do | |
check_delegation_email_metadata mail, :tess, :extra | |
end | |
it "should render correct email body" do | |
check_delegation_email_body mail, :tess, :extra | |
end | |
end | |
describe "for emergency shift" do | |
let!(:mail_count){ ActionMailer::Base.deliveries.count } | |
before { assign emergency_shift, 'Confirmed' } | |
let(:mail){ ActionMailer::Base.deliveries.last } | |
it "should send an email" do | |
expect(ActionMailer::Base.deliveries.count).to eq (mail_count + 1) | |
end | |
it "should render correct email metadata" do | |
check_delegation_email_metadata mail, :tess, :emergency | |
end | |
it "should render correct email body" do | |
check_delegation_email_body mail, :tess, :emergency | |
end | |
end | |
describe "trying to delegate an emergency shift" do | |
before { assign emergency_shift, 'Delegated' } | |
it "should redirect to error-handling page" do | |
expect(page).to have_h1 'Batch Assign Shifts' | |
end | |
it "should list shifts with errors correctly" do | |
expect(page.within("#assignments_fresh_0"){ find(".field_with_errors").text }).to include(rider.name) | |
end | |
end # "trying to delegate an emergency shift" | |
end # "as Tess" | |
describe "as Justin" do | |
before { mock_sign_in justin } | |
describe "for extra shift" do | |
let!(:mail_count){ ActionMailer::Base.deliveries.count } | |
before { assign extra_shift, 'Delegated' } | |
let(:mail){ ActionMailer::Base.deliveries.last } | |
it "should send an email" do | |
expect(ActionMailer::Base.deliveries.count).to eq (mail_count + 1) | |
end | |
it "should render correct email metadata" do | |
check_delegation_email_metadata mail, :justin, :extra | |
end | |
it "should render correct email body" do | |
check_delegation_email_body mail, :justin, :extra | |
end | |
end | |
end #"as Justin" | |
end # "ASSIGNMENT EMAIL" | |
describe "BATCH DELEGATION EMAILS" do | |
load_delegation_scenario | |
load_batch_delegation_scenario | |
describe "as Tess" do | |
before { mock_sign_in tess } | |
describe "for EXTRA shifts" do | |
let!(:mail_count){ ActionMailer::Base.deliveries.count } | |
before { batch_delegate extra_shifts, :extra } | |
let(:mails){ ActionMailer::Base.deliveries.last(2) } | |
it "should send 2 emails" do | |
expect( ActionMailer::Base.deliveries.count ).to eq mail_count + 2 | |
end | |
it "should format email metadata correctly" do | |
check_batch_delegation_email_metadata mails, :extra | |
end | |
it "should format email body correctly" do | |
check_batch_delegation_email_body mails, :tess, :extra | |
end | |
end # "for EXTRA shifts" | |
describe "for EMERGENCY shifts" do | |
let!(:mail_count){ ActionMailer::Base.deliveries.count } | |
before { batch_delegate emergency_shifts, :emergency } | |
let(:mails){ ActionMailer::Base.deliveries.last(2) } | |
it "should send 2 emails" do | |
expect( ActionMailer::Base.deliveries.count ).to eq mail_count + 2 | |
end | |
it "should format email metadata correctly" do | |
check_batch_delegation_email_metadata mails, :emergency | |
end | |
it "should format email body correctly" do | |
check_batch_delegation_email_body mails, :tess, :emergency | |
end | |
end # "for EMERGENCY shifts" | |
describe "for MIXED BATCH of shifts" do | |
let!(:mail_count){ ActionMailer::Base.deliveries.count } | |
before { batch_delegate mixed_batch, :mixed } | |
let(:mails){ ActionMailer::Base.deliveries.last(4) } | |
it "should send 4 emails" do | |
expect( ActionMailer::Base.deliveries.count ).to eq mail_count + 4 | |
end | |
it "should format email metadata correctly" do | |
check_mixed_batch_delegation_email_metadata mails | |
end | |
it "should format email body correctly" do | |
check_batch_delegation_email_body mails, :tess, :mixed | |
end | |
end # "for "for MIXED BATCH of shifts" | |
describe "trying to DELEGATE EMERGENCY shifts" do | |
before { batch_delegate emergency_shifts, :emergency_delegation } | |
it "should redirect to error-handling page" do | |
expect(page).to have_h1 'Batch Assign Shifts' | |
end | |
it "should list shifts with errors correctly" do | |
expect(page.within("#assignments_fresh_0"){ find(".field_with_errors").text }).to include(rider.name) | |
expect(page.within("#assignments_fresh_1"){ find(".field_with_errors").text }).to include(rider.name) | |
expect(page.within("#assignments_fresh_2"){ find(".field_with_errors").text }).to include(rider.name) | |
expect(page.within("#assignments_fresh_3"){ find(".field_with_errors").text }).to include(rider.name) | |
end | |
end # "trying to DELEGATE EMERGENCY shifts" | |
end # "as Tess" | |
end # "BATCH ASSIGNMENT EMAILS" | |
# .... | |
end | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/ *********************** | |
/ ******** VIEWS ******** | |
/ *********************** | |
/ ********************************************** | |
/ SHIFTS INDEX VIEW | |
/ app/views/shifts/index.html.haml | |
/ ********************************************** | |
- provide(:title, 'Shifts') | |
/Arguments | |
- if @caller | |
- header_suffix = " for #{@caller_obj.name}" | |
- span = "span8 offset2" | |
- else | |
- header_suffix = "" | |
- span = "span10 offset1" | |
/Header | |
%h1= "Shifts" + header_suffix | |
/Hot Links | |
- if can? :manage, Shift | |
= render 'hot_links', entity: @caller_obj | |
/Filters | |
.row | |
%div{ class: span+' filters' } | |
= render 'filter_form' | |
/Batch Edit Form Wrapper | |
.row.batch_form | |
= form_tag '/shift/batch_edit', method: :get, class: 'batch' do | |
/Submit Buttons | |
.center | |
= submit_tag 'Batch Edit', class: 'btn btn-primary' | |
= submit_tag 'Batch Assign', class: 'btn btn-primary' | |
= submit_tag 'Uniform Assign', class: 'btn btn-primary' | |
= hidden_field_tag :base_path, @base_path | |
/Pagination | |
.center | |
= will_paginate @shifts | |
/Table | |
.row | |
%div{ :class => span } | |
= render 'layouts/table', table: @shift_table | |
/Pagination | |
.center | |
= will_paginate @shifts | |
/Submit Buttons | |
.center | |
= submit_tag 'Batch Edit', class: 'btn btn-primary' | |
= submit_tag 'Batch Assign', class: 'btn btn-primary' | |
/Hot Links | |
- if can? :manage, Shift | |
%p.center | |
= render 'hot_links', entity: @caller_obj | |
/ ********************************************** | |
/ TABLE LAYOUT | |
/ app/views/layouts/_table.html.haml | |
/ ********************************************** | |
.table | |
/Headers | |
.row.header | |
- table.headers.each_with_index do |header, i| | |
%div{ id: "row_0_col_#{i+1}", :class => "span#{table.spans[i]}" } | |
= sort_if_sortable header | |
/Data Rows | |
- table.rows.each_with_index do |row, i| | |
.row | |
%div{ id: "row_#{i+1}" } | |
/Checkboxes (optional) | |
.checkbox | |
= checkbox_if_checkable row | |
/Cells | |
- row[:cells].each_with_index do |cell, j| | |
%div{ id: "row_#{i+1}_col_#{j+1}", :class => "span#{table.spans[j]}" } | |
= link_if_linkable cell | |
/Action Dropdown | |
.span1.action.dropdown | |
%a.dropdown-toggle{"data-toggle" => "dropdown", :href => "#"} | |
Action | |
%b.caret | |
/Action Options | |
%ul.dropdown-menu | |
- row[:actions].each do |action| | |
%li= link_to action[:val], action[:href], method: action[:method], data: action[:data] | |
/ ********************************************** | |
/ BATCH EDIT ASSIGNMENTS VIEW | |
/ app/views/assignments/batch_edit.html.haml | |
/ ********************************************** | |
- provide(:title, 'Batch Assign Shifts') | |
%h1 Batch Assign Shifts | |
.span8.offset2.profile | |
= form_tag '/assignment/batch_edit', method: :post do | |
= render 'shared/batch_error_messages', errors: @errors | |
/Fresh | |
- @assignments.fresh.each_with_index do |wrapped_assignment, i| | |
%div{ id: "assignments_fresh_#{i}" } | |
= render 'batch_fields', assignment: wrapped_assignment.assignment, index: wrapped_assignment.index | |
%hr/ | |
/Old | |
= render 'old_assignment_hidden_fields' | |
/Base Path | |
= render 'shared/base_path_field' | |
%p.center | |
= submit_tag 'Save changes', class: 'btn btn-primary' | |
/ ********************************************** | |
/ BATCH UNIFORM EDIT ASSIGNMENTS VIEW | |
/ app/views/assignments/batch_uniform_edit.html.haml | |
/ ********************************************** | |
- provide(:title, 'Uniform Assign Shifts') | |
%h1 Uniform Assign Shifts | |
.span8.offset2.profile | |
= form_tag '/assignment/batch_edit_uniform', method: :post do | |
/Shifts | |
#shifts | |
%h3 Shifts | |
- @shifts.each_with_index do |shift, i| | |
%div{ id: "shifts_#{i}" } | |
%p.shift_box | |
= render 'assignments/shift_include', shift: shift | |
%p.shift_box | |
= render 'assignment_include', assignment: shift.assignment | |
= hidden_field_tag "shift_ids[]", shift.id | |
= hidden_field_tag "ids[]", shift.assignment.id | |
= hidden_field_tag :base_path, @base_path | |
%hr/ | |
/Assignment Form | |
#assignment_form | |
%h3 Assign All Shifts To | |
.row | |
/Rider | |
.span4 | |
.center | |
= label_tag :rider | |
= select_tag 'assignment[rider_id]', options_for_select(Rider.select_options, nil ), include_blank: true | |
/Status | |
.span4 | |
.center | |
= label_tag :status | |
= select_tag 'assignment[status]', options_for_select(AssignmentStatus.select_options, :proposed) | |
%p.center | |
= submit_tag 'Save changes', class: 'btn btn-primary' | |
/ ********************************************** | |
/ ASSIGNMENTS BATCH FILEDS PARTIAL | |
/ app/views/assignments/_batch_fields.html.haml | |
/ ********************************************** | |
- #args: assignment, index | |
- name = lambda { |attr| "wrapped_assignments[fresh][][assignment][#{attr}]" } | |
- error_class = @errors.find{ |e| e[:record].shift_id == assignment.shift_id } ? 'field_with_errors' : '' | |
/Shift Box | |
%p.shift_box | |
= render 'assignments/shift_include', shift: assignment.shift | |
/Index | |
= hidden_field_tag "wrapped_assignments[fresh][][index]", index | |
/Assignment | |
.row | |
/Shift | |
= hidden_field_tag name.call('shift_id'), assignment.shift_id | |
%div{ class: error_class } | |
/Rider | |
.span4 | |
.center | |
= label_tag :rider | |
- unless @caller == :rider | |
= select_tag name.call('rider_id'), options_for_select(Rider.select_options, assignment.rider.nil? ? nil : assignment.rider.id ), include_blank: true | |
- else | |
= assignment.rider.name | |
/Status | |
.span4 | |
.center | |
= label_tag :status | |
= select_tag name.call('status'), options_for_select(AssignmentStatus.select_options, assignment.status) | |
/ ********************************************** | |
/ SHIFT INCLUDE PARTIAL | |
/ app/views/assignments/_shift_include.html.haml | |
/ ********************************************** | |
#{shift.table_time} @ #{link_to shift.restaurant.mini_contact.name, restaurant_path(shift.restaurant.mini_contact.name)} | |
/ ********************************************** | |
/ ASSIGNMENT INCLUDE PARTIAL | |
/ app/views/assignments/_assignment_include.html.haml | |
/ ********************************************** | |
- #arg: assignment | |
%strong | |
Assigned to: | |
= (link_to assignment.rider.name, rider_path(assignment.rider)) + " [#{assignment.status.text}]" | |
/ ********************************************** | |
/ RESOLVE CONFLICTS VIEW | |
/ app/views/assignments/resolve_obstacles.html.haml | |
/ ********************************************** | |
- #args: Assignments (.with_obstacles, .without_obstacles) | |
- provide(title: "Resolve Scheduling Obstacles") | |
%h1 Resolve Scheduling Obstacles | |
.row | |
.span8.offset2.profile | |
= form_tag '/assignment/resolve_obstacles', method: :post do | |
/Assignments | |
= hidden_field_tag :assignments_json, @assignments.to_json | |
/Decisions... | |
/... about Conflicts | |
- if @assignments.with_conflicts.any? | |
%div{ id: "assignments_with_conflicts" } | |
%h3 Assignments With Conflicts | |
- @assignments.with_conflicts.each_with_index do |wrapped_assignment, i| | |
%div{ id: "assignments_with_conflicts_#{i}" } | |
= render 'conflict_alert', assignment: wrapped_assignment.assignment | |
= render 'decision_radios', i: i | |
%p.center | |
%i="(NOTE: Selecting 'Yes' will delete all rider conflicts during this period)" | |
%hr/ | |
/... about Double Bookings | |
- if @assignments.with_double_bookings.any? | |
%div{ id: "assignments_with_double_bookings" } | |
%h3 Assignments With Double Bookings | |
- offset = @assignments.with_conflicts.count | |
- @assignments.with_double_bookings.each_with_index do |wrapped_assignment, i| | |
%div{ id: "assignments_with_double_bookings_#{i}" } | |
= render 'double_booking_alert', assignment: wrapped_assignment.assignment | |
= render 'decision_radios', i: i + offset | |
%hr/ | |
/Assignemnts Without Obstacles (display only) | |
- if @assignments.without_obstacles.any? | |
%div{ id: "assignments_without_obstacles" } | |
%h3 Assignments Without Obstacles | |
- @assignments.without_obstacles.each_with_index do |wrapped_assignment, i| | |
- assignment = wrapped_assignment.assignment | |
%div{ id: "assignments_without_obstacles_#{i}" } | |
.shift_box | |
= render 'shift_include', shift: assignment.shift | |
.shift_box | |
= render 'assignment_include', assignment: assignment | |
%hr/ | |
/Submit | |
%p.center | |
= submit_tag "Submit", class: 'btn btn-primary' | |
/ ********************************************** | |
/ CONFLICT ALERT PARTIAL | |
/ app/views/assignments/_conflict_alert.html.haml | |
/ ********************************************** | |
- a = assignment | |
%p.shift_box | |
= render 'assignments/shift_include', shift: a.shift | |
%p.shift_box | |
= render 'assignments/assignment_include', assignment: a | |
.center | |
%strong | |
CONFLICTS WITH: | |
%p.shift_box | |
= render 'assignments/conflicts_include', conflicts: a.conflicts | |
%p.center | |
%strong | |
Do you want to assign it anyway? | |
/ ********************************************** | |
/ CONFLICTS INCLUDE PARTIAL | |
/ app/views/assignments/_conflict_alert.html.haml | |
/ ********************************************** | |
- conflicts.each do |conflict| | |
#{conflict.table_time} | |
%br/ | |
/ ********************************************** | |
/ DOUBLE BOOKING ALERT PARTIAL | |
/ app/views/assignments/_double_booking_alert.html.haml | |
/ ********************************************** | |
- a = assignment | |
%p.shift_box | |
= render 'assignments/shift_include', shift: a.shift | |
%p.shift_box | |
= render 'assignments/assignment_include', assignment: a | |
.center | |
%strong | |
DOUBLE BOOKS WITH: | |
%p.shift_box | |
= render 'assignments/double_bookings_include', double_bookings: a.double_bookings | |
%p.center | |
%strong | |
Do you want to assign it anyway? | |
/ ********************************************** | |
/ DOUBLE BOOKINGS INCLUDE PARTIAL | |
/ app/views/assignments/_double_bookings_include.html.haml | |
/ ********************************************** | |
- double_bookings.each do |double_booking| | |
= render 'assignments/shift_include', shift: double_booking | |
%br/ | |
/ ********************************************** | |
/ DECISION RADIOS PARTIAL | |
/ app/views/assignments/_decision_radios.html.haml | |
/ ********************************************** | |
- #args: i | |
.radio | |
= radio_button_tag "decisions[#{i}]", 'Accept', true | |
= label_tag :no | |
.radio | |
= radio_button_tag "decisions[#{i}]", 'Override', false | |
= label_tag :yes | |
- # arg: @assignments (Assignments Obj) | |
- provide(title: "Batch Reassign Shifts") | |
/ ********************************************** | |
/ BATCH REASSIGN VIEW | |
/ app/views/assignments/batch_reassign.html.haml | |
/ ********************************************** | |
%h1 Batch Reassign Shifts | |
.row | |
.span8.offset2.profile | |
= form_tag '/assignment/batch_reassign', method: :post do | |
/Requiring Reassignment | |
%div{ id: "assignments_requiring_reassignment" } | |
%h3 Assignments Requiring Reassignment | |
- @assignments.requiring_reassignment.each_with_index do |wrapped_assignment, i| | |
%div{ id: "assignments_requiring_reassignment_#{i}" } | |
.field_with_errors | |
= render 'batch_fields', assignment: wrapped_assignment.assignment, index: wrapped_assignment.index | |
%hr/ | |
/No Obstacles | |
- if @assignments.without_obstacles.any? | |
%div{ id: "assignments_without_obstacles" } | |
%h3 Assignments Without Obstacles | |
- @assignments.without_obstacles.each_with_index do |wrapped_assignment, i| | |
- assignment = wrapped_assignment.assignment | |
- index = wrapped_assignment.index | |
%div{ id: "assignments_without_obstacles_#{i}" } | |
.shift_box | |
= render 'shift_include', shift: assignment.shift | |
.shift_box | |
= render 'assignment_include', assignment: assignment | |
= render 'batch_attribute_hidden_fields', assignment: assignment, assignments_key: 'without_obstacles', index: index | |
%hr/ | |
/Old | |
= render 'old_assignment_hidden_fields' | |
/Base Path | |
= render 'shared/base_path_field' | |
/Submit | |
%p.center | |
= submit_tag 'Save changes', class: 'btn btn-primary' | |
/ *********************************************************** | |
/ BATCH ATTRIBUTE HIDDEN FIELDS PARTIAL | |
/ app/views/assignments/_old_assignment_hidden_fields.html.haml | |
/ *********************************************************** | |
- #arg: assignments_key, assignment, index | |
- name = lambda { |attr| "wrapped_assignments[#{assignments_key}][][assignment][#{attr}]" } | |
/Index | |
= hidden_field_tag "wrapped_assignments[#{assignments_key}][][index]", index | |
/Assignment | |
- assignment.attributes.keys.each do |attr| | |
= hidden_field_tag name.call(attr), assignment.send(attr) | |
/ *********************************************************** | |
/ OLD ASSIGNMENTS HIDDEN FIELDS PARTIAL | |
/ app/views/assignments/_old_assignment_hidden_fields.html.haml | |
/ *********************************************************** | |
- @assignments.old.each do |wrapped_assignment| | |
= render 'batch_attribute_hidden_fields', assignment: wrapped_assignment.assignment, index: wrapped_assignment.index, assignments_key: 'old' | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment