course timetable event generator
test plan: * see the new api endpoints in the calendar events controller * can use the 'set_course_timetable' endpoint to send a schedule for a course (optionally per section) with a list of weekdays (e.g. "Mon,Wed,Fri") and times * it will automatically generate calendar events from the start date of the course (or section) to the end date that correspond to the dates * if the schedule is changed, the old events will be deleted and new ones generated * can also use the 'set_course_timetable_events' endpoint to generate events from a complete list closes #CNVS-30523 Change-Id: Idf2b4047af14a6e71838bbe9672583f5bddc3e9f Reviewed-on: https://gerrit.instructure.com/86051 Tested-by: Jenkins Reviewed-by: Jeremy Stanley <jeremy@instructure.com> QA-Review: Heath Hales <hhales@instructure.com> Product-Review: Hilary Scharton <hilary@instructure.com>
This commit is contained in:
parent
a2f1068ae9
commit
114fa41175
|
@ -752,6 +752,150 @@ class CalendarEventsApiController < ApplicationController
|
|||
render json: {status: 'ok'}
|
||||
end
|
||||
|
||||
# @API Set a course timetable
|
||||
# @beta
|
||||
#
|
||||
# Creates and updates "timetable" events for a course.
|
||||
# Can automaticaly generate a series of calendar events based on simple schedules
|
||||
# (e.g. "Monday and Wednesday at 2:00pm" )
|
||||
#
|
||||
# Existing timetable events for the course and course sections
|
||||
# will be updated if they still are part of the timetable.
|
||||
# Otherwise, they will be deleted.
|
||||
#
|
||||
# @argument timetables[course_section_id][] [Array]
|
||||
# An array of timetable objects for the course section specified by course_section_id.
|
||||
# If course_section_id is set to "all", events will be created for the entire course.
|
||||
#
|
||||
# @argument timetables[course_section_id][][weekdays] [String]
|
||||
# A comma-separated list of abbreviated weekdays
|
||||
# (Mon-Monday, Tue-Tuesday, Wed-Wednesday, Thu-Thursday, Fri-Friday, Sat-Saturday, Sun-Sunday)
|
||||
#
|
||||
# @argument timetables[course_section_id][][start_time] [String]
|
||||
# Time to start each event at (e.g. "9:00 am")
|
||||
#
|
||||
# @argument timetables[course_section_id][][end_time] [String]
|
||||
# Time to end each event at (e.g. "9:00 am")
|
||||
#
|
||||
# @argument timetables[course_section_id][][location_name] [Optional, String]
|
||||
# A location name to set for each event
|
||||
#
|
||||
# @example_request
|
||||
#
|
||||
# curl 'https://<canvas>/api/v1/calendar_events/timetable' \
|
||||
# -X POST \
|
||||
# -F 'timetables[all][][weekdays]=Mon,Wed,Fri' \
|
||||
# -F 'timetables[all][][start_time]=11:00 am' \
|
||||
# -F 'timetables[all][][end_time]=11:50 am' \
|
||||
# -F 'timetables[all][][location_name]=Room 237' \
|
||||
# -H "Authorization: Bearer <token>"
|
||||
def set_course_timetable
|
||||
get_context
|
||||
if authorized_action(@context, @current_user, :manage_calendar)
|
||||
timetable_data = params[:timetables]
|
||||
|
||||
builders = {}
|
||||
updated_section_ids = []
|
||||
timetable_data.each do |section_id, timetables|
|
||||
timetable_data[section_id] = Array(timetables)
|
||||
section = section_id == 'all' ? nil : api_find(@context.active_course_sections, section_id)
|
||||
updated_section_ids << section.id if section
|
||||
|
||||
builder = Courses::TimetableEventBuilder.new(course: @context, course_section: section)
|
||||
builders[section_id] = builder
|
||||
|
||||
builder.process_and_validate_timetables(timetables)
|
||||
if builder.errors.present?
|
||||
return render :json => {:errors => builder.errors}, :status => :bad_request
|
||||
end
|
||||
end
|
||||
|
||||
@context.timetable_data = timetable_data # so we can retrieve it later
|
||||
@context.save!
|
||||
|
||||
timetable_data.each do |section_id, timetables|
|
||||
builder = builders[section_id]
|
||||
event_hashes = builder.generate_event_hashes(timetables)
|
||||
builder.process_and_validate_event_hashes(event_hashes)
|
||||
raise "error creating timetable events #{builder.errors.join(", ")}" if builder.errors.present?
|
||||
builder.send_later(:create_or_update_events, event_hashes) # someday we may want to make this a trackable progress job /shrug
|
||||
end
|
||||
|
||||
# delete timetable events for sections missing here
|
||||
ignored_section_ids = @context.active_course_sections.where.not(:id => updated_section_ids).pluck(:id)
|
||||
if ignored_section_ids.any?
|
||||
CalendarEvent.active.for_timetable.where(:context_type => "CourseSection", :context_id => ignored_section_ids).
|
||||
update_all(:workflow_state => 'deleted', :deleted_at => Time.now.utc)
|
||||
end
|
||||
|
||||
render :json => {:status => 'ok'}
|
||||
end
|
||||
end
|
||||
|
||||
# @API Get course timetable
|
||||
# @beta
|
||||
#
|
||||
# Returns the last timetable set by the
|
||||
# {api:CalendarEventsApiController#set_course_timetable Set a course timetable} endpoint
|
||||
#
|
||||
def get_course_timetable
|
||||
get_context
|
||||
if authorized_action(@context, @current_user, :manage_calendar)
|
||||
timetable_data = @context.timetable_data || {}
|
||||
render :json => timetable_data
|
||||
end
|
||||
end
|
||||
|
||||
# @API Create or update events directly for a course timetable
|
||||
# @beta
|
||||
#
|
||||
# Creates and updates "timetable" events for a course or course section.
|
||||
# Similar to {api:CalendarEventsApiController#set_course_timetable setting a course timetable},
|
||||
# but instead of generating a list of events based on a timetable schedule,
|
||||
# this endpoint expects a complete list of events.
|
||||
#
|
||||
# @argument course_section_id [Optional, String]
|
||||
# Events will be created for the course section specified by course_section_id.
|
||||
# If not present, events will be created for the entire course.
|
||||
#
|
||||
# @argument events[] [Array]
|
||||
# An array of event objects to use.
|
||||
#
|
||||
# @argument events[][start_at] [DateTime]
|
||||
# Start time for the event
|
||||
#
|
||||
# @argument events[][end_at] [DateTime]
|
||||
# End time for the event
|
||||
#
|
||||
# @argument events[][location_name] [Optional, String]
|
||||
# Location name for the event
|
||||
#
|
||||
# @argument events[][code] [Optional, String]
|
||||
# A unique identifier that can be used to update the event at a later time
|
||||
# If one is not specified, an identifier will be generated based on the start and end times
|
||||
#
|
||||
def set_course_timetable_events
|
||||
get_context
|
||||
if authorized_action(@context, @current_user, :manage_calendar)
|
||||
section = api_find(@context.active_course_sections, params[:course_section_id]) if params[:course_section_id]
|
||||
builder = Courses::TimetableEventBuilder.new(course: @context, course_section: section)
|
||||
|
||||
event_hashes = params[:events]
|
||||
event_hashes.each do |hash|
|
||||
[:start_at, :end_at].each do |key|
|
||||
hash[key] = CanvasTime.try_parse(hash[key])
|
||||
end
|
||||
end
|
||||
builder.process_and_validate_event_hashes(event_hashes)
|
||||
if builder.errors.present?
|
||||
return render :json => {:errors => builder.errors}, :status => :bad_request
|
||||
end
|
||||
|
||||
builder.send_later(:create_or_update_events, event_hashes)
|
||||
render json: {status: 'ok'}
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def get_calendar_context
|
||||
|
|
|
@ -173,6 +173,9 @@ class CalendarEvent < ActiveRecord::Base
|
|||
scope :events_without_child_events, -> { where("NOT EXISTS (SELECT 1 FROM #{CalendarEvent.quoted_table_name} children WHERE children.parent_calendar_event_id = calendar_events.id AND children.workflow_state<>'deleted')") }
|
||||
scope :events_with_child_events, -> { where("EXISTS (SELECT 1 FROM #{CalendarEvent.quoted_table_name} children WHERE children.parent_calendar_event_id = calendar_events.id AND children.workflow_state<>'deleted')") }
|
||||
|
||||
scope :user_created, -> { where(:timetable_code => nil) }
|
||||
scope :for_timetable, -> { where.not(:timetable_code => nil) }
|
||||
|
||||
def validate_context!
|
||||
@validate_context = true
|
||||
context.validation_event_override = self
|
||||
|
|
|
@ -2547,6 +2547,7 @@ class Course < ActiveRecord::Base
|
|||
setting = setting.to_sym
|
||||
settings_options[setting] = opts
|
||||
cast_expression = "val.to_s"
|
||||
cast_expression = "val" if opts[:arbitrary]
|
||||
if opts[:boolean]
|
||||
opts[:default] ||= false
|
||||
cast_expression = "Canvas::Plugin.value_to_boolean(val)"
|
||||
|
@ -2606,6 +2607,8 @@ class Course < ActiveRecord::Base
|
|||
add_setting :restrict_student_future_view, :boolean => true, :inherited => true
|
||||
add_setting :restrict_student_past_view, :boolean => true, :inherited => true
|
||||
|
||||
add_setting :timetable_data, :arbitrary => true
|
||||
|
||||
def user_can_manage_own_discussion_posts?(user)
|
||||
return true if allow_student_discussion_editing?
|
||||
return true if user_is_instructor?(user)
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
module Courses
|
||||
class TimetableEventBuilder
|
||||
# builds calendar events for a course (or course sections) according to a timetable
|
||||
|
||||
attr_reader :course, :course_section, :event_context, :errors
|
||||
def initialize(course: nil, course_section: nil)
|
||||
raise "require course" unless course
|
||||
@course = course
|
||||
@course_section = course_section
|
||||
@event_context = course_section ? course_section : course
|
||||
end
|
||||
|
||||
# generates individual events from a simplified "timetable" between the course/section start and end dates
|
||||
# :weekdays - days of week (0-Sunday, 1-Monday, 2-Tuesday, 3-Wednesday, 4-Thursday, 5-Friday, 6-Saturday)
|
||||
# :start_time - basically any time that can be parsed by the magic (e.g. "11:30 am")
|
||||
# :end_time - ditto
|
||||
# :location_name (optional)
|
||||
def generate_event_hashes(timetable_hashes)
|
||||
time_zone = course.time_zone
|
||||
event_hashes = []
|
||||
timetable_hashes.each do |timetable_hash|
|
||||
course_start_at = timetable_hash[:course_start_at]
|
||||
course_end_at = timetable_hash[:course_end_at]
|
||||
|
||||
parse_weekdays_string(timetable_hash[:weekdays]).each do |wday|
|
||||
location_name = timetable_hash[:location_name]
|
||||
|
||||
current_date = course_start_at.to_date
|
||||
if current_date.wday != wday
|
||||
offset = wday - current_date.wday
|
||||
offset += 7 if offset < 0
|
||||
current_date += offset # should be on the right day now
|
||||
end
|
||||
|
||||
while current_date < course_end_at
|
||||
event_start_at = time_zone.parse("#{current_date} #{timetable_hash[:start_time]}")
|
||||
event_end_at = time_zone.parse("#{current_date} #{timetable_hash[:end_time]}")
|
||||
|
||||
if event_start_at > course_start_at && event_end_at < course_end_at
|
||||
event_hash = {:start_at => event_start_at, :end_at => event_end_at}
|
||||
event_hash[:location_name] = location_name if location_name
|
||||
event_hashes << event_hash
|
||||
end
|
||||
current_date += 7 # move to next week
|
||||
end
|
||||
end
|
||||
end
|
||||
event_hashes
|
||||
end
|
||||
|
||||
# expects an array of hashes
|
||||
# with :start_at, :end_at required
|
||||
# and optionally :location_name (other attributes could be added here if so desired)
|
||||
# :code can be used to give it a unique identifier for syncing (otherwise will be generated based on the times)
|
||||
def create_or_update_events(event_hashes)
|
||||
timetable_codes = event_hashes.map{|h| h[:code]}
|
||||
raise "timetable codes can't be blank" if timetable_codes.any?(&:blank?)
|
||||
|
||||
# destroy unused events
|
||||
event_context.calendar_events.active.for_timetable.where.not(:timetable_code => timetable_codes).
|
||||
update_all(:workflow_state => 'deleted', :deleted_at => Time.now.utc)
|
||||
|
||||
existing_events = event_context.calendar_events.where(:timetable_code => timetable_codes).to_a.index_by(&:timetable_code)
|
||||
event_hashes.each do |event_hash|
|
||||
CalendarEvent.unique_constraint_retry do |retry_count|
|
||||
code = event_hash[:code]
|
||||
event = event_context.calendar_events.where(:timetable_code => code).first if retry_count > 0
|
||||
event ||= existing_events[code] || create_new_event(event_hash)
|
||||
sync_event(event, event_hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ALLOWED_TIMETABLE_KEYS = [:weekdays, :course_start_at, :course_end_at, :start_time, :end_time, :location_name]
|
||||
def process_and_validate_timetables(timetable_hashes)
|
||||
timetable_hashes.each do |hash|
|
||||
hash.slice!(*ALLOWED_TIMETABLE_KEYS)
|
||||
end
|
||||
unless timetable_hashes.all?{|hash| Time.parse(hash[:start_time]) rescue nil}
|
||||
add_error("invalid start time(s)") # i'm too lazy to be more specific
|
||||
end
|
||||
unless timetable_hashes.all?{|hash| Time.parse(hash[:end_time]) rescue nil}
|
||||
add_error("invalid end time(s)")
|
||||
end
|
||||
|
||||
default_start_at = (course_section && course_section.start_at) || course.start_at || course.enrollment_term.start_at
|
||||
default_end_at = (course_section && course_section.end_at) || course.conclude_at || course.enrollment_term.end_at
|
||||
|
||||
timetable_hashes.each do |hash|
|
||||
hash[:course_start_at] ||= default_start_at
|
||||
hash[:course_end_at] ||= default_end_at
|
||||
hash[:weekdays] = standardize_weekdays_string(hash[:weekdays])
|
||||
end
|
||||
add_error("no start date found") unless timetable_hashes.all?{|hash| hash[:course_start_at]}
|
||||
add_error("no end date found") unless timetable_hashes.all?{|hash| hash[:course_end_at]}
|
||||
end
|
||||
|
||||
def process_and_validate_event_hashes(event_hashes)
|
||||
add_error("start_at and end_at are required") unless event_hashes.all?{|h| h[:start_at] && h[:end_at]}
|
||||
|
||||
event_hashes.each do |event_hash|
|
||||
event_hash[:code] ||= generate_timetable_code_for(event_hash) # ensure timetable codes
|
||||
end
|
||||
timetable_codes = event_hashes.map{|h| h[:code]}
|
||||
add_error("events (or codes) are not unique") unless timetable_codes.uniq.count == timetable_codes.count # too lazy to be specific here too
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
WEEKDAY_STR_MAP = {
|
||||
'sunday' => 'Sun', 'monday' => 'Mon', 'tuesday' => 'Tue', 'wednesday' => 'Wed', 'thursday' => 'Thu', 'friday' => 'Fri', 'saturday' => 'Sat',
|
||||
'su' => 'Sun', 'm' => 'Mon', 't' => 'Tue', 'w' => 'Wed', 'th' => 'Thu', 'f' => 'Fri', 's' => 'Sat'
|
||||
}.freeze
|
||||
def standardize_weekdays_string(weekdays_string)
|
||||
# turn strings like "M,W" into a standard string "Mon,Wed" (for sending back to the client)
|
||||
weekday_strs = weekdays_string.split(",").map do |str|
|
||||
str = str.strip.downcase
|
||||
WEEKDAY_STR_MAP[str] || str.capitalize
|
||||
end
|
||||
if weekday_strs.any?{|s| !WEEKDAY_TO_INT_MAP[s]}
|
||||
add_error("weekdays are not valid")
|
||||
nil
|
||||
else
|
||||
weekday_strs.join(",")
|
||||
end
|
||||
end
|
||||
|
||||
WEEKDAY_TO_INT_MAP = {
|
||||
'Sun' => 0, 'Mon' => 1, 'Tue' => 2, 'Wed' => 3, 'Thu' => 4, 'Fri' => 5, 'Sat' => 6
|
||||
}.freeze
|
||||
def parse_weekdays_string(weekdays_string)
|
||||
# turn our standard string (e.g. "Tue,Thu") into an array of our special numbers
|
||||
weekdays_string.split(",").map{|s| WEEKDAY_TO_INT_MAP[s]}
|
||||
end
|
||||
|
||||
def create_new_event(event_hash)
|
||||
event = event_context.calendar_events.new
|
||||
event.timetable_code = event_hash[:code]
|
||||
if course_section
|
||||
event.effective_context_code = course.asset_string
|
||||
end
|
||||
event
|
||||
end
|
||||
|
||||
def sync_event(event, event_hash)
|
||||
event.workflow_state = 'active'
|
||||
event.title = event_hash[:title] || course.name
|
||||
event.start_at = event_hash[:start_at]
|
||||
event.end_at = event_hash[:end_at]
|
||||
event.location_name = event_hash[:location_name] if event_hash[:location_name]
|
||||
event.save! if event.changed?
|
||||
end
|
||||
|
||||
def generate_timetable_code_for(event_hash)
|
||||
"#{event_context.asset_string}_#{event_hash[:start_at].to_i}-#{event_hash[:end_at].to_i}"
|
||||
end
|
||||
|
||||
def add_error(error)
|
||||
@errors ||= []
|
||||
@errors << error
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1355,6 +1355,10 @@ CanvasRails::Application.routes.draw do
|
|||
post 'calendar_events/:id/reservations', action: :reserve
|
||||
post 'calendar_events/:id/reservations/:participant_id', action: :reserve, as: 'calendar_event_reserve'
|
||||
post 'calendar_events/save_selected_contexts', action: :save_selected_contexts
|
||||
|
||||
get 'courses/:course_id/calendar_events/timetable', action: :get_course_timetable
|
||||
post 'courses/:course_id/calendar_events/timetable', action: :set_course_timetable
|
||||
post 'courses/:course_id/calendar_events/timetable_events', action: :set_course_timetable_events
|
||||
end
|
||||
|
||||
scope(controller: :appointment_groups) do
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
class AddTimetableCodeToCalendarEvents < ActiveRecord::Migration
|
||||
tag :predeploy
|
||||
|
||||
def change
|
||||
add_column :calendar_events, :timetable_code, :string
|
||||
add_index :calendar_events, [:context_id, :context_type, :timetable_code], where: "timetable_code IS NOT NULL",
|
||||
unique: true, algorithm: :concurrently, name: "index_calendar_events_on_context_and_timetable_code"
|
||||
end
|
||||
end
|
|
@ -18,7 +18,8 @@
|
|||
module CC
|
||||
module Events
|
||||
def create_events(document=nil)
|
||||
return nil unless @course.calendar_events.active.count > 0
|
||||
calendar_event_scope = @course.calendar_events.active.user_created
|
||||
return nil unless calendar_event_scope.count > 0
|
||||
|
||||
if document
|
||||
events_file = nil
|
||||
|
@ -35,7 +36,7 @@ module CC
|
|||
"xmlns:xsi"=>"http://www.w3.org/2001/XMLSchema-instance",
|
||||
"xsi:schemaLocation"=> "#{CCHelper::CANVAS_NAMESPACE} #{CCHelper::XSD_URI}"
|
||||
) do |events_node|
|
||||
@course.calendar_events.active.each do |event|
|
||||
calendar_event_scope.each do |event|
|
||||
next unless export_object?(event)
|
||||
add_exported_asset(event)
|
||||
migration_id = CCHelper.create_key(event)
|
||||
|
|
|
@ -1956,7 +1956,6 @@ describe CalendarEventsApiController, type: :request do
|
|||
it 'includes custom colors' do
|
||||
@user.custom_colors[@course.asset_string] = '#0099ff'
|
||||
@user.save!
|
||||
puts @user.inspect
|
||||
|
||||
json = api_call(:get, '/api/v1/calendar_events/visible_contexts', {
|
||||
controller: 'calendar_events_api',
|
||||
|
@ -1986,4 +1985,142 @@ describe CalendarEventsApiController, type: :request do
|
|||
expect(context['selected']).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#set_course_timetable' do
|
||||
before :once do
|
||||
@path = "/api/v1/courses/#{@course.id}/calendar_events/timetable"
|
||||
@course.start_at = DateTime.parse("2016-05-06 1:00pm -0600")
|
||||
@course.conclude_at = DateTime.parse("2016-05-19 9:00am -0600")
|
||||
@course.time_zone = 'America/Denver'
|
||||
@course.save!
|
||||
end
|
||||
|
||||
it "should check for valid options" do
|
||||
timetables = {"all" => [{:weekdays => "moonday", :start_time => "not a real time", :end_time => "this either"}]}
|
||||
json = api_call(:post, @path, {
|
||||
:course_id => @course.id.to_param, :controller => 'calendar_events_api',
|
||||
:action => 'set_course_timetable', :format => 'json'
|
||||
}, {:timetables => timetables}, {}, {:expected_status => 400})
|
||||
|
||||
expect(json['errors']).to match_array(["invalid start time(s)", "invalid end time(s)", "weekdays are not valid"])
|
||||
end
|
||||
|
||||
it "should create course-level events" do
|
||||
location_name = 'best place evr'
|
||||
timetables = {"all" => [{:weekdays => "monday, thursday", :start_time => "2:00 pm",
|
||||
:end_time => "3:30 pm", :location_name => location_name}]}
|
||||
|
||||
expect {
|
||||
json = api_call(:post, @path, {
|
||||
:course_id => @course.id.to_param, :controller => 'calendar_events_api',
|
||||
:action => 'set_course_timetable', :format => 'json'
|
||||
}, {:timetables => timetables})
|
||||
}.to change(Delayed::Job, :count).by(1)
|
||||
|
||||
run_jobs
|
||||
|
||||
expected_events = [
|
||||
{ :start_at => DateTime.parse("2016-05-09 2:00 pm -0600"), :end_at => DateTime.parse("2016-05-09 3:30 pm -0600")},
|
||||
{ :start_at => DateTime.parse("2016-05-12 2:00 pm -0600"), :end_at => DateTime.parse("2016-05-12 3:30 pm -0600")},
|
||||
{ :start_at => DateTime.parse("2016-05-16 2:00 pm -0600"), :end_at => DateTime.parse("2016-05-16 3:30 pm -0600")}
|
||||
]
|
||||
events = @course.calendar_events.for_timetable.to_a
|
||||
expect(events.map{|e| {:start_at => e.start_at, :end_at => e.end_at}}).to match_array(expected_events)
|
||||
expect(events.map(&:location_name).uniq).to eq [location_name]
|
||||
end
|
||||
|
||||
it "should create section-level events" do
|
||||
section1 = @course.course_sections.create!
|
||||
section2 = @course.course_sections.new
|
||||
section2.sis_source_id = "sisss" # can even find by sis id, yay!
|
||||
section2.end_at = DateTime.parse("2016-05-25 9:00am -0600") # and also extend dates on the section
|
||||
section2.save!
|
||||
|
||||
timetables = {
|
||||
section1.id => [{:weekdays => "Mon", :start_time => "2:00 pm", :end_time => "3:30 pm"}],
|
||||
"sis_section_id:#{section2.sis_source_id}" => [{:weekdays => "Thu", :start_time => "3:30 pm", :end_time => "4:30 pm"}]
|
||||
}
|
||||
|
||||
expect {
|
||||
json = api_call(:post, @path, {
|
||||
:course_id => @course.id.to_param, :controller => 'calendar_events_api',
|
||||
:action => 'set_course_timetable', :format => 'json'
|
||||
}, {:timetables => timetables})
|
||||
}.to change(Delayed::Job, :count).by(2)
|
||||
|
||||
run_jobs
|
||||
|
||||
expected_events1 = [
|
||||
{ :start_at => DateTime.parse("2016-05-09 2:00 pm -0600"), :end_at => DateTime.parse("2016-05-09 3:30 pm -0600")},
|
||||
{ :start_at => DateTime.parse("2016-05-16 2:00 pm -0600"), :end_at => DateTime.parse("2016-05-16 3:30 pm -0600")}
|
||||
]
|
||||
events1 = section1.calendar_events.for_timetable.to_a
|
||||
expect(events1.map{|e| {:start_at => e.start_at, :end_at => e.end_at}}).to match_array(expected_events1)
|
||||
|
||||
expected_events2 = [
|
||||
{ :start_at => DateTime.parse("2016-05-12 3:30 pm -0600"), :end_at => DateTime.parse("2016-05-12 4:30 pm -0600")},
|
||||
{ :start_at => DateTime.parse("2016-05-19 3:30 pm -0600"), :end_at => DateTime.parse("2016-05-19 4:30 pm -0600")}
|
||||
]
|
||||
events2 = section2.calendar_events.for_timetable.to_a
|
||||
expect(events2.map{|e| {:start_at => e.start_at, :end_at => e.end_at}}).to match_array(expected_events2)
|
||||
end
|
||||
|
||||
it "should be able to retrieve the timetable afterwards" do
|
||||
timetables = {"all" => [{:weekdays => "monday, thursday", :start_time => "2:00 pm", :end_time => "3:30 pm"}]}
|
||||
|
||||
#set the timetables
|
||||
api_call(:post, @path, {
|
||||
:course_id => @course.id.to_param, :controller => 'calendar_events_api',
|
||||
:action => 'set_course_timetable', :format => 'json'
|
||||
}, {:timetables => timetables})
|
||||
|
||||
json = api_call(:get, @path, {
|
||||
:course_id => @course.id.to_param, :controller => 'calendar_events_api',
|
||||
:action => 'get_course_timetable', :format => 'json'
|
||||
})
|
||||
|
||||
expected = {"all" => [{'weekdays' => "Mon,Thu", 'start_time' => "2:00 pm", 'end_time' => "3:30 pm",
|
||||
'course_start_at' => @course.start_at.iso8601, 'course_end_at' => @course.end_at.iso8601}]}
|
||||
expect(json).to eq expected
|
||||
end
|
||||
end
|
||||
|
||||
describe '#set_course_timetable_events' do
|
||||
before :once do
|
||||
@path = "/api/v1/courses/#{@course.id}/calendar_events/timetable_events"
|
||||
@events = [
|
||||
{ :start_at => DateTime.parse("2016-05-09 2:00 pm -0600"), :end_at => DateTime.parse("2016-05-09 3:30 pm -0600")},
|
||||
{ :start_at => DateTime.parse("2016-05-12 2:00 pm -0600"), :end_at => DateTime.parse("2016-05-12 3:30 pm -0600")},
|
||||
]
|
||||
end
|
||||
|
||||
it "should be able to create a bunch of events directly from a list" do
|
||||
expect {
|
||||
api_call(:post, @path, {
|
||||
:course_id => @course.id.to_param, :controller => 'calendar_events_api',
|
||||
:action => 'set_course_timetable_events', :format => 'json'
|
||||
}, {:events => @events})
|
||||
}.to change(Delayed::Job, :count).by(1)
|
||||
|
||||
run_jobs
|
||||
|
||||
events = @course.calendar_events.for_timetable.to_a
|
||||
expect(events.map{|e| {:start_at => e.start_at, :end_at => e.end_at}}).to match_array(@events)
|
||||
end
|
||||
|
||||
it "should be able to create events for a course section" do
|
||||
section = @course.course_sections.create!
|
||||
expect {
|
||||
api_call(:post, @path, {
|
||||
:course_id => @course.id.to_param, :controller => 'calendar_events_api',
|
||||
:action => 'set_course_timetable_events', :format => 'json'
|
||||
}, {:events => @events, :course_section_id => section.id.to_param})
|
||||
}.to change(Delayed::Job, :count).by(1)
|
||||
|
||||
run_jobs
|
||||
|
||||
events = section.calendar_events.for_timetable.to_a
|
||||
expect(events.map{|e| {:start_at => e.start_at, :end_at => e.end_at}}).to match_array(@events)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
require_relative('../../spec_helper')
|
||||
|
||||
describe Courses::TimetableEventBuilder do
|
||||
describe "#process_and_validate_timetables" do
|
||||
let(:builder) { described_class.new(course: course) }
|
||||
|
||||
it "should require valid start and end times" do
|
||||
tt_hash = {:weekdays => 'monday', :start_time => "hoopyfrood", :end_time => "42 oclock",
|
||||
:course_start_at => 1.day.from_now, :course_end_at => 1.week.from_now}
|
||||
builder.process_and_validate_timetables([tt_hash])
|
||||
expect(builder.errors).to match_array(["invalid start time(s)", "invalid end time(s)"])
|
||||
end
|
||||
|
||||
it "should require a start and end date" do
|
||||
tt_hash = {:weekdays => 'tuesday', :start_time => "11:30 am", :end_time => "12:30 pm"}
|
||||
builder.process_and_validate_timetables([tt_hash])
|
||||
expect(builder.errors).to match_array(["no start date found", "no end date found"])
|
||||
end
|
||||
|
||||
it "should require valid weekdays" do
|
||||
builder.course.enrollment_term.tap do |term|
|
||||
term.start_at = DateTime.parse("2016-05-06 1:00pm -0600")
|
||||
term.end_at = DateTime.parse("2016-05-19 9:00am -0600")
|
||||
end
|
||||
|
||||
tt_hash = {:weekdays => 'wednesday,humpday', :start_time => "11:30 am", :end_time => "12:30 pm"}
|
||||
builder.process_and_validate_timetables([tt_hash])
|
||||
expect(builder.errors).to match_array(["weekdays are not valid"])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#generate_event_hashes" do
|
||||
let(:builder) { described_class.new(course: course) }
|
||||
|
||||
it "should generate a bunch of event hashes" do
|
||||
builder.course.tap do |c|
|
||||
c.start_at = DateTime.parse("2016-05-06 1:00pm -0600") # on a friday - should offset to thursday
|
||||
c.conclude_at = DateTime.parse("2016-05-19 9:00am -0600") # on a thursday, but before the course time - shouldn't create an event that day
|
||||
c.time_zone = 'America/Denver'
|
||||
end
|
||||
|
||||
tt_hash = {:weekdays => "T,Th", :start_time => "3 pm", :end_time => "4:30 pm"} # tuesdays and thursdays from 3:00-4:30pm
|
||||
builder.process_and_validate_timetables([tt_hash])
|
||||
expect(builder.errors).to be_blank
|
||||
|
||||
expected_events = [
|
||||
{ :start_at => DateTime.parse("2016-05-10 3:00 pm -0600"), :end_at => DateTime.parse("2016-05-10 4:30 pm -0600")},
|
||||
{ :start_at => DateTime.parse("2016-05-12 3:00 pm -0600"), :end_at => DateTime.parse("2016-05-12 4:30 pm -0600")},
|
||||
{ :start_at => DateTime.parse("2016-05-17 3:00 pm -0600"), :end_at => DateTime.parse("2016-05-17 4:30 pm -0600")}
|
||||
]
|
||||
expect(builder.generate_event_hashes([tt_hash])).to match_array(expected_events)
|
||||
end
|
||||
|
||||
it "should work across daylight savings time changes (sigh)" do
|
||||
builder.course.tap do |c|
|
||||
c.start_at = DateTime.parse("2016-03-09 1:00pm -0600") # on a wednesday
|
||||
# DST transition happened across March 13, 2016
|
||||
c.conclude_at = DateTime.parse("2016-03-18 8:00pm -0600") # on a friday, but after the course time - should create an event that day
|
||||
c.time_zone = 'America/Denver'
|
||||
end
|
||||
|
||||
tt_hash = {:weekdays => "Monday,Friday", :start_time => "11:30", :end_time => "13:00"} # mondays and fridays from 11:30-1:00
|
||||
builder.process_and_validate_timetables([tt_hash])
|
||||
expect(builder.errors).to be_blank
|
||||
# should convert :weekdays to a standard format
|
||||
expect(tt_hash[:weekdays]).to eq "Mon,Fri"
|
||||
|
||||
expected_events = [
|
||||
{ :start_at => DateTime.parse("2016-03-11 11:30 am -0700"), :end_at => DateTime.parse("2016-03-11 1:00 pm -0700")},
|
||||
{ :start_at => DateTime.parse("2016-03-14 11:30 am -0600"), :end_at => DateTime.parse("2016-03-14 1:00 pm -0600")},
|
||||
{ :start_at => DateTime.parse("2016-03-18 11:30 am -0600"), :end_at => DateTime.parse("2016-03-18 1:00 pm -0600")}
|
||||
]
|
||||
expect(builder.generate_event_hashes([tt_hash])).to match_array(expected_events)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#process_and_validate_event_hashes" do
|
||||
let(:builder) { described_class.new(course: course) }
|
||||
|
||||
it "should require start_at and end_at" do
|
||||
event_hash = {}
|
||||
builder.process_and_validate_event_hashes([event_hash])
|
||||
expect(builder.errors).to eq ["start_at and end_at are required"]
|
||||
end
|
||||
|
||||
it "should require unique dates" do
|
||||
start_at = 1.day.from_now
|
||||
end_at = 1.day.from_now + 2.hours
|
||||
event_hashes = [
|
||||
{:start_at => start_at, :end_at => end_at},
|
||||
{:start_at => start_at, :end_at => end_at}
|
||||
]
|
||||
builder.process_and_validate_event_hashes(event_hashes)
|
||||
expect(builder.errors).to eq ["events (or codes) are not unique"]
|
||||
end
|
||||
end
|
||||
|
||||
describe "#create_or_update_events" do
|
||||
before :once do
|
||||
course
|
||||
@section = @course.course_sections.create!
|
||||
@course_builder = described_class.new(course: @course)
|
||||
@section_builder = described_class.new(course: @course, course_section: @section)
|
||||
|
||||
@start_at = 1.day.from_now
|
||||
@end_at = 1.day.from_now + 1.hour
|
||||
@start_at2 = 2.days.from_now
|
||||
@end_at2 = 2.days.from_now + 1.hour
|
||||
end
|
||||
|
||||
it "should generate timetable dates for a course" do
|
||||
event_hashes = [
|
||||
{:start_at => @start_at, :end_at => @end_at},
|
||||
{:start_at => @start_at2, :end_at => @end_at2}
|
||||
]
|
||||
@course_builder.process_and_validate_event_hashes(event_hashes)
|
||||
expect(@course_builder.errors).to be_blank
|
||||
@course_builder.create_or_update_events(event_hashes)
|
||||
events = @course.calendar_events.for_timetable.to_a
|
||||
expect(events.count).to eq 2
|
||||
expect(events.map(&:start_at)).to match_array([@start_at, @start_at2])
|
||||
expect(events.map(&:end_at)).to match_array([@end_at, @end_at2])
|
||||
end
|
||||
|
||||
it "should generate timetable dates for a course section" do
|
||||
event_hashes = [
|
||||
{:start_at => @start_at, :end_at => @end_at},
|
||||
{:start_at => @start_at2, :end_at => @end_at2}
|
||||
]
|
||||
@section_builder.process_and_validate_event_hashes(event_hashes)
|
||||
expect(@section_builder.errors).to be_blank
|
||||
@section_builder.create_or_update_events(event_hashes)
|
||||
|
||||
events = @section.calendar_events.for_timetable.to_a
|
||||
expect(events.count).to eq 2
|
||||
expect(events.map(&:start_at)).to match_array([@start_at, @start_at2])
|
||||
expect(events.map(&:end_at)).to match_array([@end_at, @end_at2])
|
||||
expect(events.first.effective_context_code).to eq @course.asset_string
|
||||
end
|
||||
|
||||
it "should remove or update existing timetable dates" do
|
||||
event_hashes = [
|
||||
{:start_at => @start_at, :end_at => @end_at},
|
||||
{:start_at => @start_at2, :end_at => @end_at2}
|
||||
]
|
||||
@course_builder.process_and_validate_event_hashes(event_hashes)
|
||||
expect(@course_builder.errors).to be_blank
|
||||
@course_builder.create_or_update_events(event_hashes)
|
||||
|
||||
ce1 = @course.calendar_events.for_timetable.to_a.detect{|ce| ce.start_at == @start_at}
|
||||
ce2 = @course.calendar_events.for_timetable.to_a.detect{|ce| ce.start_at == @start_at2}
|
||||
|
||||
location = "under the sea"
|
||||
event_hashes2 = [{:start_at => @start_at, :end_at => @end_at, :location_name => location}]
|
||||
@course_builder.process_and_validate_event_hashes(event_hashes2)
|
||||
expect(@course_builder.errors).to be_blank
|
||||
@course_builder.create_or_update_events(event_hashes2)
|
||||
|
||||
expect(ce1.reload.location_name).to eq location
|
||||
expect(ce2.reload).to be_deleted
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue